Synthesize real Lofi Hip Hop beat in Web Audio API along with rain

This commit is contained in:
Felix Brabetz
2026-05-16 05:48:07 +02:00
parent d499b2ba5f
commit ae49abd168
+78 -109
View File
@@ -2,50 +2,18 @@
// src/components/SubpageRain.astro // src/components/SubpageRain.astro
--- ---
<script is:inline> <script is:inline>
// Load YouTube API
let tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
let firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
window.player = null;
window.onYouTubeIframeAPIReady = function() {
window.player = new YT.Player('lofi-player', {
height: '0',
width: '0',
videoId: 'jfKfPfyJRdk',
playerVars: {
'autoplay': 1,
'controls': 0,
'mute': 0
},
events: {
'onReady': function(event) {
console.log('YouTube Player Ready');
// Set initial volume
if(window.playerData) {
event.target.setVolume(window.playerData.lofiVolume);
}
}
}
});
}
window.rainData = function() { window.rainData = function() {
// Store reference to data for API callback return {
let data = {
rainIntensity: 50, rainIntensity: 50,
wind: 0, wind: 0,
lofiFilter: 800, lofiFilter: 800,
musicVolume: 30, musicVolume: 50,
lofiVolume: 50,
lofiPlaying: true,
drops: [], drops: [],
audioCtx: null, audioCtx: null,
noiseNode: null, noiseNode: null,
filterNode: null, filterNode: null,
gainNode: null, gainNode: null,
melodyGain: null, beatInterval: null,
generateDrops(val) { generateDrops(val) {
this.drops = []; this.drops = [];
@@ -57,7 +25,7 @@ window.rainData = function() {
try { try {
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
// --- REGEN-SOUND --- // --- REGEN-SOUND (Weißes Rauschen) ---
let bufferSize = 2 * this.audioCtx.sampleRate; let bufferSize = 2 * this.audioCtx.sampleRate;
let noiseBuffer = this.audioCtx.createBuffer(1, bufferSize, this.audioCtx.sampleRate); let noiseBuffer = this.audioCtx.createBuffer(1, bufferSize, this.audioCtx.sampleRate);
let output = noiseBuffer.getChannelData(0); let output = noiseBuffer.getChannelData(0);
@@ -82,42 +50,87 @@ window.rainData = function() {
this.noiseNode.start(); this.noiseNode.start();
// --- MELODIE-SOUND --- // --- BEAT GENERATOR (Lofi Hip Hop) ---
let melodyOsc = this.audioCtx.createOscillator(); let bpm = 70;
this.melodyGain = this.audioCtx.createGain(); let stepTime = 60 / bpm / 2; // 8tel Noten
melodyOsc.type = 'sine'; let step = 0;
let lowpass = this.audioCtx.createBiquadFilter(); this.beatInterval = setInterval(() => {
lowpass.type = 'lowpass';
lowpass.frequency.setValueAtTime(400, this.audioCtx.currentTime);
melodyOsc.connect(lowpass);
lowpass.connect(this.melodyGain);
this.melodyGain.connect(this.audioCtx.destination);
melodyOsc.start();
let notes = [261.63, 329.63, 392.00, 440.00, 392.00, 329.63];
let i = 0;
setInterval(() => {
let now = this.audioCtx.currentTime; let now = this.audioCtx.currentTime;
let freq = notes[i];
melodyOsc.frequency.setValueAtTime(freq, now);
let maxVol = (this.musicVolume / 100) * 0.05; // Boom Bap Beat (Kick, Snare, Hi-Hat)
this.melodyGain.gain.setValueAtTime(0, now); if (step === 0 || step === 3) {
this.melodyGain.gain.linearRampToValueAtTime(maxVol, now + 1.5); this.playKick(now);
this.melodyGain.gain.linearRampToValueAtTime(0, now + 3.5); }
if (step === 4) {
this.playSnare(now, noiseBuffer);
}
if (step % 2 === 1) {
this.playHiHat(now, noiseBuffer);
}
i = (i + 1) % notes.length; step = (step + 1) % 8;
}, 4000); }, stepTime * 1000);
} catch(e) { } catch(e) {
console.log('Audio failed', e); console.log('Audio failed', e);
} }
}, },
playKick(time) {
let osc = this.audioCtx.createOscillator();
let gain = this.audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(120, time);
osc.frequency.exponentialRampToValueAtTime(40, time + 0.15); // Pitch drop
gain.gain.setValueAtTime((this.musicVolume / 100) * 0.4, time);
gain.gain.linearRampToValueAtTime(0, time + 0.2);
osc.connect(gain);
gain.connect(this.audioCtx.destination);
osc.start(time);
osc.stop(time + 0.2);
},
playSnare(time, noiseBuffer) {
let noise = this.audioCtx.createBufferSource();
noise.buffer = noiseBuffer;
let filter = this.audioCtx.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.setValueAtTime(1000, time);
let gain = this.audioCtx.createGain();
gain.gain.setValueAtTime((this.musicVolume / 100) * 0.2, time);
gain.gain.linearRampToValueAtTime(0, time + 0.15);
noise.connect(filter);
filter.connect(gain);
gain.connect(this.audioCtx.destination);
noise.start(time);
noise.stop(time + 0.15);
},
playHiHat(time, noiseBuffer) {
let noise = this.audioCtx.createBufferSource();
noise.buffer = noiseBuffer;
let filter = this.audioCtx.createBiquadFilter();
filter.type = 'highpass';
filter.frequency.setValueAtTime(6000, time);
let gain = this.audioCtx.createGain();
gain.gain.setValueAtTime((this.musicVolume / 100) * 0.05, time);
gain.gain.linearRampToValueAtTime(0, time + 0.05);
noise.connect(filter);
filter.connect(gain);
gain.connect(this.audioCtx.destination);
noise.start(time);
noise.stop(time + 0.05);
},
updateAudio() { updateAudio() {
if (this.gainNode) { if (this.gainNode) {
this.gainNode.gain.linearRampToValueAtTime(this.rainIntensity / 200 * 0.05, this.audioCtx.currentTime + 0.2); this.gainNode.gain.linearRampToValueAtTime(this.rainIntensity / 200 * 0.05, this.audioCtx.currentTime + 0.2);
@@ -125,35 +138,15 @@ window.rainData = function() {
if (this.filterNode) { if (this.filterNode) {
this.filterNode.frequency.linearRampToValueAtTime(this.lofiFilter, this.audioCtx.currentTime + 0.2); this.filterNode.frequency.linearRampToValueAtTime(this.lofiFilter, this.audioCtx.currentTime + 0.2);
} }
},
toggleLofi() {
if (window.player) {
if (this.lofiPlaying) {
window.player.pauseVideo();
this.lofiPlaying = false;
} else {
window.player.playVideo();
this.lofiPlaying = true;
}
}
},
updateLofiVolume() {
if (window.player) {
window.player.setVolume(this.lofiVolume);
}
} }
}; };
window.playerData = data; // Make accessible to API
return data;
} }
</script> </script>
<!-- Subpage: Rain Detail --> <!-- Subpage: Rain Detail -->
<div x-show="currentPage === 'page-rain'" x-cloak x-transition:enter="transition ease-out duration-500" x-transition:enter-start="opacity-0 translate-y-4" x-transition:enter-end="opacity-100 translate-y-0" class="min-h-screen pt-28 pb-24 px-6 lg:px-8 bg-stone-950 relative overflow-hidden" <div x-show="currentPage === 'page-rain'" x-cloak x-transition:enter="transition ease-out duration-500" x-transition:enter-start="opacity-0 translate-y-4" x-transition:enter-end="opacity-100 translate-y-0" class="min-h-screen pt-28 pb-24 px-6 lg:px-8 bg-stone-950 relative overflow-hidden"
x-data="window.rainData()" x-data="window.rainData()"
x-init="generateDrops(rainIntensity); $watch('rainIntensity', value => { generateDrops(value); updateAudio() }); $watch('lofiFilter', value => updateAudio()); $watch('lofiVolume', value => updateLofiVolume()); $watch('currentPage', val => { if(val === 'page-rain') { initAudio(); } })"> x-init="generateDrops(rainIntensity); $watch('rainIntensity', value => { generateDrops(value); updateAudio() }); $watch('lofiFilter', value => updateAudio()); $watch('currentPage', val => { if(val === 'page-rain') { initAudio(); } })">
<!-- Rain Container (Full Screen on this page) --> <!-- Rain Container (Full Screen on this page) -->
<div class="absolute inset-0 pointer-events-none z-0"> <div class="absolute inset-0 pointer-events-none z-0">
@@ -196,40 +189,16 @@ window.rainData = function() {
<!-- Music Volume --> <!-- Music Volume -->
<div> <div>
<label class="block text-xs font-mono text-stone-500 uppercase mb-2 tracking-widest">Melodie-Lautstärke: <span x-text="musicVolume" class="text-emerald-400"></span>%</label> <label class="block text-xs font-mono text-stone-500 uppercase mb-2 tracking-widest">Beat-Lautstärke: <span x-text="musicVolume" class="text-emerald-400"></span>%</label>
<input type="range" min="0" max="100" x-model="musicVolume" class="w-full h-2 bg-stone-800 rounded-lg appearance-none cursor-pointer accent-emerald-500"> <input type="range" min="0" max="100" x-model="musicVolume" class="w-full h-2 bg-stone-800 rounded-lg appearance-none cursor-pointer accent-emerald-500">
</div> </div>
<div class="flex justify-between text-xs text-stone-600 font-mono pt-4 border-t border-stone-800"> <div class="flex justify-between text-xs text-stone-600 font-mono pt-4 border-t border-stone-800">
<span>Sound läuft automatisch</span> <span>Sound läuft automatisch</span>
<span class="text-emerald-400">Realtime Web Audio Rain & Melody</span> <span class="text-emerald-400">Realtime Web Audio Rain & Lofi Beat</span>
</div> </div>
</div> </div>
<!-- Custom Lofi Controls -->
<div class="glass p-6 rounded-2xl border border-stone-800 space-y-4">
<div class="flex justify-between items-center">
<div>
<span class="block text-xs font-mono text-stone-500 uppercase mb-1 tracking-widest">Zusatz-Musik</span>
<span class="text-xs text-stone-400">Lofi Hip Hop Groove (YouTube Stream)</span>
</div>
<div class="flex items-center gap-4">
<!-- Play/Pause -->
<button @click="toggleLofi()" class="text-white text-xs font-mono flex items-center gap-2 bg-stone-900 px-5 py-2.5 rounded-lg border border-stone-800 hover:border-emerald-500/50 transition-colors">
<i class="fa-solid" :class="lofiPlaying ? 'fa-pause text-emerald-400' : 'fa-play text-emerald-400'"></i>
<span x-text="lofiPlaying ? 'Pause' : 'Play'"></span>
</button>
<!-- Volume -->
<div class="flex items-center gap-2">
<i class="fa-solid fa-volume-low text-stone-500"></i>
<input type="range" min="0" max="100" x-model="lofiVolume" class="w-24 h-1 bg-stone-800 rounded-lg appearance-none cursor-pointer accent-emerald-500">
</div>
</div>
</div>
<!-- Hidden YouTube Player -->
<div id="lofi-player"></div>
</div>
<!-- Content --> <!-- Content -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 text-stone-400 text-sm leading-relaxed"> <div class="grid grid-cols-1 md:grid-cols-2 gap-8 text-stone-400 text-sm leading-relaxed">
<div class="space-y-4"> <div class="space-y-4">