Download index.html from KVRT/webmpc: direct link, hf CLI and curl.
- Browser
- Download file 45.2 kB
-
https://huggingface.co/spaces/KVRT/webmpc/resolve/main/index.html
- Command line
-
hf download hf://spaces/KVRT/webmpc/index.html
-
curl -L -o index.html https://huggingface.co/spaces/KVRT/webmpc/resolve/main/index.html
45.2 kB
| <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> |