cyberbeats/public/app.js

629 lines
21 KiB
JavaScript
Raw Normal View History

2026-09-09 22:40:41 +00:00
/* =========================================================
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
};
})();