webmpc / index.html
KVRT's picture
Add 3 files
35f2373 verified
Raw History Blame Contribute Delete
45.2 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebMPC - Browser 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 pulse {
0% { transform: scale(1); }
50% { transform: scale(0.95); }
100% { transform: scale(1); }
}
.pad-active {
animation: pulse 0.1s ease-in-out;
box-shadow: inset 0 0 20px rgba(255,255,255,0.5);
}
.pad {
transition: all 0.08s ease;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
.pad:hover {
transform: translateY(-2px);
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.4);
}
.pad:active {
transform: translateY(1px);
}
.knob {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
box-shadow: inset 0 0 10px rgba(0,0,0,0.5);
position: relative;
cursor: pointer;
}
.knob:after {
content: '';
position: absolute;
width: 4px;
height: 12px;
background: #4299e1;
top: 4px;
left: 50%;
transform: translateX(-50%);
border-radius: 2px;
}
.slider {
-webkit-appearance: none;
width: 100%;
height: 6px;
border-radius: 3px;
background: linear-gradient(to right, #4a5568, #2d3748);
outline: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: #4299e1;
cursor: pointer;
box-shadow: 0 0 3px rgba(0,0,0,0.5);
}
.sequencer-step {
width: 24px;
height: 24px;
border-radius: 2px;
background-color: #2d3748;
cursor: pointer;
transition: all 0.1s ease;
}
.sequencer-step.active {
background-color: #4299e1;
}
.sequencer-step.current {
box-shadow: 0 0 0 2px white;
}
.waveform-display {
height: 40px;
background-color: #2d3748;
border-radius: 4px;
position: relative;
overflow: hidden;
}
.waveform {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
stroke: #4299e1;
fill: none;
stroke-width: 1;
}
.progress-bar {
position: absolute;
top: 0;
left: 0;
height: 100%;
background-color: rgba(255,255,255,0.1);
width: 0%;
}
input[type="file"] {
display: none;
}
</style>
</head>
<body class="bg-gray-900 min-h-screen text-white p-4">
<div class="max-w-6xl mx-auto">
<header class="text-center mb-6">
<h1 class="text-3xl font-bold mb-2 text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-blue-600">
<i class="fas fa-drum mr-2"></i>WebMPC
</h1>
<p class="text-sm text-gray-400">Browser-based Drum Machine & Sampler</p>
</header>
<div class="bg-gray-800 rounded-xl p-6 shadow-2xl border border-gray-700">
<!-- Main Pad Grid -->
<div class="grid grid-cols-4 gap-3 mb-6">
<!-- Pads will be generated dynamically -->
</div>
<!-- Controls Section -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<!-- Pad Controls -->
<div class="bg-gray-700 bg-opacity-50 p-4 rounded-lg">
<h3 class="text-lg font-semibold mb-3 text-center text-blue-300">
<i class="fas fa-sliders-h mr-2"></i>Pad Controls
</h3>
<div class="space-y-4">
<div>
<label class="block mb-1 text-sm text-gray-300">Volume</label>
<input type="range" min="0" max="1" step="0.01" value="0.8"
class="slider" id="volumeSlider" oninput="updateVolume()">
</div>
<div>
<label class="block mb-1 text-sm text-gray-300">Pan</label>
<input type="range" min="-1" max="1" step="0.1" value="0"
class="slider" id="panSlider" oninput="updatePan()">
</div>
<div>
<label class="block mb-1 text-sm text-gray-300">Pitch</label>
<input type="range" min="0.5" max="2" step="0.1" value="1"
class="slider" id="pitchSlider" oninput="updatePitch()">
</div>
</div>
</div>
<!-- Effects -->
<div class="bg-gray-700 bg-opacity-50 p-4 rounded-lg">
<h3 class="text-lg font-semibold mb-3 text-center text-green-300">
<i class="fas fa-random mr-2"></i>Effects
</h3>
<div class="space-y-4">
<div>
<label class="block mb-1 text-sm text-gray-300">Filter</label>
<input type="range" min="20" max="20000" step="10" value="20000"
class="slider" id="filterSlider" oninput="updateFilter()">
</div>
<div>
<label class="block mb-1 text-sm text-gray-300">Reverb</label>
<input type="range" min="0" max="1" step="0.1" value="0"
class="slider" id="reverbSlider" oninput="updateReverb()">
</div>
<div>
<label class="block mb-1 text-sm text-gray-300">Delay</label>
<input type="range" min="0" max="1" step="0.1" value="0"
class="slider" id="delaySlider" oninput="updateDelay()">
</div>
</div>
</div>
<!-- Recording & Export -->
<div class="bg-gray-700 bg-opacity-50 p-4 rounded-lg">
<h3 class="text-lg font-semibold mb-3 text-center text-purple-300">
<i class="fas fa-record-vinyl mr-2"></i>Record
</h3>
<div class="space-y-3">
<div class="waveform-display" id="waveformDisplay">
<svg class="waveform" id="waveform" viewBox="0 0 100 20" preserveAspectRatio="none"></svg>
<div class="progress-bar" id="progressBar"></div>
</div>
<div class="flex justify-between space-x-2">
<button onclick="startRecording()" class="bg-red-600 hover:bg-red-700 px-3 py-2 rounded-lg text-sm transition flex-1">
<i class="fas fa-microphone mr-1"></i> Mic
</button>
<button onclick="stopRecording()" class="bg-gray-600 hover:bg-gray-700 px-3 py-2 rounded-lg text-sm transition flex-1">
<i class="fas fa-stop mr-1"></i> Stop
</button>
<button onclick="exportMP3()" class="bg-green-600 hover:bg-green-700 px-3 py-2 rounded-lg text-sm transition flex-1">
<i class="fas fa-file-export mr-1"></i> Export
</button>
</div>
<div class="text-xs text-gray-400 text-center mt-2">
Record audio or export your sequence as MP3
</div>
</div>
</div>
</div>
<!-- Sequencer Section -->
<div class="bg-gray-700 bg-opacity-50 p-4 rounded-lg mb-4">
<h3 class="text-lg font-semibold mb-3 text-center text-yellow-300">
<i class="fas fa-drum mr-2"></i>Step Sequencer
</h3>
<div class="overflow-x-auto">
<div class="flex mb-2">
<div class="w-24 flex-shrink-0"></div>
<div class="flex space-x-1">
<!-- Steps will be generated dynamically -->
</div>
</div>
<!-- Sequencer rows will be generated dynamically -->
</div>
<div class="mt-4 flex justify-center space-x-4">
<button onclick="startSequencer()" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg transition">
<i class="fas fa-play mr-2"></i>Play
</button>
<button onclick="stopSequencer()" class="bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded-lg transition">
<i class="fas fa-stop mr-2"></i>Stop
</button>
<button onclick="randomizeSequencer()" class="bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded-lg transition">
<i class="fas fa-random mr-2"></i>Random
</button>
<button onclick="clearSequencer()" class="bg-red-600 hover:bg-red-700 px-4 py-2 rounded-lg transition">
<i class="fas fa-trash mr-2"></i>Clear
</button>
</div>
<div class="mt-3 flex items-center justify-center space-x-4">
<div class="flex items-center">
<label class="text-sm text-gray-300 mr-2">BPM:</label>
<input type="number" min="40" max="240" value="120" id="bpmInput"
class="bg-gray-800 text-white w-16 px-2 py-1 rounded border border-gray-600">
</div>
<div class="flex items-center">
<label class="text-sm text-gray-300 mr-2">Steps:</label>
<input type="number" min="4" max="32" value="16" id="stepsInput"
class="bg-gray-800 text-white w-16 px-2 py-1 rounded border border-gray-600">
</div>
</div>
</div>
<!-- Sample Management -->
<div class="bg-gray-700 bg-opacity-50 p-4 rounded-lg">
<h3 class="text-lg font-semibold mb-3 text-center text-green-300">
<i class="fas fa-folder-open mr-2"></i>Sample Manager
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block mb-2 text-sm text-gray-300">Load samples:</label>
<div class="flex flex-wrap gap-2">
<button onclick="loadDefaultKit()" class="bg-blue-600 hover:bg-blue-700 px-3 py-2 rounded-lg text-sm transition">
<i class="fas fa-drum mr-1"></i> Default Kit
</button>
<label for="sampleUpload" class="bg-green-600 hover:bg-green-700 px-3 py-2 rounded-lg text-sm transition cursor-pointer">
<i class="fas fa-upload mr-1"></i> Upload
</label>
<input type="file" id="sampleUpload" accept="audio/*" multiple onchange="handleSampleUpload()">
</div>
</div>
<div>
<label class="block mb-2 text-sm text-gray-300">Selected pad: <span id="selectedPadDisplay">None</span></label>
<div class="flex flex-wrap gap-2">
<button onclick="renameSample()" class="bg-purple-600 hover:bg-purple-700 px-3 py-2 rounded-lg text-sm transition">
<i class="fas fa-edit mr-1"></i> Rename
</button>
<button onclick="deleteSample()" class="bg-red-600 hover:bg-red-700 px-3 py-2 rounded-lg text-sm transition">
<i class="fas fa-trash mr-1"></i> Delete
</button>
</div>
</div>
</div>
</div>
</div>
<footer class="text-center mt-6 text-gray-400 text-xs">
<p>WebMPC - Browser Drum Machine & Sampler © 2023 | Use Chrome/Firefox for best performance</p>
</footer>
</div>
<script src="https://cdn.jsdelivr.net/npm/recorderjs@1.0.1/dist/recorder.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lamejs@1.2.0/lame.min.js"></script>
<script>
// Audio context and nodes
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
let pads = [];
let selectedPad = null;
let currentStep = 0;
let sequencerInterval;
let isPlaying = false;
let steps = 16;
let bpm = 120;
// Effects nodes
let masterGain = audioContext.createGain();
let filterNode = audioContext.createBiquadFilter();
let convolverNode = audioContext.createConvolver();
let delayNode = audioContext.createDelay();
let feedbackNode = audioContext.createGain();
let pannerNode = audioContext.createStereoPanner();
// Connect nodes
masterGain.connect(filterNode);
filterNode.connect(pannerNode);
pannerNode.connect(convolverNode);
convolverNode.connect(delayNode);
delayNode.connect(feedbackNode);
feedbackNode.connect(delayNode);
delayNode.connect(audioContext.destination);
// Set initial values
masterGain.gain.value = 0.8;
filterNode.type = "lowpass";
filterNode.frequency.value = 20000;
feedbackNode.gain.value = 0;
delayNode.delayTime.value = 0.5;
pannerNode.pan.value = 0;
// Recording variables
let recorder;
let audioStream;
let isRecording = false;
let recordedChunks = [];
// Initialize pads
function initPads() {
const padGrid = document.querySelector('.grid-cols-4');
padGrid.innerHTML = '';
pads = [];
for (let i = 0; i < 16; i++) {
const pad = {
id: i,
name: `Pad ${i+1}`,
buffer: null,
color: `hsl(${(i * 22.5) % 360}, 70%, 50%)`,
volume: 1,
pan: 0,
pitch: 1
};
pads.push(pad);
const padElement = document.createElement('button');
padElement.className = 'pad rounded-lg flex flex-col items-center justify-center h-24';
padElement.style.backgroundColor = pad.color;
padElement.innerHTML = `
<span class="font-bold text-sm">${pad.name}</span>
<span class="text-xs opacity-70">Empty</span>
`;
padElement.addEventListener('click', () => {
selectPad(i);
playPad(i);
});
padElement.addEventListener('dblclick', () => {
selectPad(i);
document.getElementById('sampleUpload').click();
});
padGrid.appendChild(padElement);
}
// Initialize default kit
loadDefaultKit();
}
// Initialize sequencer
function initSequencer() {
const sequencerContainer = document.querySelector('.bg-gray-700.bg-opacity-50 .overflow-x-auto > div:last-child');
sequencerContainer.innerHTML = '';
steps = parseInt(document.getElementById('stepsInput').value);
// Create step headers
const stepHeaders = document.querySelector('.flex.space-x-1');
stepHeaders.innerHTML = '';
for (let i = 0; i < steps; i++) {
const step = document.createElement('div');
step.className = 'sequencer-step';
step.dataset.step = i + 1;
step.addEventListener('click', () => toggleStep(selectedPad, i + 1));
stepHeaders.appendChild(step);
}
// Create sequencer rows for each pad
for (let i = 0; i < 16; i++) {
const row = document.createElement('div');
row.className = 'flex mb-1';
const padLabel = document.createElement('div');
padLabel.className = 'w-24 flex-shrink-0 text-xs text-gray-300 flex items-center pl-2 truncate';
padLabel.textContent = pads[i].name || `Pad ${i+1}`;
row.appendChild(padLabel);
const stepsContainer = document.createElement('div');
stepsContainer.className = 'flex space-x-1';
for (let j = 0; j < steps; j++) {
const step = document.createElement('div');
step.className = 'sequencer-step';
step.dataset.pad = i;
step.dataset.step = j + 1;
step.addEventListener('click', () => toggleStep(i, j + 1));
stepsContainer.appendChild(step);
}
row.appendChild(stepsContainer);
sequencerContainer.appendChild(row);
}
}
// Select a pad
function selectPad(index) {
selectedPad = index;
document.getElementById('selectedPadDisplay').textContent = pads[index].name || `Pad ${index+1}`;
// Update sliders to match pad settings
document.getElementById('volumeSlider').value = pads[index].volume;
document.getElementById('panSlider').value = pads[index].pan;
document.getElementById('pitchSlider').value = pads[index].pitch;
// Highlight selected pad
document.querySelectorAll('.pad').forEach((pad, i) => {
if (i === index) {
pad.style.boxShadow = 'inset 0 0 0 2px white';
} else {
pad.style.boxShadow = '';
}
});
}
// Play a pad
function playPad(index) {
if (!pads[index].buffer) return;
const source = audioContext.createBufferSource();
source.buffer = pads[index].buffer;
source.playbackRate.value = pads[index].pitch;
const gainNode = audioContext.createGain();
gainNode.gain.value = pads[index].volume;
const pannerNode = audioContext.createStereoPanner();
pannerNode.pan.value = pads[index].pan;
source.connect(gainNode);
gainNode.connect(pannerNode);
pannerNode.connect(masterGain);
source.start();
// Add visual feedback
const padElement = document.querySelectorAll('.pad')[index];
padElement.classList.add('pad-active');
setTimeout(() => padElement.classList.remove('pad-active'), 100);
// Update waveform display if this is the selected pad
if (selectedPad === index) {
drawWaveform(pads[index].buffer);
}
}
// Toggle a step in the sequencer
function toggleStep(padIndex, step) {
if (padIndex === null) return;
const stepElements = document.querySelectorAll(`.sequencer-step[data-pad="${padIndex}"][data-step="${step}"]`);
stepElements.forEach(stepEl => {
stepEl.classList.toggle('active');
});
}
// Start the sequencer
function startSequencer() {
if (isPlaying) return;
isPlaying = true;
currentStep = 0;
bpm = parseInt(document.getElementById('bpmInput').value);
const stepDuration = (60 / bpm) / 4; // 16th notes
// Highlight current step
updateStepHighlight();
sequencerInterval = setInterval(() => {
currentStep = (currentStep % steps) + 1;
// Highlight current step
updateStepHighlight();
// Play sounds for current step
document.querySelectorAll(`.sequencer-step[data-step="${currentStep}"].active`).forEach(step => {
const padIndex = parseInt(step.dataset.pad);
playPad(padIndex);
});
}, stepDuration * 1000);
}
// Stop the sequencer
function stopSequencer() {
clearInterval(sequencerInterval);
isPlaying = false;
currentStep = 0;
// Remove all highlights
document.querySelectorAll('.sequencer-step').forEach(step => {
step.classList.remove('current');
});
}
// Update step highlighting
function updateStepHighlight() {
document.querySelectorAll('.sequencer-step').forEach(step => {
if (parseInt(step.dataset.step) === currentStep) {
step.classList.add('current');
} else {
step.classList.remove('current');
}
});
}
// Randomize the sequencer
function randomizeSequencer() {
document.querySelectorAll('.sequencer-step').forEach(step => {
if (Math.random() > 0.7) {
step.classList.add('active');
} else {
step.classList.remove('active');
}
});
}
// Clear the sequencer
function clearSequencer() {
document.querySelectorAll('.sequencer-step').forEach(step => {
step.classList.remove('active');
});
}
// Update master volume
function updateVolume() {
if (selectedPad !== null) {
pads[selectedPad].volume = parseFloat(document.getElementById('volumeSlider').value);
}
}
// Update pan
function updatePan() {
if (selectedPad !== null) {
pads[selectedPad].pan = parseFloat(document.getElementById('panSlider').value);
}
}
// Update pitch
function updatePitch() {
if (selectedPad !== null) {
pads[selectedPad].pitch = parseFloat(document.getElementById('pitchSlider').value);
}
}
// Update filter
function updateFilter() {
filterNode.frequency.value = parseFloat(document.getElementById('filterSlider').value);
}
// Update reverb
function updateReverb() {
const amount = parseFloat(document.getElementById('reverbSlider').value);
if (amount > 0) {
// Create a simple impulse response for reverb
const length = audioContext.sampleRate * 2;
const impulse = audioContext.createBuffer(2, length, audioContext.sampleRate);
const left = impulse.getChannelData(0);
const right = impulse.getChannelData(1);
for (let i = 0; i < length; i++) {
const n = length - i;
left[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, amount * 10);
right[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, amount * 10);
}
convolverNode.buffer = impulse;
} else {
// Bypass reverb
convolverNode.buffer = null;
}
}
// Update delay
function updateDelay() {
const amount = parseFloat(document.getElementById('delaySlider').value);
feedbackNode.gain.value = amount * 0.8;
delayNode.delayTime.value = amount * 0.5;
}
// Draw waveform
function drawWaveform(buffer) {
const waveformSvg = document.getElementById('waveform');
waveformSvg.innerHTML = '';
const data = buffer.getChannelData(0);
const step = Math.ceil(data.length / 100);
const amp = 20 / 2;
let path = `M 0 ${amp} `;
for (let i = 0; i < 100; i++) {
const x = i;
const y = (1 + data[i * step]) * amp;
path += `L ${x} ${y} `;
}
const pathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');
pathElement.setAttribute('d', path);
waveformSvg.appendChild(pathElement);
}
// Start recording from microphone
function startRecording() {
if (isRecording) return;
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
audioStream = stream;
const input = audioContext.createMediaStreamSource(stream);
recorder = new Recorder(input);
recorder.record();
isRecording = true;
recordedChunks = [];
// Update UI
document.getElementById('progressBar').style.width = '0%';
document.querySelector('button[onclick="startRecording()"]').classList.add('animate-pulse');
// Start progress bar animation
let startTime = Date.now();
const maxDuration = 10000; // 10 seconds max
const updateProgress = () => {
if (!isRecording) return;
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / maxDuration * 100, 100);
document.getElementById('progressBar').style.width = `${progress}%`;
if (progress >= 100) {
stopRecording();
} else {
requestAnimationFrame(updateProgress);
}
};
updateProgress();
})
.catch(err => {
console.error('Error recording:', err);
alert('Could not access microphone. Please check permissions.');
});
}
// Stop recording
function stopRecording() {
if (!isRecording) return;
recorder.stop();
audioStream.getTracks().forEach(track => track.stop());
recorder.exportWAV(blob => {
recordedChunks.push(blob);
// Convert blob to audio buffer
const reader = new FileReader();
reader.onload = () => {
audioContext.decodeAudioData(reader.result)
.then(buffer => {
if (selectedPad !== null) {
pads[selectedPad].buffer = buffer;
pads[selectedPad].name = `Recording ${selectedPad+1}`;
// Update pad display
const padElement = document.querySelectorAll('.pad')[selectedPad];
padElement.querySelector('span:first-child').textContent = pads[selectedPad].name;
padElement.querySelector('span:last-child').textContent = `${Math.round(buffer.duration * 1000)}ms`;
// Update sequencer labels
document.querySelectorAll('.w-24.flex-shrink-0').forEach((label, i) => {
if (i === selectedPad) {
label.textContent = pads[selectedPad].name;
}
});
drawWaveform(buffer);
}
})
.catch(err => console.error('Error decoding audio:', err));
};
reader.readAsArrayBuffer(blob);
});
isRecording = false;
document.querySelector('button[onclick="startRecording()"]').classList.remove('animate-pulse');
}
// Export sequence as MP3
function exportMP3() {
if (!isPlaying) {
alert('Please play the sequence to export it');
return;
}
// Create an offline audio context to render the sequence
const sampleRate = 44100;
const offlineCtx = new OfflineAudioContext(2, steps * (sampleRate * 60 / bpm / 4), sampleRate);
// Clone all the pads and effects
const offlinePads = pads.map(pad => ({
...pad,
buffer: pad.buffer ? offlineCtx.createBuffer(
pad.buffer.numberOfChannels,
pad.buffer.length,
pad.buffer.sampleRate
) : null
}));
if (offlinePads.some(pad => !pad.buffer)) {
alert('Some pads are empty. Please load samples on all pads used in the sequence.');
return;
}
// Get all active steps
const activeSteps = [];
document.querySelectorAll('.sequencer-step.active').forEach(step => {
activeSteps.push({
pad: parseInt(step.dataset.pad),
step: parseInt(step.dataset.step)
});
});
if (activeSteps.length === 0) {
alert('No active steps in the sequencer. Please add some steps to export.');
return;
}
// Schedule all the sounds
const stepDuration = (60 / bpm) / 4; // 16th notes
activeSteps.forEach(({pad, step}) => {
const source = offlineCtx.createBufferSource();
source.buffer = offlinePads[pad].buffer;
source.playbackRate.value = offlinePads[pad].pitch;
const gainNode = offlineCtx.createGain();
gainNode.gain.value = offlinePads[pad].volume;
const pannerNode = offlineCtx.createStereoPanner();
pannerNode.pan.value = offlinePads[pad].pan;
source.connect(gainNode);
gainNode.connect(pannerNode);
pannerNode.connect(offlineCtx.destination);
source.start((step - 1) * stepDuration);
});
// Render the audio
offlineCtx.startRendering().then(renderedBuffer => {
// Convert to MP3 using lamejs
const channels = renderedBuffer.numberOfChannels;
const sampleRate = renderedBuffer.sampleRate;
const samples = renderedBuffer.length;
const left = renderedBuffer.getChannelData(0);
const right = channels > 1 ? renderedBuffer.getChannelData(1) : left;
const mp3encoder = new lamejs.Mp3Encoder(channels, sampleRate, 128);
const maxSamples = 1152;
const mp3Data = [];
for (let i = 0; i < samples; i += maxSamples) {
const leftChunk = left.subarray(i, i + maxSamples);
const rightChunk = right.subarray(i, i + maxSamples);
const mp3buf = mp3encoder.encodeBuffer(leftChunk, rightChunk);
if (mp3buf.length > 0) {
mp3Data.push(mp3buf);
}
}
const mp3buf = mp3encoder.flush();
if (mp3buf.length > 0) {
mp3Data.push(mp3buf);
}
const blob = new Blob(mp3Data, { type: 'audio/mp3' });
const url = URL.createObjectURL(blob);
// Create download link
const a = document.createElement('a');
a.href = url;
a.download = `webmpc_sequence_${new Date().toISOString().slice(0, 10)}.mp3`;
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
});
}
// Load default drum kit
function loadDefaultKit() {
const defaultSamples = [
{ name: "Kick", url: "https://assets.mixkit.co/sfx/preview/mixkit-big-kick-drum-1173.mp3" },
{ name: "Snare", url: "https://assets.mixkit.co/sfx/preview/mixkit-snare-drum-2310.mp3" },
{ name: "Closed Hat", url: "https://assets.mixkit.co/sfx/preview/mixkit-closed-hi-hat-2287.mp3" },
{ name: "Open Hat", url: "https://assets.mixkit.co/sfx/preview/mixkit-open-hi-hat-2288.mp3" },
{ name: "Clap", url: "https://assets.mixkit.co/sfx/preview/mixkit-hip-hop-clap-2290.mp3" },
{ name: "Rim", url: "https://assets.mixkit.co/sfx/preview/mixkit-rimshot-2311.mp3" },
{ name: "Tom", url: "https://assets.mixkit.co/sfx/preview/mixkit-tom-drum-2312.mp3" },
{ name: "Crash", url: "https://assets.mixkit.co/sfx/preview/mixkit-cymbal-crash-2313.mp3" },
{ name: "Perc 1", url: "https://assets.mixkit.co/sfx/preview/mixkit-percussion-hit-2291.mp3" },
{ name: "Perc 2", url: "https://assets.mixkit.co/sfx/preview/mixkit-percussion-hit-2292.mp3" },
{ name: "Shaker", url: "https://assets.mixkit.co/sfx/preview/mixkit-shaker-2293.mp3" },
{ name: "Tambourine", url: "https://assets.mixkit.co/sfx/preview/mixkit-tambourine-2294.mp3" },
{ name: "Cowbell", url: "https://assets.mixkit.co/sfx/preview/mixkit-cowbell-2295.mp3" },
{ name: "Bongo", url: "https://assets.mixkit.co/sfx/preview/mixkit-bongo-hit-2296.mp3" },
{ name: "Conga", url: "https://assets.mixkit.co/sfx/preview/mixkit-conga-hit-2297.mp3" },
{ name: "Wood Block", url: "https://assets.mixkit.co/sfx/preview/mixkit-wood-block-hit-2298.mp3" }
];
defaultSamples.forEach((sample, i) => {
fetch(sample.url)
.then(response => response.arrayBuffer())
.then(arrayBuffer => audioContext.decodeAudioData(arrayBuffer))
.then(buffer => {
pads[i].buffer = buffer;
pads[i].name = sample.name;
// Update pad display
const padElement = document.querySelectorAll('.pad')[i];
padElement.querySelector('span:first-child').textContent = sample.name;
padElement.querySelector('span:last-child').textContent = `${Math.round(buffer.duration * 1000)}ms`;
// Update sequencer labels
document.querySelectorAll('.w-24.flex-shrink-0').forEach((label, idx) => {
if (idx === i) {
label.textContent = sample.name;
}
});
if (i === selectedPad) {
drawWaveform(buffer);
}
})
.catch(err => console.error('Error loading sample:', err));
});
}
// Handle sample upload
function handleSampleUpload() {
if (selectedPad === null) {
alert('Please select a pad first');
return;
}
const files = document.getElementById('sampleUpload').files;
if (files.length === 0) return;
const file = files[0];
const reader = new FileReader();
reader.onload = () => {
audioContext.decodeAudioData(reader.result)
.then(buffer => {
pads[selectedPad].buffer = buffer;
pads[selectedPad].name = file.name.replace(/\.[^/.]+$/, ""); // Remove extension
// Update pad display
const padElement = document.querySelectorAll('.pad')[selectedPad];
padElement.querySelector('span:first-child').textContent = pads[selectedPad].name;
padElement.querySelector('span:last-child').textContent = `${Math.round(buffer.duration * 1000)}ms`;
// Update sequencer labels
document.querySelectorAll('.w-24.flex-shrink-0').forEach((label, i) => {
if (i === selectedPad) {
label.textContent = pads[selectedPad].name;
}
});
drawWaveform(buffer);
})
.catch(err => console.error('Error decoding audio:', err));
};
reader.readAsArrayBuffer(file);
}
// Rename sample
function renameSample() {
if (selectedPad === null || !pads[selectedPad].buffer) {
alert('Please select a pad with a sample loaded');
return;
}
const newName = prompt('Enter new name for this sample:', pads[selectedPad].name);
if (newName) {
pads[selectedPad].name = newName;
// Update pad display
const padElement = document.querySelectorAll('.pad')[selectedPad];
padElement.querySelector('span:first-child').textContent = newName;
// Update sequencer labels
document.querySelectorAll('.w-24.flex-shrink-0').forEach((label, i) => {
if (i === selectedPad) {
label.textContent = newName;
}
});
}
}
// Delete sample
function deleteSample() {
if (selectedPad === null || !pads[selectedPad].buffer) {
alert('No sample to delete');
return;
}
if (confirm('Are you sure you want to delete this sample?')) {
pads[selectedPad].buffer = null;
pads[selectedPad].name = `Pad ${selectedPad+1}`;
// Update pad display
const padElement = document.querySelectorAll('.pad')[selectedPad];
padElement.querySelector('span:first-child').textContent = `Pad ${selectedPad+1}`;
padElement.querySelector('span:last-child').textContent = 'Empty';
// Update sequencer labels
document.querySelectorAll('.w-24.flex-shrink-0').forEach((label, i) => {
if (i === selectedPad) {
label.textContent = `Pad ${selectedPad+1}`;
}
});
// Clear waveform
document.getElementById('waveform').innerHTML = '';
}
}
// Initialize everything when page loads
window.addEventListener('load', () => {
initPads();
initSequencer();
// Set up keyboard controls (1-9,0 for first 10 pads)
document.addEventListener('keydown', (e) => {
const keyMap = {
'1': 0, '2': 1, '3': 2, '4': 3,
'5': 4, '6': 5, '7': 6, '8': 7,
'9': 8, '0': 9, 'q': 10, 'w': 11,
'e': 12, 'r': 13, 't': 14, 'y': 15
};
if (keyMap[e.key] !== undefined) {
selectPad(keyMap[e.key]);
playPad(keyMap[e.key]);
}
// Space to play/stop sequencer
if (e.key === ' ') {
if (isPlaying) {
stopSequencer();
} else {
startSequencer();
}
e.preventDefault();
}
});
// Update sequencer when BPM or steps change
document.getElementById('bpmInput').addEventListener('change', () => {
if (isPlaying) {
stopSequencer();
startSequencer();
}
});
document.getElementById('stepsInput').addEventListener('change', () => {
initSequencer();
if (isPlaying) {
stopSequencer();
startSequencer();
}
});
});
</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=Assad3l/ebala" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p><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=KVRT/webmpc" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>