// Константы типов длительностей и количество шагов внутри ОДНОЙ доли
const DURATION_STEPS = {
'4-Е НОТЫ': 1,
'8-Е НОТЫ': 2,
'8-Е ТРИОЛИ': 3,
'16-Е НОТЫ': 4,
'СЕКСТОЛИ': 6,
'32-Е НОТЫ': 8,
// Названия из селекторов на скриншоте для точного совпадения
'4-е н.': 1,
'8-е н.': 2,
'8-е тр.': 3,
'16-е н.': 4,
'секстоли': 6,
'32-е н.': 8
};
/**
* Класс управления интерактивной сеткой с динамическими долями
*/
class DynamicDrumGrid {
constructor(options) {
this.bpm = options.bpm || 98;
this.numMeasures = options.numMeasures || 1;
// Настройки долей из интерфейса (как на изображении image_cf7223.png)
// Массив строк, соответствующий Доле 1, Доле 2, Доле 3, Доле 4
this.beatSettings = options.beatSettings || ['16-е н.', '16-е н.', 'секстоли', '8-е тр.'];
this.instruments = [
'CRASH1', 'CRASH2', 'RIDEBELL', 'RIDE', 'OPENHAT',
'HAT', 'FOOTHAT', 'TOM1', 'TOM2', 'FLOORTOM', 'SNARE', 'RIMCLICK', 'KICK'
];
// Массив шагов, который будет сгенерирован динамически
this.timeline = [];
this.gridColumnsCount = 0;
// Хранилище состояния ячеек (по умолчанию пустое)
// Структура: { 'INSTRUMENT': [true, false, ...] }
this.matrix = {};
this.initTimeline();
}
/**
* Преобразует строковое значение селектора в верхний регистр для надежности
*/
getStepsForBeat(beatType) {
const key = beatType.toLowerCase();
return DURATION_STEPS[key] || 4; // по умолчанию 16-е (4 шага на долю)
}
/**
* Генерация таймлайна на основе выбранных долей
*/
initTimeline() {
this.timeline = [];
let totalStepIndex = 0;
// Итерируемся по тактам
for (let m = 0; m < this.numMeasures; m++) {
// В каждом такте ровно 4 четвертные доли
for (let b = 0; b < 4; b++) {
const beatType = this.beatSettings[b];
const stepsInBeat = this.getStepsForBeat(beatType);
// Расчет длительности одного шага внутри этой конкретной доли в секундах
// Длительность одной четвертной доли = 60 / BPM
const beatDurationSeconds = 60.0 / this.bpm;
const stepDurationSeconds = beatDurationSeconds / stepsInBeat;
for (let s = 0; s < stepsInBeat; s++) {
this.timeline.push({
globalIndex: totalStepIndex,
measure: m + 1,
beat: b + 1,
stepInBeat: s + 1,
duration: stepDurationSeconds,
beatType: beatType
});
totalStepIndex++;
}
}
}
this.gridColumnsCount = this.timeline.length;
this.resetMatrix();
}
/**
* Сброс/инициализация матрицы триггеров под новое количество колонок
*/
resetMatrix() {
this.instruments.forEach(inst => {
this.matrix[inst] = new Array(this.gridColumnsCount).fill(false);
});
}
/**
* Установка значения в матрицу (для загрузки сохраненных уроков преподавателя)
*/
setCell(instrument, stepIndex, value) {
if (this.matrix[instrument] && stepIndex < this.gridColumnsCount) {
this.matrix[instrument][stepIndex] = value;
}
}
/**
* Генерация HTML-разметки для панели студента
* Создает визуальную сетку, где ширина колонок или их группировка отражает доли
*/
renderStudentGrid(containerElementId) {
const container = document.getElementById(containerElementId);
if (!container) return;
container.innerHTML = '';
// Создаем таблицу сетки
const table = document.createElement('table');
table.className = 'student-drum-grid';
table.style.borderCollapse = 'collapse';
table.style.width = '100%';
table.style.backgroundColor = '#111';
table.style.color = '#fff';
table.style.fontFamily = 'sans-serif';
// 1. Рендерим шапку с долями
const headerRow = document.createElement('tr');
const cornerCell = document.createElement('th');
cornerCell.style.border = '1px solid #333';
cornerCell.style.padding = '10px';
headerRow.appendChild(cornerCell);
this.timeline.forEach((step) => {
const th = document.createElement('th');
th.innerText = `${step.beat}.${step.stepInBeat}`;
th.style.border = '1px solid #333';
th.style.fontSize = '10px';
th.style.padding = '5px';
// Визуальное разделение долей цветом (черный/оранжевый стиль)
if (step.beat % 2 === 0) {
th.style.backgroundColor = '#222';
} else {
th.style.backgroundColor = '#1a1a1a';
}
// Выделение первой субдоли каждой доли
if (step.stepInBeat === 1) {
th.style.borderLeft = '2px solid #ff6600'; // Оранжевый маркер начала доли
}
headerRow.appendChild(th);
});
table.appendChild(headerRow);
// 2. Рендерим строки инструментов
this.instruments.forEach(inst => {
const row = document.createElement('tr');
// Название инструмента
const labelCell = document.createElement('td');
labelCell.innerText = inst;
labelCell.style.border = '1px solid #333';
labelCell.style.padding = '8px';
labelCell.style.fontWeight = 'bold';
labelCell.style.fontSize = '12px';
labelCell.style.width = '100px';
row.appendChild(labelCell);
// Ячейки сетки
for (let i = 0; i < this.gridColumnsCount; i++) {
const cell = document.createElement('td');
cell.style.border = '1px solid #333';
cell.style.height = '30px';
cell.style.cursor = 'default';
const currentStep = this.timeline[i];
// Сетка по долям — визуальные границы
if (currentStep.stepInBeat === 1) {
cell.style.borderLeft = '2px solid #ff6600';
}
if (currentStep.beatType.includes('тр') || currentStep.beatType.includes('секстоли')) {
cell.style.backgroundColor = '#151c15'; // Легкий оттенок для триольных сеток
}
// Если нота активна — красим в фирменный оранжевый
if (this.matrix[inst][i]) {
cell.style.backgroundColor = '#ff6600';
cell.style.color = '#000';
cell.style.textAlign = 'center';
cell.style.fontWeight = 'bold';
cell.style.fontSize = '12px';
cell.innerText = 'R'; // Из скриншота "R" (Right)
}
row.appendChild(cell);
}
table.appendChild(row);
});
container.appendChild(table);
}
/**
* Пример воспроизведения с учетом динамических длительностей шагов.
* Вместо фиксированного интервала, таймаут или веб-аудио время сдвигается
* на величину step.duration текущего шага.
*/
play(audioContext, sampleBuffers, onStepTrigger) {
let currentStepIndex = 0;
const totalSteps = this.gridColumnsCount;
let nextStepTime = audioContext.currentTime;
const scheduler = () => {
while (nextStepTime < audioContext.currentTime + 0.1) {
const stepData = this.timeline[currentStepIndex];
// Проверяем все инструменты на этом шаге
this.instruments.forEach(inst => {
if (this.matrix[inst][currentStepIndex]) {
// Функция триггера звука (в реальном коде заменить на свой плеер)
if (sampleBuffers && sampleBuffers[inst]) {
this.playSample(audioContext, sampleBuffers[inst], nextStepTime);
}
if (onStepTrigger) {
onStepTrigger(inst, currentStepIndex, nextStepTime);
}
}
});
// КЛЮЧЕВОЙ МОМЕНТ: Сдвигаем время на индивидуальную длительность текущего шага!
nextStepTime += stepData.duration;
currentStepIndex++;
if (currentStepIndex >= totalSteps) {
currentStepIndex = 0; // Зацикливание
}
}
this.timerId = setTimeout(scheduler, 25);
};
scheduler();
}
playSample(audioContext, buffer, time) {
const source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect(audioContext.destination);
source.start(time);
}
stop() {
if (this.timerId) {
clearTimeout(this.timerId);
}
}
}
// ==========================================
// ПРИМЕР ИНИЦИАЛИЗАЦИИ НА СТУДЕНТСКОЙ ПАНЕЛИ
// ==========================================
/*
const studentGrid = new DynamicDrumGrid({
bpm: 98,
numMeasures: 1,
beatSettings: ['16-е н.', '16-е н.', 'секстоли', '8-е тр.'] // Данные забираются из конфига сохраненного урока
});
// Заполняем данными как на скрине (инструмент HAT, шаги 0 и 4 - первые шестнадцатые первой и второй доли)
studentGrid.setCell('HAT', 0, true);
studentGrid.setCell('HAT', 4, true);
// Начиная с 3-й доли (индекс 8 - начало секстолей) заполняем подряд все оранжевые ячейки
for(let i = 8; i < studentGrid.gridColumnsCount; i++) {
studentGrid.setCell('HAT', i, true);
}
// Отрисовка интерфейса студента в контейнере
studentGrid.renderStudentGrid('student-grid-container');
*/