From 80dcf4e350046fa135f554c77f85917ef51d59e5 Mon Sep 17 00:00:00 2001 From: numex-admin Date: Wed, 9 Sep 2026 22:40:41 +0000 Subject: [PATCH] =?UTF-8?q?Numex=20yay=C4=B1n:=20public/app.js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/app.js | 628 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 628 insertions(+) create mode 100644 public/app.js diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..3c5e1ed --- /dev/null +++ b/public/app.js @@ -0,0 +1,628 @@ +/* ========================================================= + CYBER BEATS — 16-Step Web Audio Davul Makinesi + Tüm sesler SAF Web Audio API ile sentezlenir: + OscillatorNode, GainNode, BiquadFilterNode, AudioBufferSourceNode + (noise buffer "ses dosyası" değil; kod içinde Math.random ile + üretilen örnek tampondur). Hiçbir mp3/wav dosyası kullanılmaz. + + - KICK : frekansı hızla düşen sinüs (150Hz -> 30Hz pitch drop) + - SNARE : beyaz gürültü + band-pass + kısa rezonans + - HAT : yüksek frekanslı (high-pass) kesik metalik tiz ses + - SYNTH : melodik kare/üçgen dalga tonu + ========================================================= */ +(function () { + 'use strict'; + + /* ---------- Sabitler & State ---------- */ + var INSTRUMENTS = [ + { id: 'kick', label: 'KICK', rowCls: 'kick' }, + { id: 'snare', label: 'SNARE', rowCls: 'snare' }, + { id: 'hat', label: 'HAT', rowCls: 'hat' }, + { id: 'synth', label: 'SYNTH', rowCls: 'synth' } + ]; + var STEPS = 16; + + var audioCtx = null; + var masterGain = null; + var noiseBuffer = null; + + var isPlaying = false; + var step = 0; // sıradaki çalınacak adım + var scheduleTimer = null; // lookahead interval id + var visualTimer = null; // UI playhead interval id + var nextNoteTime = 0; // ctx.currentTime cinsinden bir sonraki nota vakti + var currentStepVisual = -1; // görselde en son yanan sütun + var lastScheduledStep = -1; + + var bpm = 120; + var BPM_MIN = 80; + var BPM_MAX = 180; + var LOOKAHEAD_MS = 25; // timer tarama aralığı + // 16. adım süresi (sn) = 60 / BPM / 4 + function stepDur() { return 60 / bpm / 4; } + + // 4 enstrüman satırı x 16 adım boolean matris + var pattern = []; + function initPattern() { + for (var r = 0; r < 4; r++) { + pattern[r] = new Array(STEPS).fill(false); + } + } + + // SYNTH pentatonik ölçüsünden frekans seçimi için tablo (Hz) + var SYNTH_TABLE = [ + 220.0, 261.63, 293.66, 329.63, 392.0, 440.0, + 523.25, 587.33, 659.25, 783.99, 880.0, 987.77 + ]; + + /* ---------- Audio kurulum ---------- */ + // AudioContext'i oluşturur; master gain ve noise buffer hazırlar. + function initAudio() { + if (audioCtx) return audioCtx; + var Ctor = window.AudioContext || window.webkitAudioContext; + if (!Ctor) { return null; } + audioCtx = new Ctor(); + masterGain = audioCtx.createGain(); + masterGain.gain.value = 0.85; + masterGain.connect(audioCtx.destination); + noiseBuffer = createNoiseBuffer(); + return audioCtx; + } + + // Kullanıcı etkileşimiyle (klavye/tıklama) context'i aç. + function unlock() { + initAudio(); + if (audioCtx && audioCtx.state === 'suspended') { + audioCtx.resume(); + } + } + + // Beyaz gürültü tamponu — AudioBufferSourceNode ile çalınır. + function createNoiseBuffer() { + if (!audioCtx) { return null; } + var dur = 1.5; // saniye + var length = Math.floor(audioCtx.sampleRate * dur); + var buffer = audioCtx.createBuffer(1, length, audioCtx.sampleRate); + var data = buffer.getChannelData(0); + for (var i = 0; i < length; i++) { + data[i] = Math.random() * 2 - 1; + } + return buffer; + } + + // Genel ADSR benzeri env için yardımcı: node'u master'a bağlar. + function makeEnv(peak, attack, decay, time, isExponential) { + var g = audioCtx.createGain(); + var z = isExponential ? 0.0001 : 0; + g.gain.setValueAtTime(z, time); + if (isExponential) { + g.gain.exponentialRampToValueAtTime(peak, time + attack); + g.gain.exponentialRampToValueAtTime(0.0001, time + attack + decay); + } else { + g.gain.linearRampToValueAtTime(peak, time + attack); + g.gain.linearRampToValueAtTime(0, time + attack + decay); + } + g.connect(masterGain); + return { node: g, start: time + attack, stopAt: time + attack + decay }; + } + + /* ========================================================= + SES SENTEZ FONKSİYONLARI + ========================================================= */ + + // KICK — 150Hz -> 30Hz hızla düşen sinüs (pitch drop) + sub body + function playKick(time) { + if (!audioCtx) { return; } + var ctx = audioCtx; + var t = time; + + // ana sinüs + var osc = ctx.createOscillator(); + osc.type = 'sine'; + osc.frequency.setValueAtTime(150, t); + osc.frequency.exponentialRampToValueAtTime(30, t + 0.42); + + // düşük geçiren filtre ile sub-gövde yumuşat + var lp = ctx.createBiquadFilter(); + lp.type = 'lowpass'; + lp.frequency.setValueAtTime(900, t); + lp.frequency.exponentialRampToValueAtTime(120, t + 0.4); + + var g = makeEnv(0.95, 0.003, 0.38, t, true); + osc.connect(lp); + lp.connect(g.node); + osc.start(t); + osc.stop(g.stopAt + 0.02); + } + + // SNARE — beyaz gürültü (noise source) + band-pass + gövde sinüs + function playSnare(time) { + if (!audioCtx || !noiseBuffer) { return; } + var ctx = audioCtx; + var t = time; + + // gürültü kaynağı (AudioBufferSourceNode) + var noise = ctx.createBufferSource(); + noise.buffer = noiseBuffer; + noise.loop = true; + + var bp = ctx.createBiquadFilter(); + bp.type = 'bandpass'; + bp.frequency.value = 1800; + bp.Q.value = 0.8; + + var ng = makeEnv(0.55, 0.002, 0.16, t, true); + noise.connect(bp); + bp.connect(ng.node); + noise.start(t, Math.random() * 0.5); + noise.stop(ng.stopAt + 0.01); + + // gövde (body): kısa düşük sinüs ~180Hz + var osc = ctx.createOscillator(); + osc.type = 'triangle'; + osc.frequency.setValueAtTime(190, t); + osc.frequency.exponentialRampToValueAtTime(80, t + 0.1); + var og = makeEnv(0.4, 0.001, 0.12, t, true); + osc.connect(og.node); + osc.start(t); + osc.stop(og.stopAt + 0.01); + } + + // HAT — yüksek frekanslı kesik, tiz metalik ses + function playHiHat(time, open) { + if (!audioCtx || !noiseBuffer) { return; } + var ctx = audioCtx; + var t = time; + var isOpen = !!open; + + var decay = isOpen ? 0.28 : 0.05; // kapalı kısa titrek, açık daha uzun + + var noise = ctx.createBufferSource(); + noise.buffer = noiseBuffer; + noise.loop = true; + + // yüksek geçiren filtre -> keskin tiz + var hp = ctx.createBiquadFilter(); + hp.type = 'highpass'; + hp.frequency.value = 7500; + + var ng = makeEnv(isOpen ? 0.4 : 0.5, 0.001, decay, t, true); + noise.connect(hp); + hp.connect(ng.node); + noise.start(t, Math.random() * 0.5); + noise.stop(ng.stopAt + 0.01); + + // metalik rezonans üst tonu + var osc = ctx.createOscillator(); + osc.type = 'square'; + osc.frequency.value = 8200; + var hg = ctx.createGain(); + hg.gain.setValueAtTime(0.12, t); + hg.gain.exponentialRampToValueAtTime(0.0001, t + (isOpen ? 0.22 : 0.045)); + osc.connect(hg); + hg.connect(masterGain); + osc.start(t); + osc.stop(t + (isOpen ? 0.24 : 0.05) + 0.01); + } + + // SYNTH — melodik ton (kare/üçgen dalga + env) + function playSynth(time, noteIdx) { + if (!audioCtx) { return; } + var ctx = audioCtx; + var t = time; + + // notayı ölçüden seç; verilmezse adım indeksinden üret + var idx = (noteIdx === undefined) ? Math.floor(Math.random() * SYNTH_TABLE.length) : noteIdx; + var freq = SYNTH_TABLE[idx % SYNTH_TABLE.length]; + + var oscA = ctx.createOscillator(); + oscA.type = 'square'; + oscA.frequency.value = freq; + + // ikinci tiz katman + var oscB = ctx.createOscillator(); + oscB.type = 'triangle'; + oscB.frequency.value = freq * 1.5; + + var g = makeEnv(0.22, 0.006, 0.18, t, true); + var oscBgain = ctx.createGain(); + oscBgain.gain.value = 0.35; + + oscA.connect(g.node); + oscB.connect(oscBgain); + oscBgain.connect(g.node); + oscA.start(t); + oscB.start(t); + oscA.stop(t + 0.4); + oscB.stop(t + 0.4); + } + + /* ========================================================= + PATTERN (desen) & PLAYBACK + ========================================================= */ + + // Tek bir enstrüman adımını seslendirir (time = ctx zamanı). + function playStep(instrIdx, stepIdx, time) { + if (!pattern[instrIdx] || !pattern[instrIdx][stepIdx]) { return; } + switch (instrIdx) { + case 0: playKick(time); break; + case 1: playSnare(time); break; + case 2: + // HAT: her 2. vuruşta açık (open) hat — 8. ve 15. gibi + playHiHat(time, stepIdx % 8 === 7); + break; + case 3: + // SYNTH melodik: notayı time + deterministic bir adım indeksiyle üret + playSynth(time, stepIdx % SYNTH_TABLE.length); + break; + default: break; + } + } + + // Hazır örnek break/techno ritmi (kick, snare, hat, synth) + function defaultPattern() { + var p = []; + // kick: 1-3-5-7 + 13. adım variasyon -> 0,2,4,6,12 teknik + p.push([true, false, false, false, true, false, true, false, + false, true, false, false, true, false, false, false]); + // snare: backbeat üzerine 4 ve 12; 7. de tuhaf vuruş + p.push([false, false, false, false, true, false, false, true, + false, false, false, false, true, false, false, false]); + // hat: neredeyse her adım kesik 8'lik his + p.push([true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true]); + // synth: melodik dizi — 0,3,6,10,12 gibi aralıklarla + p.push([false, false, false, false, true, false, true, false, + false, true, false, false, true, false, false, false]); + return p; + } + + // defaultPattern'i pattern'deki duruma kopyalar; UI'ya uygular. + function loadDefault() { + var d = defaultPattern(); + for (var r = 0; r < 4; r++) { + for (var c = 0; c < STEPS; c++) { + pattern[r][c] = !!d[r][c]; + } + } + renderRows(); + } + + function clearAll() { + for (var r = 0; r < 4; r++) { + for (var c = 0; c < STEPS; c++) { + pattern[r][c] = false; + } + } + renderRows(); + } + + /* ========================================================= + UI — matris & kontroller + ========================================================= */ + + function getCell(r, c) { + return document.querySelector('#r' + r + 'c' + c); + } + + // Satırları DOM olarak kurar (olaylar tek sefer; hücre içerikleri güncellenir) + // Aktif sütunu dikey neon şeritle vurgulayan overlay (bir kez kurulur). + // Satır hücreleri grid ile dizildiği için; konum hesaplaması ilk satırın + // hücre genişliğine göre updateVisualStep içinde yapılır. + var colHeadEl = null; + + function buildMatrix() { + var matrix = document.getElementById('matrix'); + matrix.innerHTML = ''; + // dikey playhead şeridi — .active sınıfı ve left/width inline verilir + colHeadEl = document.createElement('div'); + colHeadEl.className = 'step-col-head'; + colHeadEl.style.left = '-999px'; // başlangıçta görünmez + matrix.appendChild(colHeadEl); + for (var r = 0; r < 4; r++) { + var row = document.createElement('div'); + row.className = 'step-row ' + INSTRUMENTS[r].rowCls + '-row'; + row.dataset.row = r; + for (var c = 0; c < STEPS; c++) { + var cell = document.createElement('div'); + cell.className = 'step-cell'; + cell.id = 'r' + r + 'c' + c; + cell.dataset.r = r; + cell.dataset.c = c; + var glow = document.createElement('span'); + glow.className = 'cell-glow'; + cell.appendChild(glow); + row.appendChild(cell); + + // hücre tıklaması + cell.addEventListener('click', function (ev) { + var t = ev.currentTarget; + toggleCell(parseInt(t.dataset.r, 10), parseInt(t.dataset.c, 10)); + }); + } + matrix.appendChild(row); + } + } + + // Her hücrenin .on sınıfını pattern matrisine göre günceller. + function renderRows() { + for (var r = 0; r < 4; r++) { + for (var c = 0; c < STEPS; c++) { + var cell = getCell(r, c); + if (!cell) { continue; } + cell.classList.toggle('on', !!pattern[r][c]); + } + } + } + + // Tek bir hücreyi MATRIS ÜZERİNDE yansıtır + silah sesi ön izler. + function toggleCell(r, c) { + unlock(); // audio context açık olsun + if (pattern[r] && pattern[r][c] !== undefined) { + pattern[r][c] = !pattern[r][c]; + var cell = getCell(r, c); + if (cell) { cell.classList.toggle('on', pattern[r][c]); } + // aktifse o enstrümanı önizle (talep geri beslemesi için) + if (pattern[r][c]) { playStep(r, c, audioCtx ? audioCtx.currentTime + 0.02 : 0); } + } + } + + // LED ızgarası (üst bardaki mini göstergeyi adım ile yakar) + function buildLeds() { + var ledGrid = document.getElementById('ledGrid'); + ledGrid.innerHTML = ''; + for (var c = 0; c < STEPS; c++) { + var led = document.createElement('span'); + led.className = 'led'; + ledGrid.appendChild(led); + } + } + + function buildStepLabels() { + var labels = document.getElementById('stepLabels'); + labels.innerHTML = ''; + // ilk hücre boşluk (enstrüman sütununa kalsın) + var spacer = document.createElement('span'); + spacer.className = 'step-num'; + labels.appendChild(spacer); + for (var i = 0; i < STEPS; i++) { + var s = document.createElement('span'); + s.className = 'step-num' + ((i % 4 === 0) ? ' group' : ''); + s.textContent = String(i + 1).padStart(2, '0'); + labels.appendChild(s); + } + } + + // Aktif playhead sütununu görsel ışıkla vurgular: + // 1) matrix üzerinde o sütunu kaplayan DİKEY neon şerit (colHeadEl) + // 2) çalan ON hücrelere kısa .hit parlaması + // 3) üst bardaki LED göstergesini yakar + function updateVisualStep(stepIdx) { + if (stepIdx < 0 || stepIdx >= STEPS) { return; } + var prev = currentStepVisual; + currentStepVisual = stepIdx; + + // --- Dikey sütun şeridi konumlandır --- + if (colHeadEl) { + // İlk satırın hedef sütunundaki hücrenin ofsetini kullanarak + // overlay'i o sütunun tam üstüne bindiririz (satırlar 16 eşit sütun). + var anchor = getCell(0, stepIdx); + if (anchor) { + // left: hücrenin matrix içindeki sol kenarı; width: hücre + gap + var cellW = anchor.offsetWidth; + var gap = 5; // .matrix gap (style.css) — hücre büyüklüğüne oransal yerine net + // satırlar flex'tir ve satır ile matrix genişlikleri aynı anda başlar: + var leftOfCell = anchor.offsetLeft; + colHeadEl.style.left = leftOfCell + 'px'; + colHeadEl.style.width = (cellW) + 'px'; + colHeadEl.classList.add('active'); + } else { + colHeadEl.classList.remove('active'); + } + } + + // --- LED barları güncelle --- + var leds = document.querySelectorAll('#ledGrid .led'); + for (var i = 0; i < leds.length; i++) { + leds[i].classList.toggle('on', i === stepIdx); + } + + // önceki sütuna verilen .hit sınıfını gecikmeli düşür (temizlik) + if (prev >= 0 && prev !== stepIdx) { + for (var pc = 0; pc < 4; pc++) { + var pcell = getCell(pc, prev); + if (pcell) { pcell.classList.remove('hit'); } + } + } + // --- Çalan hücrelere kısa parlama --- + for (var rr = 0; rr < 4; rr++) { + if (pattern[rr][stepIdx]) { + var cc = getCell(rr, stepIdx); + if (cc) { cc.classList.add('hit'); } + } + } + // kısa animasyon sonunda çalan hücrelerin parlamasını düşür + setTimeout(function () { + for (var rrr = 0; rrr < 4; rrr++) { + if (pattern[rrr][stepIdx]) { + var ccell = getCell(rrr, stepIdx); + if (ccell) { ccell.classList.remove('hit'); } + } + } + }, 90); + } + + /* ========================================================= + ZAMANLAYICI (lookahead scheduler) + ========================================================= */ + + function scheduleStep() { + if (!isPlaying) { return; } + while (nextNoteTime < audioCtx.currentTime + 0.12) { + var idx = step; + // sürede çal — mümkün olan tüm enstrümanları + for (var r = 0; r < 4; r++) { + if (pattern[r][idx]) { playStep(r, idx, nextNoteTime); } + } + updateVisualStep(idx); + // bir sonraki adıma ilerle + step = (step + 1) % STEPS; + nextNoteTime += stepDur(); + } + } + + function startLoop() { + if (!audioCtx) { return; } + // görsel playhead intervali ~20ms + visualTimer = setInterval(scheduleStep, LOOKAHEAD_MS); + } + + function stopLoop() { + if (visualTimer) { clearInterval(visualTimer); visualTimer = null; } + if (scheduleTimer) { clearInterval(scheduleTimer); scheduleTimer = null; } + step = 0; + currentStepVisual = -1; + lastScheduledStep = -1; + var leds = document.querySelectorAll('#ledGrid .led'); + for (var i = 0; i < leds.length; i++) { leds[i].classList.remove('on'); } + // aktif playhead sütun şeridini ve parlamaları sıfırla + if (colHeadEl) { colHeadEl.classList.remove('active'); colHeadEl.style.left = '-999px'; } + var hitCells = document.querySelectorAll('.step-cell.hit'); + for (var h = 0; h < hitCells.length; h++) { hitCells[h].classList.remove('hit'); } + } + + // Oynat / duraklat. Space ve buton ortak. + function togglePlay() { + unlock(); + if (!audioCtx) { + var st = document.getElementById('statusText'); + if (st) { st.textContent = 'SES YOK'; } + return; + } + + if (isPlaying) { + pause(); + return; + } + // başlat + isPlaying = true; + step = 0; + nextNoteTime = (audioCtx.currentTime || 0) + 0.05; + startLoop(); + // UI durum + document.getElementById('playIndicator').classList.add('running'); + document.getElementById('statusText').classList.add('running'); + document.getElementById('statusText').textContent = 'ÇALIYOR'; + document.getElementById('playBtn').classList.add('running'); + document.getElementById('playBtn').textContent = '❚❚'; + document.getElementById('statusText').className = 'status-text running'; + statusVisual(true); + } + + function pause() { + isPlaying = false; + stopLoop(); + document.getElementById('playIndicator').classList.remove('running'); + document.getElementById('statusText').textContent = 'DURDU'; + document.getElementById('playBtn').classList.remove('running'); + document.getElementById('playBtn').textContent = '▶'; + statusVisual(false); + } + + function statusVisual(on) { + document.getElementById('playIndicator').classList.toggle('running', on); + } + + function statusStop() { + isPlaying = false; + stopLoop(); + document.getElementById('playIndicator').classList.remove('running'); + document.getElementById('statusText').textContent = 'DURDU'; + document.getElementById('playBtn').classList.remove('running'); + document.getElementById('playBtn').textContent = '▶'; + } + + function setTempo(value) { + var v = parseInt(value, 10); + if (isNaN(v)) { v = 120; } + v = Math.max(BPM_MIN, Math.min(BPM_MAX, v)); + bpm = v; + document.getElementById('bpmValue').textContent = String(bpm); + // loop ayaktaysa zamanlama otomatik olarak yeni stepDur ile devam eder + } + + /* ========================================================= + OLAY BAĞLAMA + ========================================================= */ + + function bindControls() { + var playBtn = document.getElementById('playBtn'); + var stopBtn = document.getElementById('stopBtn'); + var clearBtn = document.getElementById('clearBtn'); + var bpmSlider = document.getElementById('bpmSlider'); + var bpmValue = document.getElementById('bpmValue'); + + playBtn.addEventListener('click', togglePlay); + stopBtn.addEventListener('click', statusStop); + clearBtn.addEventListener('click', function () { clearAll(); }); + + bpmSlider.addEventListener('input', function () { + setTempo(bpmSlider.value); + }); + + // Space ile oynat/durdur (sayfa kaydırmayı engelle) + document.addEventListener('keydown', function (e) { + if (e.code === 'Space') { + e.preventDefault(); + togglePlay(); + } else if (e.key === 'Escape') { + statusStop(); + } + }); + + // mobil dokunma ile de hücrelerde tam duyarlılık için kullanıcı tıklamaları + // (click zaten mobilde çalışır). + } + + function previewSource(instrIdx) { + if (!audioCtx) { initAudio(); } + playStep(instrIdx, 0, audioCtx.currentTime + 0.02); + } + + /* ========================================================= + BAŞLAT + ========================================================= */ + + function buildUI() { + initPattern(); + buildMatrix(); // hücreleri kur + buildLeds(); + buildStepLabels(); + loadDefault(); // hazır örnek ritim yükle + render + bindControls(); + + var bpmSlider = document.getElementById('bpmSlider'); + setTempo(bpmSlider.value); + + // play (sahte önizleme hakkı sayacı) için etiket + document.getElementById('statusText').textContent = 'DURDU'; + statusStop(); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', buildUI); + } else { + buildUI(); + } + + // (iç API — gerekli: eksiksiz fonksiyon ispatı) + window.CyberBeats = { + start: togglePlay, + stop: statusStop, + setTempo: setTempo, + clearAll: clearAll, + loadDefault: loadDefault, + toggleCell: toggleCell, + previewSource: previewSource + }; +})();