<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Генератор Фона на Трафаретах</title>
<!-- Подключения ваших локальных сэмплов FluidR3 -->
<script src="webaudiofont/WebAudioFontPlayer.js"></script>
<script src="webaudiofont/0000_FluidR3_GM_sf2_file.js"></script>
<script src="webaudiofont/0010_FluidR3_GM_sf2_file.js"></script>
<script src="webaudiofont/0040_FluidR3_GM_sf2_file.js"></script>
<script src="webaudiofont/0050_FluidR3_GM_sf2_file.js"></script>
<script src="webaudiofont/0100_FluidR3_GM_sf2_file.js"></script>
<script src="webaudiofont/0120_FluidR3_GM_sf2_file.js"></script>
<script src="webaudiofont/0130_GeneralUserGS_sf2_file.js"></script>
<script src="webaudiofont/0240_FluidR3_GM_sf2_file.js"></script>
<script src="webaudiofont/0320_FluidR3_GM_sf2_file.js"></script>
<script src="webaudiofont/0330_FluidR3_GM_sf2_file.js"></script>
<script src="webaudiofont/0380_FluidR3_GM_sf2_file.js"></script>
<script src="webaudiofont/0390_FluidR3_GM_sf2_file.js"></script>
<script src="webaudiofont/0890_FluidR3_GM_sf2_file.js"></script>
<script src="webaudiofont/0910_FluidR3_GM_sf2_file.js"></script>
<script src="webaudiofont/0980_FluidR3_GM_sf2_file.js"></script>
<script src="webaudiofont/12856_3_FluidR3_GM_sf2_file.js"></script>
<script src="webaudiofont/12836_0_FluidR3_GM_sf2_file.js"></script>
<script src="webaudiofont/12838_0_FluidR3_GM_sf2_file.js"></script>
<script src="webaudiofont/12842_0_FluidR3_GM_sf2_file.js"></script>
<style>
:root {
--bg-color: #0a0a0c;
--panel-bg: rgba(20, 20, 28, 0.7);
--accent-purple: #a370f7;
--accent-cyan: #00adb5;
--accent-red: #ff2e63;
--text-main: #f5f5f7;
--text-muted: #8e8e93;
--border-color: rgba(255, 255, 255, 0.08);
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg-color);
color: var(--text-main);
text-align: center;
padding-top: 60px;
margin: 0;
background-image: radial-gradient(circle at top, rgba(163, 112, 247, 0.15) 0%, transparent 60%);
}
.container {
background: var(--panel-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
padding: 30px;
display: inline-block;
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.7);
border: 1px solid var(--border-color);
width: 100%;
max-width: 440px;
box-sizing: border-box;
}
.studio-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
}
.studio-header h2 { margin: 0; font-size: 22px; font-weight: 700; letter-spacing: -0.5px; }
.live-badge {
background: rgba(0, 173, 181, 0.15);
color: var(--accent-cyan);
padding: 4px 10px;
border-radius: 6px;
font-size: 11px;
font-weight: 800;
letter-spacing: 1px;
border: 1px solid rgba(0, 173, 181, 0.3);
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 0.6; }
50% { opacity: 1; }
100% { opacity: 0.6; }
}
.control-group { text-align: left; margin-bottom: 20px; }
.control-group label { display: block; font-size: 13px; color: var(--text-muted); margin-bottom: 8px; font-weight: 500; }
/* Кастомный выпадающий список и исправление стрелки */
.select-wrapper {
position: relative;
width: 100%;
}
/* НАДЕЖНАЯ ТЕКСТОВАЯ СТРЕЛКА: Символ "▼" гарантированно отобразится везде */
.select-wrapper::after {
content: "▼";
font-size: 11px;
color: var(--accent-cyan); /* Наша бирюзовая неоновая подсветка */
position: absolute;
right: 18px;
top: 50%;
transform: translateY(-50%);
pointer-events: none; /* Клик проходит сквозь стрелку прямо в список */
opacity: 0.8;
}
select {
width: 100%;
padding: 14px 40px 14px 16px; /* Оставляем правый зазор, чтобы текст не наезжал на стрелочку */
font-size: 15px;
background: rgba(255, 255, 255, 0.04);
color: var(--text-main);
border: 1px solid var(--border-color);
border-radius: 12px;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
transition: 0.2s ease;
}
select:focus {
outline: none;
border-color: var(--accent-purple);
background: rgba(255, 255, 255, 0.07);
}
select option {
background-color: #14141c !important;
color: #ffffff !important;
font-size: 15px;
padding: 12px;
}
#timer {
font-size: 48px;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-weight: 600;
margin: 25px 0;
color: var(--text-main);
letter-spacing: -1px;
}
.btn-group { display: flex; gap: 12px; margin-bottom: 30px; }
button {
flex: 1;
padding: 16px 20px;
font-size: 15px;
font-weight: 600;
border-radius: 12px;
border: none;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
#randomBtn { background: var(--accent-purple); color: #fff; box-shadow: 0 4px 15px rgba(163, 112, 247, 0.3); }
#randomBtn:hover { background: #b385f9; transform: translateY(-1px); }
#randomBtn:active { transform: translateY(1px); }
#stopBtn { background: rgba(255, 46, 99, 0.1); color: var(--accent-red); border: 1px solid rgba(255, 46, 99, 0.2); }
#stopBtn:hover { background: rgba(255, 46, 99, 0.2); }
/* Панель микшера */
.mixer-panel {
background: rgba(0, 0, 0, 0.2);
padding: 20px;
border-radius: 16px;
border: 1px solid var(--border-color);
margin-bottom: 20px;
}
.mixer-panel h4 { margin: 0 0 18px 0; font-size: 14px; text-transform: uppercase; letter-spacing: 1px; color: var(--accent-purple); text-align: center; }
.mixer-row { margin-bottom: 16px; text-align: left; }
.mixer-row:last-child { margin-bottom: 0; }
.mixer-row label { display: block; font-size: 12px; color: var(--text-muted); margin-bottom: 6px; }
/* Красивые ползунки */
input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 6px;
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--accent-cyan);
cursor: pointer;
transition: transform 0.1s ease;
box-shadow: 0 0 10px rgba(0, 173, 181, 0.5);
}
input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.2); }
#status {
font-family: ui-monospace, monospace;
font-size: 11px; /* Слегка уменьшили размер, чтобы длинный текст аккуратнее упаковывался */
color: var(--text-muted);
line-height: 1.4;
background: rgba(0, 0, 0, 0.15);
padding: 10px;
border-radius: 10px;
border: 1px solid var(--border-color);
word-break: break-word;
margin-bottom: 20px; /* Отступ снизу, чтобы оттолкнуть выбор стиля */
/* ЖЕСТКАЯ СТАБИЛИЗАЦИЯ ВЫСОТЫ ДЛЯ ВЕРХНЕГО РАСПОЛОЖЕНИЯ */
min-height: 54px; /* Фиксированная высота с запасом под многострочный текст */
display: flex;
align-items: center; /* Центрирование короткого текста по вертикали */
justify-content: center;/* Центрирование по горизонтали */
box-sizing: border-box;
}
</style>
</head>
<body>
<div class="container">
<div class="studio-header">
<h2>Генератор Фона</h2>
<div class="live-badge">LIVE GEN</div>
</div>
<div class="control-group">
<!-- Блок статуса, куда движок выводит информацию о загруженных трафаретах и текущем миксе -->
<div id="status">Статус: загрузка базы трафаретов...</div>
</div>
<!-- Счетчик времени -->
<div id="timer">02:00</div>
<!-- Выбор жанра видео-контента -->
<div class="control-group" style="margin-top: 15px;">
<label>Стиль видео-контента:</label>
<div class="select-wrapper">
<select id="videoProfileSelect">
<option value="cooking">🍔 Кулинария и Эстетика (Уют, Тепло)</option>
<option value="lifehacks">💡 Лайфхаки и DIY (Динамика, Ритм)</option>
<option value="cinematicView">🏔️ Видовые видео и Пейзажи (Атмосфера)</option>
</select>
</div>
</div>
<!-- Живой зеркальный эквалайзер -->
<div class="visualizer-container" style="margin: 20px 0; text-align: center;">
<canvas id="visualizer" width="400" height="100" style="background: #111; border-radius: 8px; border: 1px solid #333; display: block; width: 100%;"></canvas>
</div>
<!-- Панель управления -->
<div class="btn-group">
<button id="randomBtn">Случайный микс 🎲</button>
<button id="stopBtn">Стоп 🛑</button>
<button id="downloadBtn" style="background: #a370f7; color: #fff;">Скачать MP3/WebM 💾</button>
</div>
<!-- Микшер баланса громкости -->
<div class="mixer-panel" style="margin-top: 25px;">
<h4>Микшер инструментов 🎛️</h4>
<div class="mixer-row">
<label>Соло (MIDI Контур)</label>
<input type="range" id="volSolo" min="0" max="1" step="0.05" value="0.35">
</div>
<div class="mixer-row">
<label>Фон (Синт-Пады)</label>
<input type="range" id="volPad" min="0" max="1" step="0.05" value="0.20">
</div>
<div class="mixer-row">
<label>Линия баса</label>
<input type="range" id="volBass" min="0" max="1" step="0.05" value="0.25">
</div>
<div class="mixer-row">
<label>Ритм (Барабаны)</label>
<input type="range" id="volDrums" min="0" max="1" step="0.05" value="0.30">
</div>
</div>
</div>
<script>
// ==========================================
// ЧАСТЬ 1: ИНИЦИАЛИЗАЦИЯ И МАТРИЦА ЗВУКОВЫХ СУЩНОСТЕЙ
// ==========================================
const AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx = null;
let gainNode = null;
let player = new WebAudioFontPlayer();
let channelSolo = null;
let channelPad = null;
let channelBass = null;
let channelDrums = null;
let delayNode = null;
let delayFeedback = null;
let reverbNode = null;
let analyserNode = null;
let visualContext = null;
let animationId = null;
let mediaRecorder = null;
let recordedChunks = [];
let latestTrackUrl = null;
// Накопительные базы данных трафаретов из MIDI
let rhythmDatabase = [];
let melodyDatabase = [];
let activeTemplate = [];
let currentMelodyPattern = [0, 2, 4, null, 3, 5, null, 2];
let templateIndex = 0;
let melodyPatternIndex = 0;
let nextNoteTime = 0.0;
let chordChangeTime = 0.0;
let lastSoloTime = 0;
let isPlaying = false;
let timerId = null;
let countdownInterval = null;
let totalSeconds = 120;
let wavePhase = 0;
// В маппинг добавлен супер-мягкий аналоговый sub_bass вместо старого лида
const instruments = {
// Соло и Мелодические инструменты
grand_piano: window._tone_0000_FluidR3_GM_sf2_file, // 0000 Рояль
bright_piano: window._tone_0010_FluidR3_GM_sf2_file, // 0010 Яркое пианино
honky_tonk: window._tone_0040_FluidR3_GM_sf2_file, // 0040 Регтайм пианино
electric_piano: window._tone_0050_FluidR3_GM_sf2_file, // 0050 Электропиано
music_box: window._tone_0100_FluidR3_GM_sf2_file, // 0100 Музыкальная шкатулка
marimba: window._tone_0120_FluidR3_GM_sf2_file, // 0120 Маримба
bright_piano_gs:window._tone_0130_GeneralUserGS_sf2_file, // 0130 Пианино (GeneralUserGS)
nylon_guitar: window._tone_0240_FluidR3_GM_sf2_file, // 0240 Акустическая гитара
// Басовые инструменты
acoustic_bass: window._tone_0320_FluidR3_GM_sf2_file, // 0320 Акустический бас
electric_bass: window._tone_0330_FluidR3_GM_sf2_file, // 0330 Электробас
synth_bass_1: window._tone_0380_FluidR3_GM_sf2_file, // 0380 Плотный синт-бас
synth_bass_2: window._tone_0390_FluidR3_GM_sf2_file, // 0390 Суб-синт-бас
// Синтезаторные подклады (Пады)
pad_warm: window._tone_0890_FluidR3_GM_sf2_file, // 0890 Мягкий теплый пад
pad_choir: window._tone_0910_FluidR3_GM_sf2_file, // 0910 Обволакивающий хор
crystal_pluck: window._tone_0980_FluidR3_GM_sf2_file, // 0980 Хрустальные колокольчики
// НАСТОЯЩИЕ ИЗОЛИРОВАННЫЕ БАРАБАНЫ С ВАШЕГО СКРИНШОТА:
drum_kick: window._drum_36_0_FluidR3_GM_sf2_file, // 12836 Бочка (Bass Drum 1) - "пум"
drum_snare: window._drum_38_0_FluidR3_GM_sf2_file, // 12838 Снейр (Snare Drum 1) - "хлопок"
drum_hihat: window._drum_42_0_FluidR3_GM_sf2_file, // 12842 Хэт (Closed Hi-hat) - "тик"
// Шумы для кинематографичных пейзажей
sfx_kit: window._drum_56_3_FluidR3_GM_sf2_file // 12856 Шумы
};
// Текущий активный режим видео-музыки (по умолчанию Кулинария)
let currentProfileKey = 'cooking';
// =================================================================
// 🎛️ ПРОФЕССИОНАЛЬНО ВЫВЕРЕННЫЕ ЗВУКОВЫЕ ПРЕСЕТЫ (КАЧЕСТВЕННЫЙ ЗВУК)
// =================================================================
const VIDEO_MUSIC_PROFILES = {
// 🍔 КУЛИНАРИЯ: Уютный, мягкий, ресторанный лаунж. Абсолютно чистый рояль без гула.
cooking: {
name: "Кулинария (Уютный Лаунж)",
soloInstrument: 'grand_piano', // Акустический благородный рояль
altInstrument: 'bright_piano_gs', // Подстраховка на пианино
bassInstrument: 'acoustic_bass', // Глубокий, мягкий деревянный контрабас
padInstrument: 'pad_choir', // Очень воздушная, едва заметная подложка
// Мажорная пентатоника До (C Major Pentatonic) на 3 октавы. Фальшь исключена физически.
scale: [ 48, 50, 52, 55, 57, 60, 62, 64, 67, 69, 72, 74, 76, 79, 81 ],
noteChance: 0.40, // Размеренная, неторопливая игра
structuralDelay: 0.50, // Спокойный, размеренный темп
noteDuration: 1.60, // Оптимальная длительность нот без «каши»
// Дорогой Lo-Fi / Lounge ритм. Бочка качает, снейр мягкий, хэт делает аккуратный «тик»
drumPattern: {
kick: [ 1, 0, 0, 0, 0, 0, 1, 0 ], // Спокойный, уверенный пульс
snare: [ 0, 0, 0, 0, 1, 0, 0, 0 ], // Четкий хлопок на 5-ю долю
hihat: [ 1, 0, 1, 0, 1, 0, 1, 0 ] // Легкий, прореженный «шаг» хэта
}
},
// 💡 ЛАЙФХАКИ: Современный, продуктивный, прыгучий и «умный» инди-поп / электроника.
lifehacks: {
name: "Лайфхаки (Продуктивность)",
soloInstrument: 'marimba', // Сочная, быстрая деревянная маримба
altInstrument: 'music_box', // Озорные колокольчики музыкальной шкатулки
bassInstrument: 'acoustic_bass', // ИСПРАВЛЕНО: Гарантированно загруженный контрабас (трансформируется в синт-бас)
padInstrument: 'pad_choir', // ИСПРАВЛЕНО: Гарантированно загруженный хор (трансформируется в синт-пэд)
// Мажорная пентатоника Соль (G Major Pentatonic). Позитивный, яркий и технологичный окрас.
scale: [ 43, 45, 47, 50, 52, 55, 57, 59, 62, 64, 67, 69, 71, 74, 76 ],
noteChance: 0.65, // Плотная, но структурированная и внятная игра
structuralDelay: 0.25, // Динамичный, собранный шаг
noteDuration: 0.22, // Короткие, «стреляющие» и прыгучие ноты маримбы
// ИСПРАВЛЕНО: Профессиональный Tech-Lounge грув. Идеальное распределение долей без наслоений.
drumPattern: {
kick: [ 1, 0, 0, 0, 1, 0, 0, 0 ], // Четкий прямой пульс на сильные доли
snare: [ 0, 0, 0, 0, 1, 0, 0, 0 ], // Мягкий ответный хлопок строго на 5-й шаг
hihat: [ 0, 0, 1, 0, 0, 0, 1, 0 ] // Идеальный «off-beat» хэт, создающий динамику между бочками
}
},
// 🏔️ ПЕЙЗАЖИ: Глубокий оркестровый эмбиент, кинематографичный размах и полет.
cinematicView: {
name: "Пейзажи (Эстетика & Кинематограф)",
soloInstrument: 'grand_piano', // Редкие, глубокие капли рояля из темноты
altInstrument: 'pad_choir',
bassInstrument: 'synth_bass', // Обволакивающий, заполняющий комнату суб-бас
padInstrument: 'pad_choir', // Мощные, живые волны оркестрового хора
// Натуральный минор Ля (A Natural Minor) — эпический, глубокий, слегка меланхоличный окрас
scale: [ 45, 47, 48, 50, 52, 53, 55, 57, 59, 60, 62, 64, 65, 67, 69 ],
noteChance: 0.25, // Очень редкие, весомые ноты-акценты
structuralDelay: 0.75, // Длинное, парящее время ожидания
noteDuration: 4.50, // Максимально длинное, растворяющееся эхо
// Глубокий Cinematic Chill-Out ритм. Барабаны дышат, бочка мягкая, хэт выключен.
drumPattern: {
kick: [ 1, 0, 0, 0, 0, 0, 0, 0 ], // Глубокий, редкий удар в начале такта
snare: [ 0, 0, 0, 0, 1, 0, 0, 0 ], // Один мягкий, размытый хлопок в центре
hihat: [ 0, 0, 0, 0, 0, 0, 0, 0 ] // Полный минимализм — хэт выключен для атмосферы
},
// Процедурные эмбиент-волны, генерирующие объем пространства
sfx: {
lowWave: { chance: 0.35, note: 33, duration: 5.0, velocity: 0.25 }, // Суб-основа такта
midWave: { chance: 0.25, note: 45, duration: 4.0, velocity: 0.20 }, // Среднее заполнение
highWave: { chance: 0.15, note: 57, duration: 3.5, velocity: 0.15 } // Верхний мерцающий воздух
}
}
};
// Глобальные ссылки на ладовую гармонию с базовыми значениями
const scale = [];
const chordRoots = [];
let currentChord = [];
// Из пула сольных инструментов убраны все гитары, банджо, саксофоны и brass
const soloInstrumentsPool = [ 'synth_piano', 'vocal_lead', 'vocal_chiff', 'lid3', 'lid5', 'lid6' ];
const padInstrumentsPool = [ 'pad_warm', 'pad_poly', 'pad_choir', 'pad_sweep', 'pad_atmos' ];
// Обновленный пул басов
const bassInstrumentsPool = [ 'synth_bass', 'electric_bass', 'sub_bass' ];
let currentSoloInst = 'synth_piano';
let currentPadInst = 'pad_warm';
let currentBassInst = 'synth_bass';
let currentSpeedModifier = 1.0;
// Аварийные пулы на случай отсутствия JSON
const fallbackRhythms = [ [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5], [0, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75] ];
const fallbackMelodies = [ [0, 2, null, 4, 3, null, 5, 2], [0, 1, 2, 3, 2, 1, 0, null] ];
// ЧАСТЬ 2: ЗАГРУЗКА ДАННЫХ И КВАНТОВАНИЕ ВРЕМЕНИ
// ==========================================
// Запуск сквозного пайплайна загрузки шаблонов из MIDI
fetch('rhythm_templates.json')
.then(function(response) {
if (!response.ok) throw new Error('Файл базы данных не найден');
return response.json();
})
.then(function(data) {
rhythmDatabase = (data.rhythms && data.rhythms.length > 0) ? data.rhythms : fallbackRhythms;
melodyDatabase = (data.melodies && data.melodies.length > 0) ? data.melodies : fallbackMelodies;
// 1. ДИАГНОСТИКА БАРАБАНОВ ПРИ СТАРТЕ
let statusKick = instruments.drum_kick ? "✅ ГОТОВ" : "❌ НЕ ЗАГРУЖЕН";
let statusSnare = instruments.drum_snare ? "✅ ГОТОВ" : "❌ НЕ ЗАГРУЖЕН";
let statusHat = instruments.drum_hihat ? "✅ ГОТОВ" : "❌ НЕ ЗАГРУЖЕН";
// 2. ВЫВОД ИСПРАВЛЕННОГО СТАТУСА НА ЭКРАН
const statusEl = document.getElementById('status');
if (statusEl) {
statusEl.style.color = '#86c232';
statusEl.innerHTML = `Статус: Генеративный движок готов! Трафареты из MIDI: ритмы (${rhythmDatabase.length}), мелодии (${melodyDatabase.length})<br/>` +
`🔍 Сэмплы: Бочка: ${statusKick} | Снейр: ${statusSnare} | Хэт: ${statusHat}`;
}
})
.catch(function(err) {
console.error('Откат на процедурные матрицы:', err);
rhythmDatabase = fallbackRhythms;
melodyDatabase = fallbackMelodies;
document.getElementById('status').style.color = '#ffa500';
document.getElementById('status').innerText = 'Предупреждение: Корректный JSON не найден. Задействована базовая резервная сетка матриц.';
});
// Генератор случайного трезвучия на основе глобальной гармонии
function generateNextChord() {
// МАССИВ СТУПЕНЕЙ: Сдвиги в полутонах для смены аккордов (0=Тоника, -3=Параллель, 5=Субдоминанта, 7=Доминанта)
let steps = [ 0, -3, 2, 4, 5, 7 ];
// Берем текущую базовую ноту из нашей гаммы с защитой от пустоты
let currentRoot = scale[ Math. floor( Math. random() * scale. length ) ] || 60;
// Выбираем случайный гармонический шаг из массива
let shift = steps[ Math. floor( Math. random() * steps. length ) ];
let nextRoot = currentRoot + shift;
// Возвращаем полноценное красивое мажорное трезвучие
return [ nextRoot, nextRoot + 4, nextRoot + 7 ];
}
// Главный планировщик событий Web Audio API с квантованием ломаных ритмов
function scheduler() {
if ( !isPlaying || !activeTemplate || activeTemplate.length === 0 ) return;
// Защита: подтягиваем время к текущему, если оно отстало
if ( nextNoteTime < audioCtx.currentTime ) {
nextNoteTime = audioCtx.currentTime + 0.01;
}
const profile = VIDEO_MUSIC_PROFILES[ currentProfileKey ];
while ( nextNoteTime < audioCtx.currentTime + 0.1 ) {
// ==========================================
// ИСПРАВЛЕНИЕ: СИНХРОНИЗАЦИЯ АККОРДОВ И ПЭДОВ
// ==========================================
// Для Лайфхаков меняем аккорды чуть чаще (каждые 8 шагов), для остальных — каждые 16 шагов,
// чтобы длинные пады успевали подстраиваться под сумасшедший темп электроники.
let chordStepInterval = ( currentProfileKey === 'lifehacks' ) ? 8 : 16;
if ( templateIndex % chordStepInterval === 0 ) {
// 1. Обновляем гармонию
currentChord = generateNextChord();
// 2. ИСПРАВЛЕНИЕ: Перезаписываем ИМЕННО ГЛОБАЛЬНУЮ переменную currentPadInst, которую ищет плеер!
currentPadInst = profile ? profile.padInstrument : null;
if ( currentPadInst && instruments[ currentPadInst ] && channelPad && Array.isArray( currentChord ) ) {
// ИСПРАВЛЕНИЕ ОГИБАЮЩЕЙ ПЭДА: Физически перенастраиваем сэмпл хора под плотный синт-пэд
if ( instruments[ currentPadInst ].zones ) {
instruments[ currentPadInst ].zones.forEach(function(zone) {
zone.ahdsr = true;
if ( currentProfileKey === 'lifehacks' ) {
// Трансформируем хор в футуристический электронный "Gate Pad"
zone.attack = 0.01; // Мгновенное появление (убираем кулинарный наплыв)
zone.decay = 0.40;
zone.sustain = 0.85; // Очень плотная неоновая подложка
zone.release = 0.15; // Резко затухает, чтобы не гудеть поверх ритма
} else {
// Возвращаем воздушный бесконечный шлейф для Кулинарии и Видов
zone.attack = 0.08;
zone.decay = 2.50;
zone.sustain = 0.50;
zone.release = 1.50;
}
});
}
if ( channelPad.gain ) {
let userVol = parseFloat( document.getElementById('volPad').value || 0.5 ) * 0.85;
if ( currentProfileKey === 'cooking' ) userVol = userVol * 0.30;
else if ( currentProfileKey === 'lifehacks' ) userVol = userVol * 0.65; // Подняли громкость, чтобы пробить маримбу
channelPad.gain.cancelScheduledValues( nextNoteTime );
channelPad.gain.setValueAtTime( userVol, nextNoteTime );
}
currentChord.forEach( function( note ) {
if ( Number.isFinite( note ) && note > 0 ) {
// Для кухни поднимаем наверх, для лайфхаков — оставляем в плотном среднем регистре (note)
let finalPadNote = ( currentProfileKey === 'cooking' ) ? note + 12 : note;
let padDuration = ( currentProfileKey === 'cooking' ) ? 3.2 : (currentProfileKey === 'lifehacks' ? 2.5 : 6.0);
let padVelocity = ( currentProfileKey === 'cooking' ) ? 0.20 : (currentProfileKey === 'lifehacks' ? 0.38 : 0.50);
player.queueWaveTable( audioCtx, channelPad, instruments[ currentPadInst ], nextNoteTime, finalPadNote, padDuration, padVelocity );
}
});
}
}
// Вызов процедурного сольного шага и баса
playSoloNote( nextNoteTime );
// МАТЕМАТИЧЕСКИЙ ТРИГГЕР УДАРНЫХ
if ( channelDrums ) {
playDrumsNode( nextNoteTime, templateIndex );
// ЭФФЕКТ SIDECHAIN: Компрессия баса в момент удара бочки
if ( channelBass && channelBass.gain && ( templateIndex % 4 === 0 ) ) {
let userBassVol = parseFloat( document.getElementById('volBass').value || 0.5 );
let bassMult = ( currentProfileKey === 'cooking' || currentProfileKey === 'cinematicView' ) ? 0.85 : 0.70;
let currentVol = userBassVol * bassMult;
let targetGain = ( currentProfileKey === 'cooking' || currentProfileKey === 'cinematicView' ) ? 0.80 : 0.20;
channelBass.gain.cancelScheduledValues( nextNoteTime );
channelBass.gain.setValueAtTime( currentVol * targetGain, nextNoteTime );
channelBass.gain.linearRampToValueAtTime( currentVol, nextNoteTime + 0.15 );
}
}
// --- БЛОК ЗАЩИТЫ И КВАНТОВАНИЯ РИТМА ---
let currentDelta = activeTemplate[ templateIndex ];
templateIndex++;
if ( templateIndex >= activeTemplate.length ) {
templateIndex = 0;
}
let nextDelta = activeTemplate[ templateIndex ];
let diff = nextDelta - currentDelta;
if ( diff > 0 && diff < 0.08 ) diff = 0.125;
if ( Number.isFinite( diff ) && diff > 0 ) {
const grid = [ 0.125, 0.25, 0.375, 0.5, 0.75, 1.0, 1.5, 2.0 ];
diff = grid.reduce( function( prev, curr ) {
return ( Math.abs( curr - diff ) < Math.abs( prev - diff ) ? curr : prev );
});
}
if ( !Number.isFinite( diff ) || diff <= 0 || diff > 3.0 ) diff = 0.5;
nextNoteTime += ( diff * currentSpeedModifier );
}
timerId = setTimeout( scheduler, 25 );
}
// ==========================================
// ЧАСТЬ 3: ИДЕАЛЬНЫЙ СИНТ-ЭМБИЕНТ СИНТЕЗ (ОЧИЩЕН ОТ НЕКАЧЕСТВЕННЫХ ИНСТРУМЕНТОВ)
// ==========================================
function playSoloNote( time ) {
if ( !isPlaying || !channelSolo ) return;
const profile = VIDEO_MUSIC_PROFILES[ currentProfileKey ];
const actualSoloInst = profile.soloInstrument;
const actualBassInst = profile.bassInstrument;
// Защита: если трафарет пуст, играть нечего
if ( !Array.isArray(activeTemplate) || activeTemplate.length === 0 ) return;
// Глобальный шаг по сетке activeTemplate
let templateStep = templateIndex % activeTemplate.length;
let rawValue = activeTemplate[ templateStep ];
// Математический перевод дельты трафарета в чистые музыкальные полутона
let mOffset = Number.isFinite(rawValue) ? Math.floor((rawValue * 12) % 24) : 0;
// Тоника аккорда (берём первую опорную ноту текущего аккорда)
let baseNote = Array.isArray( currentChord ) ? currentChord[0] : currentChord;
if ( !Number.isFinite( baseNote ) ) baseNote = 60;
// Базовая длительность из пресетов профиля
let finalNoteDuration = profile.noteDuration || 1.50;
// ==========================================
// БЛОК 1: ГЕНЕРАЦИЯ СОЛО ПАРТИИ (ПИАНИНО / МАРИМБА)
// ==========================================
if ( instruments[ actualSoloInst ] ) {
let noteValue = baseNote + mOffset;
let closestNote = profile.scale.reduce( function( prev, curr ) {
return ( Math.abs( curr - noteValue ) < Math.abs( prev - noteValue ) ? curr : prev );
});
if ( channelSolo.pan ) {
if ( currentProfileKey === 'cinematicView' ) {
channelSolo.pan.setValueAtTime( 0.0, time );
} else {
channelSolo.pan.setValueAtTime( Math.random() * 0.4 - 0.2, time );
}
}
let userSoloVol = parseFloat( document.getElementById('volSolo').value || 0.5 );
if ( channelSolo.gain ) {
let dynamicGainModifier = 0.55;
if ( profile.structuralDelay && profile.structuralDelay <= 0.25 ) {
dynamicGainModifier = 0.40;
}
channelSolo.gain.cancelScheduledValues( time );
channelSolo.gain.setValueAtTime( userSoloVol * dynamicGainModifier, time );
}
let targetInstrumentObj = instruments[ actualSoloInst ];
if ( targetInstrumentObj && targetInstrumentObj.zones ) {
if ( currentProfileKey === 'cooking' ) {
targetInstrumentObj.zones.forEach(function(zone) {
zone.ahdsr = true;
zone.attack = 0.005;
zone.decay = 0.80;
zone.sustain = 0.40;
zone.release = 0.90;
});
} else if ( currentProfileKey === 'lifehacks' ) {
targetInstrumentObj.zones.forEach(function(zone) {
zone.ahdsr = true;
zone.attack = 0.002;
zone.decay = 0.06;
zone.sustain = 0.0;
zone.release = 0.04;
});
} else if ( currentProfileKey === 'cinematicView' ) {
targetInstrumentObj.zones.forEach(function(zone) {
zone.ahdsr = true;
zone.attack = 0.05;
zone.decay = 3.00;
zone.sustain = 0.60;
zone.release = 2.50;
});
}
}
// Алгоритм "живого оркестра" для соло
let isStrongBeat = ( templateStep % 4 === 0 );
let baseVelocity = isStrongBeat ? 0.65 : 0.42;
let humanizedVelocity = baseVelocity + ( Math.random() * 0.12 - 0.06 );
let maxTimeShift = ( currentProfileKey === 'lifehacks' ) ? 0.002 : 0.006;
let humanizedTime = time + ( Math.random() * maxTimeShift * 2 - maxTimeShift );
if ( typeof lastPlayedNote !== 'undefined' && lastPlayedNote === closestNote && (profile.structuralDelay || 0.5) <= 0.25 ) {
finalNoteDuration = finalNoteDuration * 0.60;
}
window.lastPlayedNote = closestNote;
if ( Math.random() < (profile.noteChance || 0.50) ) {
player.queueWaveTable( audioCtx, channelSolo, targetInstrumentObj, humanizedTime, closestNote, finalNoteDuration, humanizedVelocity );
}
}
// ==========================================
// БЛОК 2: ГЕНЕРАЦИЯ ГЛУБОКОГО БАСА (ИСПРАВЛЕНО И СИНХРОНИЗИРОВАНО)
// ==========================================
// Бас играет ТОЛЬКО на кухне и в лайфхаках. В кинематографичных видах он полностью отключен.
if ( actualBassInst && instruments[ actualBassInst ] && channelBass && currentProfileKey !== 'cinematicView' ) {
// В Кулинарии (живой контрабас) оставляем сочный низ (-24), в Лайфхаках поднимаем выше (-12), чтобы синт-бас читался
let octaveShift = ( currentProfileKey === 'cooking' ) ? -24 : -12;
let bassNoteValue = baseNote + octaveShift;
let bassOffset = Math.floor(mOffset / 2);
let finalBassNote = bassNoteValue + bassOffset;
// ИСПРАВЛЕНИЕ: Безопасное вычисление дефолтной ноты и передача её в качестве аккумулятора reduce
let fallbackScaleNote = (profile.scale && profile.scale.length > 0) ? profile.scale[0] : 60;
let defaultLowNote = fallbackScaleNote + octaveShift;
let closestBassNote = profile.scale.reduce( function( prev, curr ) {
let lowCurr = curr + octaveShift;
return ( Math.abs( lowCurr - finalBassNote ) < Math.abs( prev - finalBassNote ) ? lowCurr : prev );
}, defaultLowNote); // ИСПРАВЛЕНО: Теперь сюда безопасно передается число, а не сломанный массив
// Настройка громкости басового канала с учетом ползунков микшера
let userBassVol = parseFloat( document.getElementById('volBass').value || 0.5 );
if ( channelBass.gain ) {
// Для лайфхаков делаем чуть громче (0.75), чтобы пробить микс, для кухни — мягче (0.45)
let bassVolumeModifier = ( currentProfileKey === 'cooking' ) ? 0.45 : 0.75;
channelBass.gain.setValueAtTime( userBassVol * bassVolumeModifier, time );
}
// Настройка физики звука под сэмплы
let targetBassObj = instruments[ actualBassInst ];
if ( targetBassObj && targetBassObj.zones ) {
targetBassObj.zones.forEach(function(zone) {
zone.ahdsr = true;
if ( currentProfileKey === 'lifehacks' ) {
// ТРАНСФОРМАЦИЯ: Сжимаем живой контрабас в плотный, прыгучий и качающий электронный синт-бас
zone.attack = 0.001; // Мгновенный бьющий щелчок
zone.decay = 0.12; // Экстремально короткий спад (вместо прежних 0.40) для Tech-эффекта
zone.sustain = 0.45; // Собранное тело звука
zone.release = 0.08; // Моментально тухнет, освобождая место бочке
} else {
// Классические мягкие настройки для ресторанного контрабаса в Кулинарии
zone.attack = 0.03;
zone.decay = 1.50;
zone.sustain = 0.60;
zone.release = 0.50;
}
});
}
// В лайфхаках бас фигачит на каждый шаг (templateStep % 1 === 0) сплошной стеной. На кухне — на каждый второй шаг.
let bassTriggerRate = ( currentProfileKey === 'lifehacks' ) ? 1 : 2;
if ( templateStep % bassTriggerRate === 0 ) {
let bassDuration = finalNoteDuration * 1.5;
player.queueWaveTable( audioCtx, channelBass, targetBassObj, time, closestBassNote, bassDuration, 0.70 );
}
}
}
function playDrumsNode( time, stepIndex ) {
if ( !isPlaying || !channelDrums ) return;
const profile = VIDEO_MUSIC_PROFILES[ currentProfileKey ];
// Регулируем общую громкость ударных из ползунка
if ( channelDrums.gain ) {
let userVol = parseFloat( document.getElementById('volDrums').value || 0.5 );
channelDrums.gain.setValueAtTime( userVol, time );
}
// ИСПРАВЛЕНИЕ ДЛЯ ВИДОВЫХ: Дополнительно накладываем фоновый хор (атмосферу) БЕЗ остановки основного барабанного ритма!
if ( currentProfileKey === 'cinematicView' ) {
let atmosInst = instruments.pad_choir;
if ( atmosInst && profile.sfx ) {
const sfx = profile.sfx;
if ( Math.random() < sfx.lowWave.chance ) player.queueWaveTable( audioCtx, channelDrums, atmosInst, time, sfx.lowWave.note, sfx.lowWave.duration, sfx.lowWave.velocity );
if ( Math.random() < sfx.midWave.chance ) player.queueWaveTable( audioCtx, channelDrums, atmosInst, time, sfx.midWave.note, sfx.midWave.duration, sfx.midWave.velocity );
if ( Math.random() < sfx.highWave.chance ) player.queueWaveTable( audioCtx, channelDrums, atmosInst, time, sfx.highWave.note, sfx.highWave.duration, sfx.highWave.velocity );
}
// УБРАН RETURN! Теперь видовые видео тоже спускаются ниже и играют ритм-секцию, если она задана
}
// Воспроизведение ударных по паттерну
if ( profile.drumPattern ) {
const pattern = profile.drumPattern;
let step = Math.abs(Math.floor(stepIndex)) % 8;
let baseVelocity = ( currentProfileKey === 'lifehacks' ) ? 0.70 : 0.55;
// 1. Бочка (Kick)
if ( pattern.kick && pattern.kick[ step ] === 1 && instruments.drum_kick ) {
player.queueWaveTable( audioCtx, channelDrums, instruments.drum_kick, time, 36, 0.20, baseVelocity * 0.95 );
}
// 2. Малый барабан (Snare)
if ( pattern.snare && pattern.snare[ step ] === 1 && instruments.drum_snare ) {
player.queueWaveTable( audioCtx, channelDrums, instruments.drum_snare, time, 38, 0.15, baseVelocity * 0.85 );
}
// 3. Хэт (Closed Hi-hat)
if ( pattern.hihat && pattern.hihat[ step ] > 0 && instruments.drum_hihat ) {
let hihatMod = (pattern.hihat[ step ] === 2) ? 1.0 : 0.60;
player.queueWaveTable( audioCtx, channelDrums, instruments.drum_hihat, time, 42, 0.05, baseVelocity * 0.60 * hihatMod );
}
}
}
// ==========================================
// ЧАСТЬ 4: СТОХАСТИЧЕСКАЯ ГЕНЕРАЦИЯ СРЕДЫ И СТУДИЙНЫЙ МАСТЕР
// ==========================================
document.getElementById('randomBtn').addEventListener('click', async function() {
if (rhythmDatabase.length === 0 || melodyDatabase.length === 0) return;
stopMusic();
// ПОЛУЧАЕМ НАСТРОЙКИ ИЗ АКТИВНОГО ПРОФИЛЯ ВИДЕО
const profile = VIDEO_MUSIC_PROFILES[ currentProfileKey ];
// Базовое назначение инструментов и падов
currentSoloInst = profile. soloInstrument;
currentBassInst = profile. bassInstrument;
currentPadInst = profile. padInstrument ? profile. padInstrument : null;
// ОБЪЕДИНЕННАЯ НАСТРОЙКА ИНСТРУМЕНТОВ И СКОРОСТИ ПО ЖАНРАМ:
if ( currentProfileKey === 'lifehacks' ) {
// 1. Устанавливаем быстрый, бодрый темп для DIY
currentSpeedModifier = 0.70;
// 2. Выбираем ОДИН инструмент на весь трек (50% пианино / 50% маримба)
if ( Math. random() < 0.50 ) {
currentSoloInst = 'bright_piano_gs'; // Включаем новое пианино 0130 GS
} else {
currentSoloInst = 'marimba'; // Включаем маримбу 0120
}
}
else if ( currentProfileKey === 'cinematicView' ) {
// Медленный, летящий темп для пейзажей
currentSpeedModifier = 1.35;
}
else {
// Комфортный средний темп для кулинарии (cooking)
currentSpeedModifier = 1.00;
}
// Заполняем глобальную гамму шкалы из настроек нашего профиля
scale. length = 0;
profile. scale. forEach( function( n ) {
scale. push( n );
});
// ==========================================
// 1. ИСПРАВЛЕННАЯ НАСТРОЙКА ТОНАЛЬНОСТИ И АККОРДОВ (БЕЗ БРИДЖЕЙ В КУЛИНАРИИ)
// ==========================================
let baseChordNote = 60; // По умолчанию чистый До-мажор
if (currentProfileKey === 'cooking') {
// Для кулинарии намертво фиксируем До-мажор, чтобы музыка была уютной и предсказуемой
baseChordNote = 60;
} else {
// Для остальных режимов оставляем контролируемую рандомизацию тоники
let rootNotes = [ 60, 62, 65, 67, 69 ];
baseChordNote = rootNotes[ Math.floor( Math.random() * rootNotes.length ) ];
}
// Безопасное заполнение опорных нот аккордов с защитой от неопределенной шкалы
chordRoots.length = 0;
if ( Array.isArray( scale ) && scale.length >= 6 ) {
chordRoots.push( scale[ 0 ], scale[ 2 ], scale[ 4 ], scale[ 5 ] );
} else {
chordRoots.push( baseChordNote, baseChordNote + 4, baseChordNote + 7, baseChordNote + 9 );
}
// Запускаем первую генерацию сетки аккордов (для кулинарии это всегда будет красивое трезвучие До-мажор)
currentChord = [ baseChordNote, baseChordNote + 4, baseChordNote + 7 ];
chordChangeTime = audioCtx ? audioCtx.currentTime + 4.0 : 4.0;
// 2. УМНЫЙ ВЫБОР И СИНХРОНИЗАЦИЯ ВАШИХ MIDI-ТРАФАРЕТОВ (Ритмы: 124, Мелодии: 35)
randomRhythmIdx = Math. floor( Math. random() * rhythmDatabase. length );
randomMelodyIdx = Math. floor( Math. random() * melodyDatabase. length );
// Подтягиваем ваши реальные MIDI-шаблоны
activeTemplate = rhythmDatabase[ randomRhythmIdx ];
let rawMelodyPattern = melodyDatabase[ randomMelodyIdx ];
// ИСПРАВЛЕНИЕ ФАЗОВОГО СДВИГА: Выравниваем длину мелодии под длину текущего ритма
currentMelodyPattern = [];
if ( Array. isArray( activeTemplate ) && activeTemplate. length > 0 ) {
for ( let i = 0; i < activeTemplate. length; i++ ) {
// Безопасно берем ноту из MIDI-мелодии, зацикливая её по кругу при необходимости
let noteFromMIDI = Array. isArray( rawMelodyPattern ) && rawMelodyPattern. length > 0
? rawMelodyPattern[ i % rawMelodyPattern. length ]
: 0;
// Музыкальное «дыхание»: с вероятностью 15% аккуратно сдвигаем ноту на ступень,
// чтобы один и тот же MIDI-трафарет каждый раз выдавал новые полутона
if ( Math. random() < 0.15 ) {
noteFromMIDI += ( Math. random() < 0.50 ? 2 : -2 );
}
currentMelodyPattern. push( noteFromMIDI );
}
} else {
// Аварийный фоллбэк на случай пустого массива в базе
activeTemplate = [ 0.25, 0.25, 0.25, 0.25 ];
currentMelodyPattern = [ 0, 2, 4, 7 ];
}
templateIndex = 0;
melodyPatternIndex = 0;
// 3. АКТИВАЦИЯ АУДИО-КОНТЕКСТА И МАСТЕР-ШИНЫ
if ( ! audioCtx ) audioCtx = new AudioContext();
if ( audioCtx. state === 'suspended' ) await audioCtx. resume();
// Входной Gain: Снижен с 0.75 до 0.45 для чистоты микса без каши
let gainNode = audioCtx. createGain();
gainNode. gain. setValueAtTime( 0.45, audioCtx. currentTime );
// 2. Мастер-компрессор: Время восстановления (release) уменьшено с 0.25 до 0.10.
// Теперь компрессор успевает "отпускать" звук между быстрыми нотами (каждые 0.25 сек).
let masterCompressor = audioCtx.createDynamicsCompressor();
masterCompressor.threshold.setValueAtTime(-14.0, audioCtx.currentTime);
masterCompressor.knee.setValueAtTime(8, audioCtx.currentTime);
masterCompressor.ratio.setValueAtTime(4, audioCtx.currentTime);
masterCompressor.attack.setValueAtTime(0.015, audioCtx.currentTime);
masterCompressor.release.setValueAtTime(0.10, audioCtx.currentTime); // Было 0.25
// 3. Лимитер (Brickwall): Время атаки чуть сглажено, чтобы убрать цифровой треск (клиппинг).
// Релиз ускорен до 0.03, чтобы лимитер не "плющил" следующие за пиками ноты.
let brickwallLimiter = audioCtx.createDynamicsCompressor();
brickwallLimiter.threshold.setValueAtTime(-0.5, audioCtx.currentTime);
brickwallLimiter.knee.setValueAtTime(0, audioCtx.currentTime);
brickwallLimiter.ratio.setValueAtTime(20, audioCtx.currentTime);
brickwallLimiter.attack.setValueAtTime(0.003, audioCtx.currentTime); // Было 0.001
brickwallLimiter.release.setValueAtTime(0.03, audioCtx.currentTime); // Было 0.05
analyserNode = audioCtx.createAnalyser();
analyserNode.fftSize = 64;
let dest = audioCtx.createMediaStreamDestination();
recordedChunks = [];
mediaRecorder = new MediaRecorder(dest.stream, { mimeType: 'audio/webm' });
mediaRecorder.ondataavailable = function(e) {
if (e.data && e.data.size > 0) {
recordedChunks.push(e.data);
}
};
gainNode.connect(masterCompressor);
masterCompressor.connect(brickwallLimiter);
brickwallLimiter.connect(analyserNode);
analyserNode.connect(audioCtx.destination);
analyserNode.connect(dest);
channelSolo = audioCtx. createGain();
channelPad = audioCtx. createGain();
channelBass = audioCtx. createGain();
channelDrums = audioCtx. createGain();
// СЧИТЫВАЕМ ЗНАЧЕНИЯ ПОЛЗУНКОВ ИЗ ИНТЕРФЕЙСА ДЛЯ КАЖДОГО КАНАЛА
let volSoloVal = parseFloat( document. getElementById('volSolo'). value );
let volPadVal = parseFloat( document. getElementById('volPad'). value );
let volBassVal = parseFloat( document. getElementById('volBass'). value );
let volDrumsVal = parseFloat( document. getElementById('volDrums'). value );
// ИСПРАВЛЕНИЕ: Выставляем чистую рабочую громкость без чрезмерного занижения
channelSolo. gain. setValueAtTime( volSoloVal * 0.75, audioCtx. currentTime );
channelPad. gain. setValueAtTime( volPadVal * 0.85, audioCtx. currentTime );
channelBass. gain. setValueAtTime( volBassVal * 0.55, audioCtx. currentTime );
// Для Кулинарии и Лайфхаков даем барабанам играть в полную силу ползунка
if ( currentProfileKey === 'cinematicView' ) {
channelDrums. gain. setValueAtTime( volDrumsVal * 0.40, audioCtx. currentTime ); // Атмосферный фон тише
} else {
channelDrums. gain. setValueAtTime( volDrumsVal * 0.85, audioCtx. currentTime ); // Чистые барабаны громче!
}
// Привязка динамического изменения ползунков на лету во время проигрывания
// Привязка динамического изменения ползунков на лету во время проигрывания
document. getElementById('volSolo'). oninput = function( e ) { if ( channelSolo ) channelSolo. gain. setValueAtTime( parseFloat( e. target. value ) * 0.75, audioCtx. currentTime ); };
document. getElementById('volPad'). oninput = function( e ) { if ( channelPad ) channelPad. gain. setValueAtTime( parseFloat( e. target. value ) * 0.85, audioCtx. currentTime ); };
document. getElementById('volBass'). oninput = function( e ) { if ( channelBass ) channelBass. gain. setValueAtTime( parseFloat( e. target. value ) * 0.55, audioCtx. currentTime ); };
document. getElementById('volDrums'). oninput = function( e ) {
if ( channelDrums ) {
let mult = ( currentProfileKey === 'cinematicView' ) ? 0.40 : 0.85;
channelDrums. gain. setValueAtTime( parseFloat( e. target. value ) * mult, audioCtx. currentTime );
}
};
// НАСТРОЙКА ЭФФЕКТА ЗАДЕРЖКИ (DELAY)
delayNode = audioCtx. createDelay();
delayFeedback = audioCtx. createGain();
delayNode. delayTime. setValueAtTime( 0.4, audioCtx. currentTime );
delayFeedback. gain. setValueAtTime( 0.35, audioCtx. currentTime );
// Зацикливаем дилей сам на себя через обратную связь (feedback)
delayNode. connect( delayFeedback );
delayFeedback. connect( delayNode );
// НАСТРОЙКА РЕВЕРБЕРАЦИИ ПОД ТИП КОНТЕНТА
reverbNode = audioCtx. createConvolver();
if ( currentProfileKey === 'cinematicView' ) {
reverbNode. buffer = createReverbImpulse( audioCtx, 4.2, 2.5 ); // Огромный глубокий шлейф
} else if ( currentProfileKey === 'lifehacks' ) {
reverbNode. buffer = createReverbImpulse( audioCtx, 0.8, 1.2 ); // Сухой собранный отклик
} else {
reverbNode. buffer = createReverbImpulse( audioCtx, 1.6, 1.5 ); // Мягкое комнатное эхо
}
// КОММУТАЦИЯ: Подключаем все каналы к главному входному Gain-узлу (gainNode)
channelBass. connect( gainNode );
channelDrums. connect( gainNode );
channelSolo. connect( gainNode );
channelPad. connect( gainNode );
// ПОД КЛЮЧЕНИЕ ПРОСТРАНСТВЕННЫХ ЭФФЕКТОВ
channelSolo. connect( delayNode ); // Соло-линия уходит в дилей
channelPad. connect( reverbNode ); // Пад уходит в реверберацию
delayNode. connect( gainNode ); // Выход дилея подмешивается в мастер
reverbNode. connect( gainNode ); // Выход реверберации подмешивается в мастер
isPlaying = true;
mediaRecorder. start();
nextNoteTime = audioCtx. currentTime + 0.05;
chordChangeTime = audioCtx. currentTime;
scheduler();
const canvas = document. getElementById('visualizer');
visualContext = canvas. getContext('2d');
drawVisualizer();
startTimer();
// ДИНАМИЧЕСКИЙ ВЫВОД РИТМ-СЕКЦИИ В СТАТУС
let drumsList = ( currentProfileKey === 'cinematicView' ) ? 'Атмосферный хор-эффект 🌌' : 'Чистые акустические барабаны 🥁';
// ВЫВОДИМ В СТАТУС АКТУАЛЬНЫЕ НАСТРОЙКИ ПРОФИЛЯ ВИДЕО
document. getElementById('status'). style. color = '#a370f7';
document. getElementById('status'). innerText =
'🎬 Режим: ' + profile. name +
' | Скорость: x' + currentSpeedModifier +
' | [Соло: ' + currentSoloInst + ']' +
' | [Бас: ' + currentBassInst + ']' +
' | [Ритм: ' + drumsList + ']' +
' | Ритм-маска №' + ( randomRhythmIdx + 1 ) +
' | Мелодия №' + ( randomMelodyIdx + 1 );
});
// ==========================================
// ЧАСТЬ 5: ТАЙМЕРЫ, СТУДИЙНЫЙ ЭКВАЛАЙЗЕР И СКАЧИВАНИЕ
// ==========================================
function startTimer() {
totalSeconds = 120;
updateTimerDisplay();
countdownInterval = setInterval(function() {
totalSeconds--;
updateTimerDisplay();
if (totalSeconds === 5) {
gainNode.gain.setValueAtTime(gainNode.gain.value, audioCtx.currentTime);
gainNode.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 5);
document.getElementById('status').style.color = '#00adb5';
document.getElementById('status').innerText = 'Завершение трека (Fade-out)...';
}
if (totalSeconds <= 0) {
stopMusic();
document.getElementById('status').style.color = '#888';
document.getElementById('status').innerText = '2 минуты истекли. Трек завершен.';
}
}, 1000);
}
function updateTimerDisplay() {
let minutes = Math.floor(totalSeconds / 60);
let seconds = totalSeconds % 60;
document.getElementById('timer').innerText =
minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0');
}
function createReverbImpulse(context, duration, decay) {
if (!duration) duration = 3.0;
if (!decay) decay = 2.0;
let sampleRate = context.sampleRate;
let length = sampleRate * duration;
let impulse = context.createBuffer(2, length, sampleRate);
let left = impulse.getChannelData(0);
let right = impulse.getChannelData(1);
for (let i = 0; i < length; i++) {
let percent = i / length;
let scaleVal = Math.pow(1 - percent, decay);
left[i] = (Math.random() * 2 - 1) * scaleVal;
right[i] = (Math.random() * 2 - 1) * scaleVal;
}
return impulse;
}
function stopMusic() {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
try { mediaRecorder.stop(); } catch(e) { console.error(e); }
}
isPlaying = false;
clearTimeout(timerId);
clearInterval(countdownInterval);
melodyPatternIndex = 0;
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
if (gainNode) { try { gainNode.gain.setValueAtTime(0, audioCtx.currentTime); gainNode.disconnect(); } catch(e){} }
if (channelSolo) { try { channelSolo.gain.setValueAtTime(0, audioCtx.currentTime); channelSolo.disconnect(); } catch(e){} }
if (channelPad) { try { channelPad.gain.setValueAtTime(0, audioCtx.currentTime); channelPad.disconnect(); } catch(e){} }
if (channelBass) { try { channelBass.gain.setValueAtTime(0, audioCtx.currentTime); channelBass.disconnect(); } catch(e){} }
if (channelDrums) { try { channelDrums.gain.setValueAtTime(0, audioCtx.currentTime); channelDrums.disconnect(); } catch(e){} }
if (delayNode) { try { delayNode.disconnect(); } catch(e){} }
if (delayFeedback) { try { delayFeedback.disconnect(); } catch(e){} }
if (reverbNode) { try { reverbNode.disconnect(); } catch(e){} }
channelSolo = null; channelPad = null; channelBass = null; channelDrums = null;
delayNode = null; delayFeedback = null; reverbNode = null;
// СБРОС АНАЛИЗАТОРА: Отключаем визуализатор от звуковой шины
analyserNode = null;
document.getElementById('status').style.color = '#888';
document.getElementById('status').innerText = 'Статус: остановлен';
// Мгновенно перерисовываем экран, чтобы показать ровную линию покоя
const canvas = document.getElementById('visualizer');
if (canvas && visualContext) {
visualContext.fillStyle = '#111111';
visualContext.fillRect(0, 0, canvas.width, canvas.height);
// Рисуем одну тонкую статичную линию по центру
visualContext.beginPath();
visualContext.lineWidth = 2;
visualContext.strokeStyle = '#00adb5';
visualContext.moveTo(0, canvas.height / 2);
visualContext.lineTo(canvas.width, canvas.height / 2);
visualContext.stroke();
}
}
function drawVisualizer() {
// Разрешаем анимацию только если музыка играет
if (isPlaying) {
animationId = requestAnimationFrame(drawVisualizer);
}
const canvas = document.getElementById('visualizer');
if (!canvas || !visualContext) return;
const width = canvas.width;
const height = canvas.height;
const centerY = height / 2;
visualContext.fillStyle = 'rgba(17, 17, 17, 0.25)';
visualContext.fillRect(0, 0, width, height);
let hasAudio = false;
let bufferLength = 32;
let dataArray = [];
// Строгая проверка: опрашиваем частоты только если нода физически существует
if (isPlaying && analyserNode) {
bufferLength = analyserNode.frequencyBinCount;
dataArray = new Uint8Array(bufferLength);
analyserNode.getByteFrequencyData(dataArray);
hasAudio = dataArray.some(function(volume) { return volume > 5; });
}
if (hasAudio) {
const halfLength = Math.floor(bufferLength * 0.7);
const barWidth = (width / 2) / halfLength;
for (let i = 0; i < halfLength; i++) {
let barHeight = (dataArray[i] / 255) * (height * 0.85);
if (barHeight < 2) barHeight = 2;
let percent = i / halfLength;
let r = Math.floor(163 - (percent * 63));
let g = Math.floor(112 + (percent * 100));
let b = Math.floor(247 - (percent * 47));
visualContext.fillStyle = 'rgb(' + r + ',' + g + ',' + b + ')';
let xLeft = (width / 2) - (i * barWidth) - barWidth;
let xRight = (width / 2) + (i * barWidth);
let yPos = centerY - (barHeight / 2);
visualContext.fillRect(xLeft, yPos, barWidth - 2, barHeight);
visualContext.fillRect(xRight, yPos, barWidth - 2, barHeight);
}
} else {
// Если трек на паузе или остановлен, рисуем спокойную ровную линию
visualContext.beginPath();
visualContext.lineWidth = 2;
visualContext.strokeStyle = '#00adb5';
visualContext.moveTo(0, centerY);
visualContext.lineTo(width, centerY);
visualContext.stroke();
}
}
document.getElementById('stopBtn').addEventListener('click', stopMusic);
const initialCanvas = document.getElementById('visualizer');
if (initialCanvas) {
visualContext = initialCanvas.getContext('2d');
drawVisualizer();
}
document.getElementById('downloadBtn').addEventListener('click', function() {
const triggerDownload = function(url) {
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = 'generative-mix-' + Math.floor(Date.now() / 1000) + '.webm';
document.body.appendChild(a);
a.click();
setTimeout(function() { document.body.removeChild(a); }, 100);
};
if (isPlaying && mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.onstop = function() {
if (recordedChunks.length === 0) return;
const blob = new Blob(recordedChunks, { type: 'audio/webm' });
latestTrackUrl = URL.createObjectURL(blob);
triggerDownload(latestTrackUrl);
};
stopMusic();
return;
}
if (latestTrackUrl) {
triggerDownload(latestTrackUrl);
} else {
alert('Сначала сгенерируйте и запустите микс!');
}
});
// ОБРАБОТЧИК ВЫПАДАЮЩЕГО СПИСКА ВЫБОРА ФОНА С ИСПРАВЛЕНИЕМ СТАТУСА
const profileSelector = document.getElementById('videoProfileSelect');
if (profileSelector) {
profileSelector.addEventListener('change', function(e) {
// Меняем глобальный ключ на выбранный (cooking, lifehacks или cinematicView)
currentProfileKey = e.target.value;
const currentProfile = VIDEO_MUSIC_PROFILES[currentProfileKey];
const statusElement = document.getElementById('status');
if (statusElement && currentProfile) {
if (isPlaying) {
statusElement.style.color = '#00adb5';
statusElement.innerText = 'Выбран стиль: "' + currentProfile.name + '". Нажмите "Случайный микс 🎲", чтобы применить новые инструменты!';
} else {
statusElement.style.color = '#8e8e93';
statusElement.innerText = 'Подготовлен профиль: ' + currentProfile.name + '. Готов к генерации.';
}
}
});
}
</script>