Tap pads to loop.
Spin discs to scratch.

DECK A
124.0
PITCH
GAIN
HI
MID
LOW
124.0
DECK B
PITCH
Neon Nights
Digital Sun
Heavy Gravity
My MP3
Song Collection
Neon Nights Catchy Dance
Digital Sun Electronic Pop
Heavy Gravity Bass Banger
Blinding Lights 80s Vibe
Levitating Disco House
Bad Guy Bass Groovy
Stay Modern Catchy
VOICE
REC
function createProceduralLoops() { // Define 16-step patterns for 8 pads state.proceduralLoops = [ // Pad 0: House Beat (Kick + Hat) { steps: 16, play: (t, step) => { if (step % 4 === 0) playKick(t, state.nodes.busA); // Kick on 1, 5, 9, 13 if (step % 2 !== 0) playHat(t, state.nodes.busA); // Offbeat hats }}, // Pad 1: Bassline { steps: 16, play: (t, step) => { const pattern = [36, 0, 36, 48, 0, 36, 0, 43, 36, 0, 36, 48, 0, 46, 0, 43]; if(pattern[step]) playSynth(t, pattern[step], 'triangle', 0.15, state.nodes.busA); }}, // Pad 2: Synth Chords { steps: 16, play: (t, step) => { if (step === 2 || step === 8 || step === 14) { playSynth(t, 60, 'sawtooth', 0.2, state.nodes.busA, true); // C playSynth(t, 63, 'sawtooth', 0.2, state.nodes.busA, true); // Eb playSynth(t, 67, 'sawtooth', 0.2, state.nodes.busA, true); // G } }}, // Pad 3: Percussion { steps: 16, play: (t, step) => { if (step % 8 === 4) playSnare(t, state.nodes.busA); if (step % 4 === 3) playWoodblock(t, state.nodes.busA); }}, // Pad 4: Fast Arp (Deck B) { steps: 16, play: (t, step) => { const arp = [72, 75, 79, 84, 79, 75, 72, 67, 72, 75, 79, 84, 87, 84, 79, 75]; playSynth(t, arp[step], 'square', 0.05, state.nodes.busB); }}, // Pad 5: Sub Bass (Deck B) { steps: 16, play: (t, step) => { if(step === 0 || step === 8) playSynth(t, 24, 'sine', 0.8, state.nodes.busB, false, 0.4); }}, // Pad 6: Bright Chords (Deck B) { steps: 16, play: (t, step) => { if (step % 8 === 0) { playSynth(t, 60, 'sine', 0.3, state.nodes.busB, true, 0.5); playSynth(t, 67, 'sine', 0.3, state.nodes.busB, true, 0.5); } }}, // Pad 7: Rapid Hats (Deck B) { steps: 16, play: (t, step) => { playHat(t, state.nodes.busB, step % 4 === 0 ? 0.8 : 0.3); }} ]; } // --- Synths --- function mtof(note) { return 440 * Math.pow(2, (note - 69) / 12); } function playKick(time, outNode) { const osc = state.audioCtx.createOscillator(); const gain = state.audioCtx.createGain(); osc.connect(gain); gain.connect(outNode); osc.frequency.setValueAtTime(150, time); osc.frequency.exponentialRampToValueAtTime(0.01, time + 0.5); gain.gain.setValueAtTime(1, time); gain.gain.exponentialRampToValueAtTime(0.01, time + 0.5); osc.start(time); osc.stop(time + 0.5); } function playSnare(time, outNode) { const osc = state.audioCtx.createOscillator(); const oscGain = state.audioCtx.createGain(); osc.type = 'triangle'; osc.connect(oscGain); const noise = state.audioCtx.createBufferSource(); const bufferSize = state.audioCtx.sampleRate * 0.2; const buffer = state.audioCtx.createBuffer(1, bufferSize, state.audioCtx.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1; noise.buffer = buffer; const noiseFilter = state.audioCtx.createBiquadFilter(); noiseFilter.type = 'highpass'; noiseFilter.frequency.value = 1000; noise.connect(noiseFilter); const noiseGain = state.audioCtx.createGain(); noiseFilter.connect(noiseGain); oscGain.connect(outNode); noiseGain.connect(outNode); osc.frequency.setValueAtTime(250, time); oscGain.gain.setValueAtTime(0.5, time); oscGain.gain.exponentialRampToValueAtTime(0.01, time + 0.1); noiseGain.gain.setValueAtTime(0.5, time); noiseGain.gain.exponentialRampToValueAtTime(0.01, time + 0.2); osc.start(time); noise.start(time); osc.stop(time + 0.2); } function playHat(time, outNode, vol = 0.5) { const noise = state.audioCtx.createBufferSource(); const bufferSize = state.audioCtx.sampleRate * 0.1; const buffer = state.audioCtx.createBuffer(1, bufferSize, state.audioCtx.sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1; noise.buffer = buffer; const filter = state.audioCtx.createBiquadFilter(); filter.type = 'highpass'; filter.frequency.value = 7000; const gain = state.audioCtx.createGain(); gain.gain.setValueAtTime(vol, time); gain.gain.exponentialRampToValueAtTime(0.01, time + 0.05); noise.connect(filter).connect(gain).connect(outNode); noise.start(time); } function playWoodblock(time, outNode) { const osc = state.audioCtx.createOscillator(); const gain = state.audioCtx.createGain(); osc.type = 'square'; osc.frequency.setValueAtTime(800, time); const filter = state.audioCtx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.value = 1000; filter.Q.value = 5; gain.gain.setValueAtTime(0.5, time); gain.gain.exponentialRampToValueAtTime(0.01, time + 0.05); osc.connect(filter).connect(gain).connect(outNode); osc.start(time); osc.stop(time + 0.1); } function playSynth(time, note, type = 'square', vol = 0.2, outNode, useFilter = false, dur = 0.1) { const osc = state.audioCtx.createOscillator(); const gain = state.audioCtx.createGain(); osc.type = type; osc.frequency.value = mtof(note); gain.gain.setValueAtTime(vol, time); gain.gain.setTargetAtTime(0, time + dur * 0.2, dur * 0.5); if (useFilter) { const filter = state.audioCtx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.setValueAtTime(2000, time); filter.frequency.exponentialRampToValueAtTime(200, time + dur); osc.connect(filter).connect(gain).connect(outNode); } else { osc.connect(gain).connect(outNode); } osc.start(time); osc.stop(time + dur + 0.1); } function triggerScratch(velocity) { if (!state.audioCtx || state.audioCtx.state !== 'running') return; const t = state.audioCtx.currentTime; const osc = state.audioCtx.createOscillator(); const gain = state.audioCtx.createGain(); // Simulate record scratch with fast freq sweep osc.type = 'sawtooth'; const baseFreq = 300 + Math.abs(velocity) * 50; osc.frequency.setValueAtTime(baseFreq, t); osc.frequency.exponentialRampToValueAtTime(50, t + 0.1); const filter = state.audioCtx.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.value = 1500; filter.Q.value = 2; gain.gain.setValueAtTime(0, t); gain.gain.linearRampToValueAtTime(0.4, t + 0.02); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.15); osc.connect(filter).connect(gain).connect(state.nodes.master); osc.start(t); osc.stop(t + 0.2); // Animate character arms randomly when scratching const moveL = Math.random() > 0.5; if(moveL) elements.pawL.style.transform = `translate(${Math.random()*20-10}px, -30px)`; else elements.pawR.style.transform = `translate(${Math.random()*20-10}px, -30px)`; setTimeout(() => { elements.pawL.style.transform = ''; elements.pawR.style.transform = ''; }, 100); } function playPianoKey(note) { if (!state.audioCtx) return; playSynth(state.audioCtx.currentTime, note, 'triangle', 0.4, state.nodes.master, true, 0.5); // Character visual react elements.pawR.style.transform = `translate(10px, -40px)`; setTimeout(() => elements.pawR.style.transform = '', 150); } function playGuitarString(note) { if (!state.audioCtx) return; const t = state.audioCtx.currentTime; // Plucked string simulation using fast decaying saw const osc = state.audioCtx.createOscillator(); const gain = state.audioCtx.createGain(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(mtof(note), t); gain.gain.setValueAtTime(0.5, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.8); const filter = state.audioCtx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.setValueAtTime(4000, t); filter.frequency.exponentialRampToValueAtTime(200, t + 0.5); osc.connect(filter).connect(gain).connect(state.nodes.master); osc.start(t); osc.stop(t + 1); elements.pawL.style.transform = `translate(-10px, -40px)`; setTimeout(() => elements.pawL.style.transform = '', 150); } // --- Sequencer --- function nextNote() { const secondsPerBeat = 60.0 / window.appState.bpm; const secondsPer16th = 0.25 * secondsPerBeat; state.nextNoteTime += secondsPer16th; state.current16thNote++; if (state.current16thNote === 16) { state.current16thNote = 0; } } function scheduleNote(beatNumber, time) { // Check HTML5 audio overrides first // Pad 0-3 for(let i=0; i<4; i++) { if(state.padsActive[i]) { const el = document.getElementById(`audio-pad${i}`); if(el && el.src) { // If using HTML audio, just ensure it's playing (not precise timing, but works for background loops) if(el.paused) el.play().catch(()=>{}); } else { // Procedural state.proceduralLoops[i].play(time, beatNumber); } } else { const el = document.getElementById(`audio-pad${i}`); if(el && !el.paused) el.pause(); } } // Pad 4-7 (Deck B) for(let i=4; i<8; i++) { if(state.padsActive[i]) { // No HTML audio slots for 4-7 to keep UI simple, use procedural state.proceduralLoops[i].play(time, beatNumber); } } // Visual Sync (Flash headphones on quarter notes) if (beatNumber % 4 === 0) { requestAnimationFrame(() => { const color = `var(--rainbow-${(Math.floor(beatNumber/4) % 6) + 1})`; elements.hpGlowL.style.fill = color; elements.hpGlowR.style.fill = color; setTimeout(() => { elements.hpGlowL.style.fill = '#333'; elements.hpGlowR.style.fill = '#333'; }, 100); }); } } function scheduler() { while (state.nextNoteTime < state.audioCtx.currentTime + state.scheduleAheadTime) { scheduleNote(state.current16thNote, state.nextNoteTime); nextNote(); } state.timerID = window.setTimeout(scheduler, state.lookahead); } function startSequencer() { if (state.isPlaying) return; initAudioEngine(); if (state.audioCtx.state === 'suspended') state.audioCtx.resume(); state.isPlaying = true; state.current16thNote = 0; state.nextNoteTime = state.audioCtx.currentTime + 0.05; scheduler(); drawVisualizer(); // Auto-start continuous rotation for discs requestAnimationFrame(rotateDiscs); } // --- Interaction Handlers --- function handlePadClick(e) { const pad = e.target.closest('.pad'); if (!pad) return; startSequencer(); // Ensure engine is running const index = parseInt(pad.dataset.pad); state.padsActive[index] = !state.padsActive[index]; if (state.padsActive[index]) { pad.classList.add('active'); } else { pad.classList.remove('active'); } } function setupTurntable(deckEl, discEl, isDeckA) { deckEl.addEventListener('pointerdown', (e) => { startSequencer(); deckEl.setPointerCapture(e.pointerId); const rect = deckEl.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; const angle = Math.atan2(e.clientY - centerY, e.clientX - centerX) * 180 / Math.PI; if (isDeckA) { state.isDraggingA = true; state.lastAngleA = angle; } else { state.isDraggingB = true; state.lastAngleB = angle; } }); deckEl.addEventListener('pointermove', (e) => { if (isDeckA && !state.isDraggingA) return; if (!isDeckA && !state.isDraggingB) return; const rect = deckEl.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; const angle = Math.atan2(e.clientY - centerY, e.clientX - centerX) * 180 / Math.PI; let delta = 0; if (isDeckA) { delta = angle - state.lastAngleA; // Handle wrap around if (delta > 180) delta -= 360; if (delta < -180) delta += 360; state.rotationA += delta; state.lastAngleA = angle; discEl.style.transform = `rotate(${state.rotationA}deg)`; } else { delta = angle - state.lastAngleB; if (delta > 180) delta -= 360; if (delta < -180) delta += 360; state.rotationB += delta; state.lastAngleB = angle; discEl.style.transform = `rotate(${state.rotationB}deg)`; } // Play scratch sound based on drag speed if (Math.abs(delta) > 2) { const scratchEl = document.getElementById('audio-scratch'); if (scratchEl && scratchEl.src && scratchEl.src !== window.location.href) { if(Math.random() > 0.8) { scratchEl.currentTime = 0; scratchEl.play().catch(()=>{}); } } else { triggerScratch(delta); } } }); deckEl.addEventListener('pointerup', (e) => { if (isDeckA) state.isDraggingA = false; else state.isDraggingB = false; deckEl.releasePointerCapture(e.pointerId); }); } function setupMixer() { const trackRect = elements.faderTrack.getBoundingClientRect(); const updateFaderVisuals = () => { const percent = state.faderValue * 100; elements.faderCap.style.left = `${percent}%`; // Update audio volumes/filters based on crossfader if (state.audioCtx) { // Linear panning const gainA = Math.cos(state.faderValue * 0.5 * Math.PI); const gainB = Math.cos((1.0 - state.faderValue) * 0.5 * Math.PI); state.nodes.busA.gain.setTargetAtTime(gainA, state.audioCtx.currentTime, 0.05); state.nodes.busB.gain.setTargetAtTime(gainB, state.audioCtx.currentTime, 0.05); // Filter effect at extremes if (state.faderValue > 0.8) { state.filterNodeA.frequency.setTargetAtTime(500, state.audioCtx.currentTime, 0.1); } else { state.filterNodeA.frequency.setTargetAtTime(22000, state.audioCtx.currentTime, 0.1); } if (state.faderValue < 0.2) { state.filterNodeB.frequency.setTargetAtTime(500, state.audioCtx.currentTime, 0.1); } else { state.filterNodeB.frequency.setTargetAtTime(22000, state.audioCtx.currentTime, 0.1); } } }; elements.faderTrack.addEventListener('pointerdown', (e) => { elements.faderTrack.setPointerCapture(e.pointerId); const handleMove = (ev) => { const rect = elements.faderTrack.getBoundingClientRect(); let x = ev.clientX - rect.left; x = Math.max(0, Math.min(x, rect.width)); state.faderValue = x / rect.width; updateFaderVisuals(); }; handleMove(e); const moveListener = (ev) => handleMove(ev); const upListener = () => { elements.faderTrack.removeEventListener('pointermove', moveListener); elements.faderTrack.removeEventListener('pointerup', upListener); }; elements.faderTrack.addEventListener('pointermove', moveListener); elements.faderTrack.addEventListener('pointerup', upListener); }); updateFaderVisuals(); // Init position } function updateMixer() { // Helper to call from init if(state.audioCtx) { const gainA = Math.cos(state.faderValue * 0.5 * Math.PI); const gainB = Math.cos((1.0 - state.faderValue) * 0.5 * Math.PI); state.nodes.busA.gain.value = gainA; state.nodes.busB.gain.value = gainB; state.filterNodeA.frequency.value = 22000; state.filterNodeB.frequency.value = 22000; } } // --- Visuals & Animations --- function rotateDiscs() { // Base rotation based on tempo const rps = (window.appState.bpm / 60) * 0.5; // Half speed for visuals const degreesPerFrame = rps * 360 / 60; // Assuming 60fps if (state.isPlaying) { if (!state.isDraggingA && state.padsActive.slice(0,4).some(a=>a)) { state.rotationA += degreesPerFrame; elements.discA.style.transform = `rotate(${state.rotationA}deg)`; } if (!state.isDraggingB && state.padsActive.slice(4,8).some(a=>a)) { state.rotationB += degreesPerFrame; elements.discB.style.transform = `rotate(${state.rotationB}deg)`; } } requestAnimationFrame(rotateDiscs); } function resizeCanvas() { const rect = elements.canvas.parentElement.getBoundingClientRect(); elements.canvas.width = rect.width; elements.canvas.height = rect.height; } function drawVisualizer() { if (!state.analyser) return; requestAnimationFrame(drawVisualizer); const w = elements.canvas.width; const h = elements.canvas.height; const ctx = elements.ctx; state.analyser.getByteFrequencyData(state.visualizerData); ctx.clearRect(0, 0, w, h); const barWidth = (w / state.visualizerData.length) * 2.5; let x = 0; for (let i = 0; i < state.visualizerData.length; i++) { const val = state.visualizerData[i]; const percent = val / 255; const barHeight = h * percent * 0.8; // Max 80% height // Color based on height const hue = i * (360 / state.visualizerData.length) + (Date.now() / 20); ctx.fillStyle = `hsla(${hue}, 100%, 50%, 0.6)`; // Draw rounded bottom bars ctx.beginPath(); ctx.roundRect(x, h - barHeight, barWidth - 2, barHeight, [10, 10, 0, 0]); ctx.fill(); x += barWidth; } } function initBGM() { const el = document.getElementById('audio-bgm'); if (!el?.src) return; el.volume = 0.3; const playBgm = () => { el.play().catch(() => { document.addEventListener('touchstart', playBgm, { once: true }); document.addEventListener('click', playBgm, { once: true }); }); }; playBgm(); } function initApp() { applyAllEditableValues(); initBGM(); // Layout setups resizeCanvas(); window.addEventListener('resize', resizeCanvas); // Event Listeners elements.pads.forEach(pad => { pad.addEventListener('pointerdown', handlePadClick); }); // Piano Keys elements.keys.forEach(key => { key.addEventListener('pointerdown', (e) => { startSequencer(); playPianoKey(parseInt(key.dataset.note)); }); }); // Guitar Strings elements.strings.forEach(str => { str.addEventListener('pointerdown', (e) => { startSequencer(); playGuitarString(parseInt(str.dataset.note)); }); }); // Mic Controls elements.btnRecord.addEventListener('click', () => { startSequencer(); toggleRecording(); }); elements.micEcho.addEventListener('input', (e) => { if (state.micEchoNode) state.micEchoNode.delayTime.setTargetAtTime(parseFloat(e.target.value), state.audioCtx.currentTime, 0.05); }); elements.micFilter.addEventListener('input', (e) => { if (state.micFilterNode) state.micFilterNode.frequency.setTargetAtTime(parseFloat(e.target.value), state.audioCtx.currentTime, 0.05); }); setupTurntable(elements.deckA, elements.discA, true); setupTurntable(elements.deckB, elements.discB, false); setupMixer(); // Start overlay elements.btnStart.addEventListener('click', () => { elements.startOverlay.classList.add('hidden'); startSequencer(); }); // Start silent if user dismisses another way (safety) document.addEventListener('pointerdown', (e) => { if(!state.isPlaying && e.target.closest('#deck-area')) { startSequencer(); elements.startOverlay.classList.add('hidden'); } }, {once: true}); } document.addEventListener('DOMContentLoaded', initApp);