let audioContext = null; let sequence = []; let playhead = 0; let bpm = 100; let drumSounds = {kick: null, snare: null, hiHat: null, clap: null}; let oscillator = [], envelope = []; let button = document.getElementById('play'); let stopButton = document.getElementById('stop'); let clearButton = document.getElementById('clear'); let bpmInput = document.getElementById('bpm'); button.addEventListener('click', play); stopButton.addEventListener('click', stop); clearButton.addEventListener('bpmInput.addEventListener('input', updateBpm); function createOscillator(type) {let o = audioContext.createOscillator(); o.type = type; return o;} function createEnvelope() {let e = audioContext.createEnvelope(); e.attack = 0; e.decay = 0; e.sustain = 0.7; e.release = 0.5; return e;} function play() {sequence = Array.from(document.querySelectorAll('.step')).map((step) => {return step.getAttribute('data-type') !== 'clap' ? step.getAttribute('data-bpm') * (1/120*Math.pow(2, 2/12 * ((sequence.indexOf(step)+1)%16)) * Math.pow(2, 2/12 * (15 - (sequence.indexOf(step)+1)))) : step.getAttribute('data-bpm') * 8/120*1.25 * Math.pow(2, 8/12 * (sequence.indexOf(step))) * 3;}).map((value) => Number(value)); sequence = sequence.map((value, index) => value + 20 * Math.floor((index + playhead) / 16)); let gainNode = audioContext.createGain(); let masterGainNode = audioContext.createGain(); masterGainNode.gain.value = 0; document.querySelectorAll('.row').forEach((row, rowIdx) => {document.querySelectorAll('.step', rowIdx).forEach((step, stepIdx) => {if (sequence[stepIdx*4 + rowIdx]) {let drumType = row.children[stepIdx].getAttribute('data-type'); let sound = drumSounds[drumType]; if (!sound) {sound = oscillator[drumType] = createOscillator(drumType + 'Sound'); let env = envelope[drumType] = createEnvelope(); env.connect(sound); sound.connect(gainNode); gainNode.connect(masterGainNode); masterGainNode.connect(audioContext.destination); }; let freq = Math.floor(sequence[stepIdx*4 + rowIdx]); let volume = sequence[stepIdx*4 + rowIdx] > 40 ? 1 : sequence[stepIdx*4 + rowIdx] / 80; sound.frequency.value = freq; env.attack.value = volume; env.decay.value = (80 - freq) / 10; env.sustain.value = 1; env.release.value = (freq - 20) / 5; gainNode.gain.value = volume; if (!masterGainNode.gain.value) masterGainNode.gain.setValueAtTime(volume, audioContext.currentTime + 0.1/4); } else {if (masterGainNode.gain.value && masterGainNode.gain.value < sequence[stepIdx*4 + rowIdx]/80) masterGainNode.gain.exponentialRampToValueAtTime(sequence[stepIdx*4 + rowIdx]/80, audioContext.currentTime + 0.1/4); if (stepIdx == 15) masterGainNode.gain.exponentialRampToValueAtTime(0, audioContext.currentTime + 0.5/4); } ;}); let stepTimer = setInterval(playStep, 400/audioContext.sampleRate); function playStep() {playhead += Math.min(1, Math.floor(bpmInput.value / 60)); let playheadIdx = (playhead % 16); if (playhead == 0 && sequence.some((value) => !value)) button.click(); document.querySelectorAll('.step').forEach((step, stepIdx) => {step.style.background = (stepIdx == playheadIdx) ? '#ff0000' : (sequence[stepIdx*4 + playheadIdx*4] ? '#22ff22' : '#333333'); }); if (playhead >= 16) {clearInterval(stepTimer); masterGainNode.gain.exponentialRampToValueAtTime(0, audioContext.currentTime + 0.1/4); let enabledStep = document.querySelectorAll('.step')[playheadIdx]; if (sequence[enabledStep.attributes['data-step'].value*4 + playheadIdx*4]) enabledStep.style.background = '#22ff22'; else enabledStep.style.background = '#333333'; } ;};