Created
October 3, 2024 19:50
-
-
Save HelgeSverre/2d65a61ceee9d2fbf5c3ae0d3d0f612d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>DnB Beat Generator Visualization</title> | |
<style> | |
body, html { | |
margin: 0; | |
overflow: hidden; | |
background-color: black; | |
} | |
canvas { | |
position: absolute; | |
top: 0; | |
left: 0; | |
} | |
button { | |
position: absolute; | |
bottom: 20px; | |
left: 50%; | |
transform: translateX(-50%); | |
padding: 10px 20px; | |
font-size: 16px; | |
background-color: #0074D9; | |
color: white; | |
border: none; | |
border-radius: 5px; | |
cursor: pointer; | |
} | |
button.playing { | |
background-color: #ff4136; | |
} | |
</style> | |
</head> | |
<body> | |
<canvas id="visualizer"></canvas> | |
<button id="toggleAudio">Start Audio</button> | |
<script> | |
class DnBBeatGenerator { | |
constructor() { | |
this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
this.bpm = 174; | |
this.sixteenthNote = (60 / this.bpm) / 4; | |
this.barLength = this.sixteenthNote * 16; | |
this.currentBar = 0; | |
this.masterCompressor = this.audioContext.createDynamicsCompressor(); | |
this.masterCompressor.connect(this.audioContext.destination); | |
this.kick = this.createKick(); | |
this.snare = this.createSnare(); | |
this.hihat = this.createHihat(); | |
this.bassline = this.createBassline(); | |
this.reese = this.createReese(); | |
// Start oscillators | |
this.kick.oscillator.start(); | |
this.bassline.oscillator.start(); | |
this.reese.oscillator.start(); | |
} | |
createKick() { | |
const kick = this.audioContext.createOscillator(); | |
const kickEnv = this.audioContext.createGain(); | |
kick.frequency.value = 50; | |
kick.connect(kickEnv); | |
kickEnv.connect(this.masterCompressor); | |
kickEnv.gain.value = 0; | |
return { oscillator: kick, envelope: kickEnv }; | |
} | |
createSnare() { | |
const snareEnv = this.audioContext.createGain(); | |
snareEnv.connect(this.masterCompressor); | |
return { envelope: snareEnv }; | |
} | |
createHihat() { | |
const hihatEnv = this.audioContext.createGain(); | |
hihatEnv.connect(this.masterCompressor); | |
return { envelope: hihatEnv }; | |
} | |
createBassline() { | |
const bass = this.audioContext.createOscillator(); | |
const bassEnv = this.audioContext.createGain(); | |
const bassFilter = this.audioContext.createBiquadFilter(); | |
bass.type = 'sawtooth'; | |
bass.connect(bassFilter); | |
bassFilter.connect(bassEnv); | |
bassEnv.connect(this.masterCompressor); | |
bassFilter.type = 'lowpass'; | |
bassFilter.frequency.value = 100; | |
bassEnv.gain.value = 0; | |
return { oscillator: bass, envelope: bassEnv, filter: bassFilter }; | |
} | |
createReese() { | |
const reese = this.audioContext.createOscillator(); | |
const reeseEnv = this.audioContext.createGain(); | |
const reeseFilter = this.audioContext.createBiquadFilter(); | |
reese.type = 'sawtooth'; | |
reese.connect(reeseFilter); | |
reeseFilter.connect(reeseEnv); | |
reeseEnv.connect(this.masterCompressor); | |
reeseFilter.type = 'lowpass'; | |
reeseFilter.frequency.value = 500; | |
reeseEnv.gain.value = 0; | |
return { oscillator: reese, envelope: reeseEnv, filter: reeseFilter }; | |
} | |
scheduleKick(time) { | |
this.kick.oscillator.frequency.setValueAtTime(50, time); | |
this.kick.oscillator.frequency.exponentialRampToValueAtTime(0.01, time + 0.1); | |
this.kick.envelope.gain.setValueAtTime(1, time); | |
this.kick.envelope.gain.exponentialRampToValueAtTime(0.01, time + 0.2); | |
} | |
scheduleSnare(time) { | |
const bufferSize = this.audioContext.sampleRate * 0.1; | |
const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate); | |
const data = buffer.getChannelData(0); | |
for (let i = 0; i < bufferSize; i++) { | |
data[i] = (Math.random() * 2 - 1) * Math.pow((1 - i / bufferSize), 2); | |
} | |
const snare = this.audioContext.createBufferSource(); | |
snare.buffer = buffer; | |
snare.connect(this.snare.envelope); | |
snare.start(time); | |
this.snare.envelope.gain.setValueAtTime(0.7, time); | |
this.snare.envelope.gain.exponentialRampToValueAtTime(0.01, time + 0.2); | |
} | |
scheduleHihat(time, isOpen) { | |
const bufferSize = this.audioContext.sampleRate * (isOpen ? 0.1 : 0.05); | |
const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate); | |
const data = buffer.getChannelData(0); | |
for (let i = 0; i < bufferSize; i++) { | |
data[i] = (Math.random() * 2 - 1) * Math.pow((1 - i / bufferSize), 4); | |
} | |
const hihat = this.audioContext.createBufferSource(); | |
hihat.buffer = buffer; | |
hihat.connect(this.hihat.envelope); | |
hihat.start(time); | |
this.hihat.envelope.gain.setValueAtTime(isOpen ? 0.3 : 0.2, time); | |
this.hihat.envelope.gain.exponentialRampToValueAtTime(0.01, time + (isOpen ? 0.1 : 0.05)); | |
} | |
scheduleReese(time, note) { | |
const freq = 55 * Math.pow(2, note / 12); | |
this.reese.oscillator.frequency.setValueAtTime(freq, time); | |
this.reese.envelope.gain.setValueAtTime(0.3, time); | |
this.reese.envelope.gain.exponentialRampToValueAtTime(0.01, time + 0.3); | |
this.reese.filter.frequency.setValueAtTime(500, time); | |
this.reese.filter.frequency.exponentialRampToValueAtTime(100, time + 0.2); | |
} | |
scheduleBeat(startTime) { | |
const bassPattern = [0, 0, 7, 7, 3, 3, 10, 10]; | |
const reesePattern = [0, 3, 7, 10]; | |
for (let i = 0; i < 16; i++) { | |
const time = startTime + i * this.sixteenthNote; | |
// Kick on every quarter note | |
if (i % 4 === 0) { | |
this.scheduleKick(time); | |
} | |
// Snare on 2 and 4 | |
if (i === 4 || i === 12) { | |
this.scheduleSnare(time); | |
} | |
// Hi-hats | |
this.scheduleHihat(time, i % 4 === 2); | |
// Bassline | |
if (i % 2 === 0) { | |
const bassNote = bassPattern[i / 2 % bassPattern.length]; | |
this.scheduleBassline(time, bassNote); | |
} | |
// Reese bass (every 8th note on odd bars) | |
if (this.currentBar % 2 === 1 && i % 2 === 0) { | |
const reeseNote = reesePattern[i / 2 % reesePattern.length]; | |
this.scheduleReese(time, reeseNote); | |
} | |
} | |
this.currentBar++; | |
} | |
start() { | |
const scheduleAhead = 0.1; | |
let nextNoteTime = this.audioContext.currentTime; | |
const scheduler = () => { | |
while (nextNoteTime < this.audioContext.currentTime + scheduleAhead) { | |
this.scheduleBeat(nextNoteTime); | |
nextNoteTime += this.barLength; | |
} | |
requestAnimationFrame(scheduler); | |
}; | |
scheduler(); | |
} | |
} | |
// Visualization and UI Logic | |
const canvas = document.getElementById('visualizer'); | |
const ctx = canvas.getContext('2d'); | |
let isPlaying = false; | |
let beatGenerator = null; | |
canvas.width = window.innerWidth; | |
canvas.height = window.innerHeight; | |
const centerX = canvas.width / 2; | |
const centerY = canvas.height / 2; | |
const maxRadius = Math.min(canvas.width, canvas.height) * 0.4; | |
function drawTunnel(time) { | |
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; | |
ctx.fillRect(0, 0, canvas.width, canvas.height); | |
const zoomFactor = Math.sin(time * 0.001) * 0.5 + 1.5; | |
const skewX = Math.sin(time * 0.0007) * 0.3; | |
const skewY = Math.cos(time * 0.0005) * 0.3; | |
for (let radius = maxRadius; radius > 0; radius -= 5) { | |
ctx.save(); | |
ctx.translate(centerX, centerY); | |
ctx.scale(zoomFactor, zoomFactor); | |
ctx.transform(1, skewY, skewX, 1, 0, 0); | |
ctx.beginPath(); | |
for (let angle = 0; angle < Math.PI * 2; angle += 0.1) { | |
const noiseFactor = Math.random() * 50; | |
const pulseFactor = Math.sin(time * 0.01 + radius * 0.05) * 20; | |
const x = Math.cos(angle) * (radius + noiseFactor + pulseFactor); | |
const y = Math.sin(angle) * (radius + noiseFactor + pulseFactor); | |
if (angle === 0) { | |
ctx.moveTo(x, y); | |
} else { | |
ctx.lineTo(x, y); | |
} | |
} | |
ctx.closePath(); | |
const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, maxRadius); | |
const hue = (time * 0.05 + radius) % 360; | |
gradient.addColorStop(0, `hsla(${hue}, 100%, 50%, 0.7)`); | |
gradient.addColorStop(1, `hsla(${(hue + 180) % 360}, 100%, 10%, 0.1)`); | |
ctx.strokeStyle = gradient; | |
ctx.lineWidth = 2; | |
ctx.stroke(); | |
ctx.restore(); | |
} | |
if (Math.random() < 0.05) { | |
ctx.fillStyle = `rgba(255, 255, 255, ${Math.random() * 0.2})`; | |
ctx.fillRect(0, 0, canvas.width, canvas.height); | |
} | |
} | |
function render(time) { | |
drawTunnel(time); | |
requestAnimationFrame(render); | |
} | |
render(0); | |
document.getElementById('toggleAudio').addEventListener('click', () => { | |
if (isPlaying) { | |
// Stop audio | |
isPlaying = false; | |
beatGenerator.audioContext.close(); | |
beatGenerator = null; | |
document.getElementById('toggleAudio').classList.remove('playing'); | |
document.getElementById('toggleAudio').textContent = 'Start Audio'; | |
} else { | |
// Start audio | |
isPlaying = true; | |
beatGenerator = new DnBBeatGenerator(); | |
beatGenerator.start(); | |
document.getElementById('toggleAudio').classList.add('playing'); | |
document.getElementById('toggleAudio').textContent = 'Stop Audio'; | |
} | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment