drum-machine / index.html
IgorSlinko's picture
nice. but the grid became not clickable. return the grid to the state when I can click on the boxes - Initial Deployment
b6d4ada verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Drum Machine - Кахон</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 playHead {
from { left: 0; }
to { left: 100%; }
}
.play-head {
animation: playHead linear infinite;
animation-duration: var(--bpm);
}
.note {
transition: all 0.1s ease;
}
.note.active {
transform: scale(1.05);
box-shadow: 0 0 10px rgba(255, 255, 255, 0.8);
}
.pulse {
animation: pulse 0.5s;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.staff-container {
position: relative;
height: 120px;
margin-bottom: 20px;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
}
.staff {
position: relative;
display: inline-block;
height: 100px;
width: 800px;
background-color: rgba(255, 255, 255, 0.05);
border-radius: 4px;
}
.staff-line {
position: absolute;
width: 100%;
height: 1px;
background-color: white;
}
.staff-line:nth-child(1) { top: 10%; }
.staff-line:nth-child(2) { top: 30%; }
.staff-line:nth-child(3) { top: 50%; }
.staff-line:nth-child(4) { top: 70%; }
.staff-line:nth-child(5) { top: 90%; }
.neutral-clef {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
font-size: 40px;
font-weight: bold;
color: white;
}
.bar-line {
position: absolute;
top: 0;
height: 100%;
width: 2px;
background-color: white;
}
.drum-note {
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
background-color: white;
transform: translateX(-50%);
}
.drum-note.bass { top: 90%; }
.drum-note.snare { top: 50%; }
.drum-note.hihat { top: 30%; }
.drum-note.rim { top: 70%; }
.note-stem {
position: absolute;
width: 2px;
background-color: white;
transform-origin: top center;
}
</style>
</head>
<body class="bg-gray-900 text-white min-h-screen flex flex-col">
<header class="py-6 bg-gray-800 shadow-lg">
<div class="container mx-auto px-4">
<h1 class="text-3xl font-bold text-center text-yellow-400">
<i class="fas fa-drum mr-2"></i> Drum Machine - Кахон
</h1>
</div>
</header>
<main class="flex-grow container mx-auto px-4 py-8">
<div class="flex flex-col lg:flex-row gap-8">
<!-- Controls -->
<div class="lg:w-1/4 bg-gray-800 p-6 rounded-lg shadow-lg">
<div class="mb-6">
<h2 class="text-xl font-semibold mb-4 text-yellow-400">Controls</h2>
<div class="flex items-center justify-between mb-4">
<button id="playBtn" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center">
<i class="fas fa-play mr-2"></i> Play
</button>
<button id="stopBtn" class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg flex items-center">
<i class="fas fa-stop mr-2"></i> Stop
</button>
</div>
<div class="mb-4">
<label for="bpm" class="block mb-2">BPM: <span id="bpmValue">120</span></label>
<input type="range" id="bpm" min="60" max="240" value="120" class="w-full accent-yellow-500">
</div>
<div class="mb-4">
<label for="volume" class="block mb-2">Volume: <span id="volumeValue">80</span>%</label>
<input type="range" id="volume" min="0" max="100" value="80" class="w-full accent-yellow-500">
</div>
<div class="mb-4">
<label for="steps" class="block mb-2">Steps: <span id="stepsValue">16</span></label>
<input type="range" id="steps" min="4" max="32" step="4" value="16" class="w-full accent-yellow-500">
</div>
<button id="clearBtn" class="w-full bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg flex items-center justify-center">
<i class="fas fa-trash-alt mr-2"></i> Clear All
</button>
</div>
<div class="mb-6">
<h2 class="text-xl font-semibold mb-4 text-yellow-400">Sounds</h2>
<div class="grid grid-cols-2 gap-2">
<button data-sound="bass" class="sound-btn bg-blue-600 hover:bg-blue-700 text-white p-2 rounded flex items-center justify-center">
<i class="fas fa-drum mr-2"></i> Bass
</button>
<button data-sound="snare" class="sound-btn bg-purple-600 hover:bg-purple-700 text-white p-2 rounded flex items-center justify-center">
<i class="fas fa-drum mr-2"></i> Snare
</button>
<button data-sound="hihat" class="sound-btn bg-green-600 hover:bg-green-700 text-white p-2 rounded flex items-center justify-center">
<i class="fas fa-drum mr-2"></i> Hi-Hat
</button>
<button data-sound="rim" class="sound-btn bg-yellow-600 hover:bg-yellow-700 text-white p-2 rounded flex items-center justify-center">
<i class="fas fa-drum mr-2"></i> Rim
</button>
</div>
</div>
<div>
<h2 class="text-xl font-semibold mb-4 text-yellow-400">Pattern</h2>
<select id="patternSelect" class="w-full bg-gray-700 text-white p-2 rounded mb-2">
<option value="empty">Empty</option>
<option value="basic">Basic Pattern</option>
<option value="complex">Complex Pattern</option>
<option value="random">Random</option>
</select>
<div class="flex gap-2">
<button id="savePatternBtn" class="flex-1 bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg">
Save
</button>
<button id="loadPatternBtn" class="flex-1 bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg">
Load
</button>
</div>
</div>
</div>
<!-- Drum Machine Grid -->
<div class="lg:w-3/4">
<div class="bg-gray-800 p-6 rounded-lg shadow-lg">
<h2 class="text-xl font-semibold mb-4 text-yellow-400">Drum Grid</h2>
<!-- Playhead -->
<div class="relative h-2 mb-4 bg-gray-700 rounded overflow-hidden">
<div id="playHead" class="absolute top-0 left-0 w-1 h-2 bg-yellow-400 z-10" style="display: none;"></div>
</div>
<!-- Note Visualization -->
<div id="noteVisualization" class="mb-6 p-4 bg-gray-700 rounded-lg">
<div class="text-center text-gray-400">Notes will appear here when playing</div>
</div>
<!-- Staff Visualization -->
<div id="staffVisualization" class="mb-6 p-4 bg-gray-700 rounded-lg">
<h3 class="text-lg font-semibold mb-2">Drum Notation</h3>
<div class="staff-container">
<div class="staff" id="drumStaff">
<!-- Staff lines -->
<div class="staff-line"></div>
<div class="staff-line"></div>
<div class="staff-line"></div>
<div class="staff-line"></div>
<div class="staff-line"></div>
<!-- Neutral clef -->
<div class="neutral-clef">𝄽</div>
<!-- Bar lines will be added here -->
<!-- Notes will be added here -->
</div>
</div>
</div>
<!-- Drum Grid -->
<div class="overflow-x-auto">
<table id="drumGrid" class="w-full">
<thead>
<tr>
<th class="w-16 h-8 bg-gray-700"></th>
<!-- Step numbers -->
</tr>
</thead>
<tbody>
<!-- Rows will be added by JavaScript -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</main>
<footer class="py-4 bg-gray-800 text-center text-gray-400">
<div class="container mx-auto px-4">
<p>Drum Machine - Кахон | Created with HTML, CSS & JavaScript</p>
</div>
</footer>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Audio context
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioContext = new AudioContext();
const masterGain = audioContext.createGain();
masterGain.gain.value = 0.8;
masterGain.connect(audioContext.destination);
// Sound samples (base64 encoded short samples for demo purposes)
const sounds = {
bass: {
name: "Bass",
color: "bg-blue-600",
sample: generateKick()
},
snare: {
name: "Snare",
color: "bg-purple-600",
sample: generateSnare()
},
hihat: {
name: "Hi-Hat",
color: "bg-green-600",
sample: generateHiHat()
},
rim: {
name: "Rim",
color: "bg-yellow-600",
sample: generateRim()
}
};
// Generate simple drum sounds (for demo)
function generateKick() {
const sampleRate = audioContext.sampleRate;
const duration = 0.5;
const numFrames = duration * sampleRate;
const buffer = audioContext.createBuffer(1, numFrames, sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < numFrames; i++) {
const n = -0.5 * Math.cos(i / (sampleRate * 0.002)) + 0.5;
data[i] = n * Math.exp(-i / (sampleRate * 0.1));
}
return buffer;
}
function generateSnare() {
const sampleRate = audioContext.sampleRate;
const duration = 0.3;
const numFrames = duration * sampleRate;
const buffer = audioContext.createBuffer(1, numFrames, sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < numFrames; i++) {
const noise = Math.random() * 2 - 1;
const env = Math.exp(-i / (sampleRate * 0.1));
data[i] = noise * env;
}
return buffer;
}
function generateHiHat() {
const sampleRate = audioContext.sampleRate;
const duration = 0.1;
const numFrames = duration * sampleRate;
const buffer = audioContext.createBuffer(1, numFrames, sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < numFrames; i++) {
const noise = Math.random() * 2 - 1;
const env = 1 - (i / numFrames);
data[i] = noise * env;
}
return buffer;
}
function generateRim() {
const sampleRate = audioContext.sampleRate;
const duration = 0.2;
const numFrames = duration * sampleRate;
const buffer = audioContext.createBuffer(1, numFrames, sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < numFrames; i++) {
const n = Math.sin(i / (sampleRate * 0.0005));
data[i] = n * Math.exp(-i / (sampleRate * 0.05));
}
return buffer;
}
// Play a sound
function playSound(sound) {
const source = audioContext.createBufferSource();
source.buffer = sound.sample;
source.connect(masterGain);
source.start();
}
// State variables
let isPlaying = false;
let currentStep = 0;
let timerId = null;
let bpm = 120;
let steps = 16;
let selectedSound = 'bass';
let pattern = {};
// Initialize pattern
function initPattern() {
pattern = {};
Object.keys(sounds).forEach(sound => {
pattern[sound] = Array(steps).fill(false);
});
}
initPattern();
// DOM elements
const drumGrid = document.getElementById('drumGrid');
const playBtn = document.getElementById('playBtn');
const stopBtn = document.getElementById('stopBtn');
const bpmSlider = document.getElementById('bpm');
const bpmValue = document.getElementById('bpmValue');
const volumeSlider = document.getElementById('volume');
const volumeValue = document.getElementById('volumeValue');
const stepsSlider = document.getElementById('steps');
const stepsValue = document.getElementById('stepsValue');
const clearBtn = document.getElementById('clearBtn');
const soundBtns = document.querySelectorAll('.sound-btn');
const playHead = document.getElementById('playHead');
const noteVisualization = document.getElementById('noteVisualization');
const patternSelect = document.getElementById('patternSelect');
const savePatternBtn = document.getElementById('savePatternBtn');
const loadPatternBtn = document.getElementById('loadPatternBtn');
// Create drum grid
function createDrumGrid() {
// Clear existing grid
drumGrid.querySelector('tbody').innerHTML = '';
// Create header with step numbers
const headerRow = drumGrid.querySelector('thead tr');
headerRow.innerHTML = '<th class="w-16 h-8 bg-gray-700"></th>';
for (let i = 0; i < steps; i++) {
const th = document.createElement('th');
th.className = 'w-8 h-8 bg-gray-700 text-xs text-center';
th.textContent = i + 1;
headerRow.appendChild(th);
}
// Create rows for each sound
Object.keys(sounds).forEach(sound => {
const tr = document.createElement('tr');
// Sound name cell
const th = document.createElement('th');
th.className = 'w-16 h-8 bg-gray-700 text-xs text-center';
th.textContent = sounds[sound].name;
tr.appendChild(th);
// Create cells for each step
for (let i = 0; i < steps; i++) {
const td = document.createElement('td');
td.className = 'w-8 h-8 p-0 border border-gray-700';
const cell = document.createElement('div');
cell.className = 'w-full h-full cursor-pointer note hover:bg-gray-600';
cell.dataset.sound = sound;
cell.dataset.step = i;
if (pattern[sound][i]) {
cell.classList.add(sounds[sound].color);
} else {
cell.classList.add('bg-gray-800');
}
cell.addEventListener('click', function() {
toggleStep(sound, i);
});
td.appendChild(cell);
tr.appendChild(td);
}
drumGrid.querySelector('tbody').appendChild(tr);
});
}
// Toggle step on/off
function toggleStep(sound, step) {
pattern[sound][step] = !pattern[sound][step];
updateGrid();
// Play the sound when clicking
if (pattern[sound][step]) {
playSound(sounds[sound]);
}
}
// Update grid display
function updateGrid() {
const cells = document.querySelectorAll('#drumGrid .note');
cells.forEach(cell => {
const sound = cell.dataset.sound;
const step = parseInt(cell.dataset.step);
if (pattern[sound][step]) {
cell.classList.remove('bg-gray-800');
cell.classList.add(sounds[sound].color);
} else {
cell.classList.remove(sounds[sound].color);
cell.classList.add('bg-gray-800');
}
});
updateDrumNotation();
}
// Update drum notation display
function updateDrumNotation() {
const staff = document.getElementById('drumStaff');
const cells = document.querySelectorAll('#drumGrid .note');
// Clear existing notes and bar lines
document.querySelectorAll('.drum-note, .note-stem, .bar-line').forEach(el => el.remove());
// Add bar lines for each measure (assuming 4/4 time)
const measures = Math.ceil(steps / 16);
for (let i = 0; i <= measures; i++) {
const barLine = document.createElement('div');
barLine.className = 'bar-line';
barLine.style.left = `${(i * 100) / measures}%`;
staff.appendChild(barLine);
}
cells.forEach(cell => {
const sound = cell.dataset.sound;
const step = parseInt(cell.dataset.step);
if (pattern[sound][step]) {
cell.classList.remove('bg-gray-800');
cell.classList.add(sounds[sound].color);
// Add drum notation
const note = document.createElement('div');
note.className = `drum-note ${sound}`;
note.style.left = `${(step / steps) * 100}%`;
staff.appendChild(note);
// Add stem based on note duration
const stem = document.createElement('div');
stem.className = 'note-stem';
stem.style.left = `${(step / steps) * 100}%`;
stem.style.height = '20px';
stem.style.top = sound === 'hihat' ? '30%' : sound === 'snare' ? '50%' : sound === 'rim' ? '70%' : '90%';
staff.appendChild(stem);
} else {
cell.classList.remove(sounds[sound].color);
cell.classList.add('bg-gray-800');
}
});
}
// Play the sequence
function playSequence() {
if (!isPlaying) return;
// Highlight current step
const cells = document.querySelectorAll(`#drumGrid .note[data-step="${currentStep}"]`);
cells.forEach(cell => {
cell.classList.add('active');
const sound = cell.dataset.sound;
if (pattern[sound][currentStep]) {
cell.classList.add('pulse');
playSound(sounds[sound]);
// Add note to visualization
addNoteToVisualization(sounds[sound].name, currentStep);
}
setTimeout(() => {
cell.classList.remove('active');
cell.classList.remove('pulse');
}, 100);
});
// Move playhead
const progress = (currentStep / steps) * 100;
playHead.style.left = `${progress}%`;
// Increment step
currentStep = (currentStep + 1) % steps;
// Schedule next step
const stepDuration = (60 / bpm) * (4 / (steps / 4)) * 1000;
timerId = setTimeout(playSequence, stepDuration);
}
// Add note to visualization
function addNoteToVisualization(noteName, step) {
const noteElement = document.createElement('div');
noteElement.className = 'inline-block mx-1 text-xs bg-opacity-70 rounded px-2 py-1 mb-1';
switch(noteName) {
case 'Bass':
noteElement.classList.add('bg-blue-600');
noteElement.textContent = 'B';
break;
case 'Snare':
noteElement.classList.add('bg-purple-600');
noteElement.textContent = 'S';
break;
case 'Hi-Hat':
noteElement.classList.add('bg-green-600');
noteElement.textContent = 'H';
break;
case 'Rim':
noteElement.classList.add('bg-yellow-600');
noteElement.textContent = 'R';
break;
}
noteVisualization.appendChild(noteElement);
// Limit number of notes shown
if (noteVisualization.children.length > 20) {
noteVisualization.removeChild(noteVisualization.children[0]);
}
}
// Start playback
function startPlayback() {
if (isPlaying) return;
isPlaying = true;
currentStep = 0;
playBtn.disabled = true;
stopBtn.disabled = false;
playHead.style.display = 'block';
noteVisualization.innerHTML = '';
// Calculate animation duration based on BPM
const loopDuration = (60 / bpm) * 4 * 1000; // 4 beats per measure
playHead.style.setProperty('--bpm', `${loopDuration}ms`);
playHead.style.animationDuration = `${loopDuration}ms`;
// Start audio context if suspended
if (audioContext.state === 'suspended') {
audioContext.resume();
}
playSequence();
}
// Stop playback
function stopPlayback() {
isPlaying = false;
clearTimeout(timerId);
playBtn.disabled = false;
stopBtn.disabled = true;
playHead.style.display = 'none';
// Remove active classes
document.querySelectorAll('#drumGrid .note').forEach(cell => {
cell.classList.remove('active');
cell.classList.remove('pulse');
});
}
// Clear all steps
function clearAll() {
if (confirm('Are you sure you want to clear all steps?')) {
initPattern();
updateGrid();
}
}
// Load predefined pattern
function loadPattern(patternName) {
initPattern();
switch(patternName) {
case 'basic':
// Basic rock pattern
for (let i = 0; i < steps; i++) {
// Hi-hat on every step
pattern.hihat[i] = true;
// Bass drum on 1 and 3
if (i % (steps / 4) === 0) {
pattern.bass[i] = true;
}
// Snare on 2 and 4
if (i % (steps / 2) === (steps / 4)) {
pattern.snare[i] = true;
}
}
break;
case 'complex':
// More complex pattern
for (let i = 0; i < steps; i++) {
// Hi-hat
pattern.hihat[i] = true;
// Bass drum
if (i % (steps / 4) === 0 || (i % (steps / 8) === (steps / 16))) {
pattern.bass[i] = true;
}
// Snare
if (i % (steps / 2) === (steps / 4) || (i % (steps / 4) === (steps / 8))) {
pattern.snare[i] = true;
}
// Rim
if (i % (steps / 8) === (steps / 16) && !pattern.snare[i]) {
pattern.rim[i] = true;
}
}
break;
case 'random':
// Random pattern
for (let sound in pattern) {
for (let i = 0; i < steps; i++) {
pattern[sound][i] = Math.random() > 0.7;
}
}
break;
}
updateGrid();
}
// Save current pattern
function savePattern() {
const patternName = prompt('Enter a name for this pattern:');
if (patternName) {
localStorage.setItem(`drumMachinePattern_${patternName}`, JSON.stringify(pattern));
alert(`Pattern "${patternName}" saved!`);
}
}
// Load saved pattern
function loadSavedPattern() {
const patternName = prompt('Enter the name of the pattern to load:');
if (patternName) {
const savedPattern = localStorage.getItem(`drumMachinePattern_${patternName}`);
if (savedPattern) {
pattern = JSON.parse(savedPattern);
steps = pattern.bass.length;
stepsSlider.value = steps;
stepsValue.textContent = steps;
createDrumGrid();
updateGrid();
alert(`Pattern "${patternName}" loaded!`);
} else {
alert(`Pattern "${patternName}" not found!`);
}
}
}
// Event listeners
playBtn.addEventListener('click', startPlayback);
stopBtn.addEventListener('click', stopPlayback);
bpmSlider.addEventListener('input', function() {
bpm = parseInt(this.value);
bpmValue.textContent = bpm;
if (isPlaying) {
stopPlayback();
startPlayback();
}
});
volumeSlider.addEventListener('input', function() {
const volume = parseInt(this.value) / 100;
volumeValue.textContent = this.value;
masterGain.gain.value = volume;
});
stepsSlider.addEventListener('input', function() {
steps = parseInt(this.value);
stepsValue.textContent = steps;
initPattern();
createDrumGrid();
});
clearBtn.addEventListener('click', clearAll);
soundBtns.forEach(btn => {
btn.addEventListener('click', function() {
selectedSound = this.dataset.sound;
soundBtns.forEach(b => b.classList.remove('ring-2', 'ring-yellow-400'));
this.classList.add('ring-2', 'ring-yellow-400');
playSound(sounds[selectedSound]);
});
});
// Select first sound by default
soundBtns[0].classList.add('ring-2', 'ring-yellow-400');
patternSelect.addEventListener('change', function() {
if (this.value !== 'empty') {
loadPattern(this.value);
}
});
savePatternBtn.addEventListener('click', savePattern);
loadPatternBtn.addEventListener('click', loadSavedPattern);
// Set note duration style
function setNoteDuration(noteElement, duration) {
switch(duration) {
case 'quarter':
// Single note head with stem
break;
case 'eighth':
// Add flag
noteElement.style.borderRight = '6px solid transparent';
noteElement.style.borderLeft = '6px solid transparent';
noteElement.style.borderBottom = '6px solid white';
noteElement.style.borderRadius = '0';
break;
case 'sixteenth':
// Double flag
noteElement.style.borderRight = '6px solid transparent';
noteElement.style.borderLeft = '6px solid transparent';
noteElement.style.borderBottom = '6px solid white';
noteElement.style.borderRadius = '0';
noteElement.style.boxShadow = '10px 0 0 0 white';
break;
}
}
// Initialize
createDrumGrid();
updateGrid();
// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
if (e.code === 'Space') {
e.preventDefault();
if (isPlaying) {
stopPlayback();
} else {
startPlayback();
}
}
// Number keys for sounds
if (e.code === 'Digit1') {
document.querySelector('[data-sound="bass"]').click();
} else if (e.code === 'Digit2') {
document.querySelector('[data-sound="snare"]').click();
} else if (e.code === 'Digit3') {
document.querySelector('[data-sound="hihat"]').click();
} else if (e.code === 'Digit4') {
document.querySelector('[data-sound="rim"]').click();
}
});
});
</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=IgorSlinko/drum-machine" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>