629 lines
21 KiB
JavaScript
629 lines
21 KiB
JavaScript
/* =========================================================
|
||
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
|
||
};
|
||
})();
|