<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Generative Engine — Vanshots Only</title>
<style>
:root { --bg: #12121a; --panel: #1e1e26; --accent: #a370f7; --text: #e0e0e6; }
body { background: var(--bg); color: var(--text); font-family: 'Segoe UI', sans-serif; margin: 0; padding: 20px; display: flex; flex-direction: column; align-items: center; }
.studio-panel { background: var(--panel); padding: 24px; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.5); width: 100%; max-width: 900px; }
h2 { margin-top: 0; color: var(--accent); }
.controls-row { display: flex; gap: 16px; align-items: flex-end; flex-wrap: wrap; margin-bottom: 16px; }
label { display: flex; flex-direction: column; color: #aaa; font-size: 13px; margin-right: 8px; min-width: 80px; }
input[type="range"] { width: 140px; accent-color: var(--accent); }
button { padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; transition: 0.2s; }
#randomBtn { background: #2c2c36; color: var(--accent); border: 1px solid var(--accent); }
#stopBtn { background: #3a1f21; color: white; }
#downloadBtn { background: #00adb5; color: white; }
button:hover { transform: translateY(-2px); filter: brightness(1.1); }
.timer-status { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; font-size: 20px; font-weight: bold; color: #fff; }
#status { color: #86c232; font-size: 15px; line-height: 1.4; }
canvas#visualizer { width: 100%; height: 180px; background: #0a0a0d; border-radius: 8px; margin-top: 16px; border: 1px solid #333; }
select { padding: 8px; border-radius: 4px; background: #2a2a33; color: white; border: 1px solid #444; }
</style>
</head>
<body>
<div class="studio-panel">
<h2>🎹 Генеративный движок — на ваншотах</h2>
<div style="margin-bottom:16px;">
<label>Профиль видео:</label>
<select id="videoProfileSelect">
<option value="cooking">🍳 Кулинария (Уютный лаунж)</option>
<option value="lifehacks" selected>🛠 Лайфхаки (Энергичный DIY)</option>
<option value="cinematicView">🌌 Кинематограф (Атмосферный пейзаж)</option>
</select>
</div>
<div class="controls-row">
<label>Соло<br><input type="range" id="volSolo" min="0" max="1" step="0.01" value="0.5"></label>
<label>Пад<br><input type="range" id="volPad" min="0" max="1" step="0.01" value="0.5"></label>
<label>Бас<br><input type="range" id="volBass" min="0" max="1" step="0.01" value="0.5"></label>
<label>Барабаны<br><input type="range" id="volDrums" min="0" max="1" step="0.01" value="0.5"></label>
</div>
<div class="controls-row">
<button id="randomBtn">🎲 Случайный микс</button>
<button id="stopBtn">⏹ Стоп</button>
<button id="downloadBtn" disabled>⬇️ Скачать трек</button>
</div>
<div class="timer-status">
<span>Таймер: <span id="timer">02:00</span></span>
<div id="status" style="max-width: 500px;">Статус: готов к запуску</div>
</div>
<canvas id="visualizer"></canvas>
</div>
<script>
/**
* ==========================================
* ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ
* ==========================================
*/
const AudioContext = window.AudioContext || window.webkitAudioContext;
let audioCtx = null;
// Мастер-шина
let gainNode = null;
let masterCompressor = null;
let brickwallLimiter = null;
let analyserNode = null;
let delayNode = null;
let delayFeedback = null;
let reverbNode = null;
// Каналы инструментов
let channelSolo = null;
let channelPad = null;
let channelBass = null;
let channelDrums = null;
// Запись
let mediaRecorder = null;
let recordedChunks = [];
let latestTrackUrl = null;
// Визуализация
let animationId = null;
let isPlaying = false;
let countdownInterval = null;
let totalSeconds = 120;
// Состояние движка
let currentProfileKey = 'lifehacks';
let currentSpeedModifier = 1.0;
let scale = [];
let chordRoots = [];
let currentChord = [60, 64, 67];
let activeTemplate = [];
let currentMelodyPattern = [];
let templateIndex = 0;
let melodyPatternIndex = 0;
let nextNoteTime = 0.0;
let chordChangeTime = 0.0;
/**
* ==========================================
* ПРОФИЛИ
* ==========================================
*/
const VIDEO_MUSIC_PROFILES = {
'cooking': {
name: 'Кулинария',
soloInstrument: 'piano',
bassInstrument: 'acoustic_bass',
padInstrument: 'pad_warm',
scale: [60, 62, 64, 65, 67, 69, 71, 72],
noteDuration: 1.5,
structuralDelay: 0.5,
noteChance: 0.6,
drumPattern: { kick: [1,0,1,0, 1,0,1,0], snare: [0,1,0,1, 0,1,0,1], hihat: [1,1,1,1, 1,1,1,1] },
sfx: { lowWave: { note: 36, duration: 2.0, velocity: 0.3, chance: 0.1 }, midWave: { note: 48, duration: 1.5, velocity: 0.4, chance: 0.15 }, highWave: { note: 60, duration: 1.0, velocity: 0.35, chance: 0.1 } }
},
'lifehacks': {
name: 'Лайфхаки',
soloInstrument: 'bright_piano',
bassInstrument: 'synth_bass_1',
padInstrument: 'pad_choir',
scale: [62, 64, 66, 67, 69, 71, 73, 74],
noteDuration: 0.8,
structuralDelay: 0.2,
noteChance: 0.7,
drumPattern: { kick: [1,1,1,0, 1,1,1,0], snare: [0,0,1,1, 0,0,1,1], hihat: [2,2,2,2, 2,2,2,2] },
sfx: {}
},
'cinematicView': {
name: 'Кинематограф',
soloInstrument: 'marimba',
bassInstrument: null,
padInstrument: 'pad_warm',
scale: [57, 59, 60, 62, 64, 65, 67, 69],
noteDuration: 2.5,
structuralDelay: 0.8,
noteChance: 0.5,
drumPattern: null,
sfx: { lowWave: { note: 30, duration: 4.0, velocity: 0.2, chance: 0.3 }, midWave: { note: 45, duration: 3.0, velocity: 0.3, chance: 0.25 }, highWave: { note: 62, duration: 2.0, velocity: 0.25, chance: 0.2 } }
}
};
/**
* ==========================================
* ЗАГРУЗКА ВАНШОТОВ (SAMPLES)
* ==========================================
*/
const sampleUrls = {
kick: "audio/drums/kick.mp3",
snare: "audio/drums/snare.mp3",
hat_closed: "audio/drums/hat_closed.mp3",
hat_open: "audio/drums/hat_open.mp3",
reverse_clap: "audio/fx/reverse_clap.wav",
bass_C: "audio/instruments/bass/dune_bass_C.wav",
pad_C: "audio/instruments/pads/zebra_pad_C.wav",
piano_M1_C: "audio/instruments/piano/piano_M1_C.wav",
piano_acoustic_G: "audio/instruments/piano/piano_acoustic_G.wav",
string_pizz_G: "audio/instruments/strings/string_pizz_G.wav"
};
// MIDI note → ключ в loadedSamples
const midiToSampleKey = {
36: "kick",
38: "snare",
42: "hat_closed",
46: "hat_open",
39: "reverse_clap",
48: "bass_C",
52: "pad_C",
60: "piano_M1_C",
67: "piano_acoustic_G",
75: "string_pizz_G"
};
let loadedSamples = {}; // { name: AudioBuffer }
async function loadSamples() {
const toLoad = Object.entries(sampleUrls);
for (const [name, url] of toLoad) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`Не удалось загрузить ${url}`);
const arrayBuffer = await response.arrayBuffer();
const buffer = await audioCtx.decodeAudioData(arrayBuffer);
loadedSamples[name] = buffer;
console.log(`✅ Загружен сэмпл: ${name}`);
} catch (e) {
console.warn(`⚠️ Не удалось загрузить сэмпл ${name}:`, e.message);
loadedSamples[name] = null;
}
}
}
/**
* ==========================================
* ИНИЦИАЛИЗАЦИЯ АУДИО-ГРАФА
* ==========================================
*/
function initAudioGraph() {
if (audioCtx && channelSolo) return;
if (!audioCtx) {
audioCtx = new AudioContext();
}
try {
gainNode = audioCtx.createGain();
gainNode.gain.setValueAtTime(0.45, audioCtx.currentTime);
masterCompressor = audioCtx.createDynamicsCompressor();
masterCompressor.threshold.setValueAtTime(-14.0, audioCtx.currentTime);
masterCompressor.ratio.setValueAtTime(4, audioCtx.currentTime);
brickwallLimiter = audioCtx.createDynamicsCompressor();
brickwallLimiter.threshold.setValueAtTime(-0.5, audioCtx.currentTime);
brickwallLimiter.ratio.setValueAtTime(20, audioCtx.currentTime);
analyserNode = audioCtx.createAnalyser();
analyserNode.fftSize = 64;
channelSolo = audioCtx.createGain();
channelPad = audioCtx.createGain();
channelBass = audioCtx.createGain();
channelDrums = audioCtx.createGain();
delayNode = audioCtx.createDelay();
delayFeedback = audioCtx.createGain();
delayNode.delayTime.setValueAtTime(0.4, audioCtx.currentTime);
delayFeedback.gain.setValueAtTime(0.35, audioCtx.currentTime);
delayNode.connect(delayFeedback);
delayFeedback.connect(delayNode);
reverbNode = null;
try {
const impulse = createReverbImpulse(audioCtx, 1.6, 1.5);
reverbNode = audioCtx.createConvolver();
reverbNode.buffer = impulse;
} catch (e) {
console.warn('⚠️ Не удалось создать реверб, работаем без него.', e);
}
// Подключение мастер-шины
channelSolo.connect(gainNode);
channelPad.connect(gainNode);
channelBass.connect(gainNode);
channelDrums.connect(gainNode);
channelSolo.connect(delayNode);
if (reverbNode) {
channelPad.connect(reverbNode);
}
delayNode.connect(gainNode);
if (reverbNode) {
reverbNode.connect(gainNode);
}
gainNode.connect(masterCompressor);
masterCompressor.connect(brickwallLimiter);
brickwallLimiter.connect(analyserNode);
analyserNode.connect(audioCtx.destination);
console.log('✅ Аудио-граф успешно построен.');
} catch (err) {
console.error('❌ Критическая ошибка при построении графа:', err);
audioCtx = null;
channelSolo = null;
}
}
function createReverbImpulse(context, duration, decay) {
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 playSampleOnChannel(buffer, gainValue, channelNode, timeOffset = 0) {
if (!buffer) return;
const source = audioCtx.createBufferSource();
source.buffer = buffer;
source.playbackRate.value = 1;
const gain = audioCtx.createGain();
gain.gain.setValueAtTime(gainValue, audioCtx.currentTime + timeOffset);
source.connect(gain);
gain.connect(channelNode);
source.start(audioCtx.currentTime + timeOffset);
}
/**
* ==========================================
* ПЛАНИРОВЩИК НОТ И РИТМА
* ==========================================
*/
function scheduleNotes() {
const profile = VIDEO_MUSIC_PROFILES[currentProfileKey];
const bpm = 120; // базовый темп
const beatDuration = 60 / bpm;
const step = beatDuration * currentSpeedModifier;
// Если уже играем — не запускаем второй планировщик
if (isPlaying) return;
isPlaying = true;
nextNoteTime = audioCtx.currentTime;
chordChangeTime = audioCtx.currentTime;
scheduler();
}
function scheduler() {
const profile = VIDEO_MUSIC_PROFILES[currentProfileKey];
const bpm = 120;
const beatDuration = 60 / bpm;
const step = beatDuration * currentSpeedModifier;
let now = audioCtx.currentTime;
// Планируем события на ближайшее окно (чтобы не отставать)
while (nextNoteTime <= now + 0.1) {
const beatIndex = Math.floor(nextNoteTime / beatDuration);
const patternPos = beatIndex % 8; // 8‑дольный паттерн
// Барабаны (по паттерну)
if (profile.drumPattern) {
const kickPattern = profile.drumPattern.kick;
const snarePattern = profile.drumPattern.snare;
const hatPattern = profile.drumPattern.hihat;
if (kickPattern[patternPos]) {
const volSoloVal = parseFloat(document.getElementById('volDrums').value);
const mult = (currentProfileKey === 'cinematicView') ? 0.40 : 0.85;
const sampleKey = midiToSampleKey[36];
if (sampleKey && loadedSamples[sampleKey]) {
playSampleOnChannel(loadedSamples[sampleKey], volSoloVal * mult, channelDrums, 0);
}
}
if (snarePattern[patternPos]) {
const volSoloVal = parseFloat(document.getElementById('volDrums').value);
const mult = (currentProfileKey === 'cinematicView') ? 0.40 : 0.85;
const sampleKey = midiToSampleKey[38];
if (sampleKey && loadedSamples[sampleKey]) {
playSampleOnChannel(loadedSamples[sampleKey], volSoloVal * mult, channelDrums, 0);
}
}
// хэты: 2 = открытый, 1 = закрытый
if (hatPattern[patternPos] === 1) {
const volSoloVal = parseFloat(document.getElementById('volDrums').value);
const mult = (currentProfileKey === 'cinematicView') ? 0.40 : 0.85;
const sampleKey = midiToSampleKey[42];
if (sampleKey && loadedSamples[sampleKey]) {
playSampleOnChannel(loadedSamples[sampleKey], volSoloVal * mult * 0.7, channelDrums, 0);
}
} else if (hatPattern[patternPos] === 2) {
const volSoloVal = parseFloat(document.getElementById('volDrums').value);
const mult = (currentProfileKey === 'cinematicView') ? 0.40 : 0.85;
const sampleKey = midiToSampleKey[46];
if (sampleKey && loadedSamples[sampleKey]) {
playSampleOnChannel(loadedSamples[sampleKey], volSoloVal * mult * 0.7, channelDrums, 0);
}
}
}
// Соло-мелодия (простая рандомизация по гамме)
// --- НОВАЯ МЕЛОДИЯ: АРПЕДЖИО ПО ШАБЛОНУ ---
// Считаем, сколько тактов прошло с начала трека
const beatsSinceStart = Math.floor(nextNoteTime / beatDuration);
// Играем арпеджио каждые 2 такта (на 1-й и 3-й доле)
if (beatsSinceStart % 2 === 0 && patternPos === 0) {
const scale = profile.scale;
// Простой паттерн: 1-3-5-3 ступени гаммы
const patternIndices = [0, 2, 4, 2];
const indexInPattern = (beatsSinceStart / 2) % patternIndices.length;
const noteIndex = patternIndices[indexInPattern];
if (noteIndex < scale.length) {
const midiNote = scale[noteIndex];
const sampleKey = midiToSampleKey[midiNote];
if (sampleKey && loadedSamples[sampleKey]) {
const volSoloVal = parseFloat(document.getElementById('volSolo').value);
playSampleOnChannel(loadedSamples[sampleKey], volSoloVal * 0.75, channelSolo, 0);
}
}
}
nextNoteTime += step;
}
if (isPlaying) {
requestAnimationFrame(scheduler);
}
}
/**
* ==========================================
* ОБНОВЛЕНИЕ ГРОМКОСТЕЙ С ПОЛЗУНКОВ (ONINPUT)
* ==========================================
*/
function setupVolumeControls() {
const ids = [
{ id: 'volSolo', channel: channelSolo, mult: 0.75 },
{ id: 'volPad', channel: channelPad, mult: 0.85 },
{ id: 'volBass', channel: channelBass, mult: 0.55 },
{ id: 'volDrums', channel: channelDrums, mult: null } // спец. логика ниже
];
ids.forEach(item => {
const el = document.getElementById(item.id);
if (!el) return;
el.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
if (!item.channel) return;
let mult = item.mult;
if (item.id === 'volDrums') {
mult = (currentProfileKey === 'cinematicView') ? 0.40 : 0.85;
}
item.channel.gain.setValueAtTime(val * mult, audioCtx.currentTime);
});
});
}
/**
* ==========================================
* ТАЙМЕР И FADE-OUT
* ==========================================
*/
function startTimer() {
totalSeconds = 120;
updateTimerDisplay();
countdownInterval = setInterval(() => {
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 drawVisualizer() {
const canvas = document.getElementById('visualizer');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const bufferLength = analyserNode.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
analyserNode.getByteFrequencyData(dataArray);
ctx.clearRect(0, 0, canvas.width, canvas.height);
const barWidth = (canvas.width / bufferLength) * 1.2;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const barHeight = dataArray[i] * 0.9;
ctx.fillStyle = `hsl(${i * 2}, 70%, 50%)`;
ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
x += barWidth + 1;
}
if (isPlaying && animationId === null) {
animationId = requestAnimationFrame(drawVisualizer);
} else if (!isPlaying && animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
}
/**
* ==========================================
* ЗАПИСЬ ТРЕКА (MEDIA RECORDER)
* ==========================================
*/
function startRecording() {
if (!audioCtx || !channelSolo) {
console.warn('🔧 Аудио-граф не готов, запускаем инициализацию...');
initAudioGraph();
if (!channelSolo) {
alert('Не удалось инициализировать аудио. Проверьте консоль (F12) на ошибки.');
return;
}
}
const recNode = audioCtx.createGain();
recNode.gain.setValueAtTime(1, audioCtx.currentTime);
channelSolo.connect(recNode);
channelPad.connect(recNode);
channelBass.connect(recNode);
channelDrums.connect(recNode);
const destination = audioCtx.createMediaStreamDestination();
recNode.connect(destination);
mediaRecorder = new MediaRecorder(destination.stream);
recordedChunks = [];
mediaRecorder.ondataavailable = e => recordedChunks.push(e.data);
mediaRecorder.onstop = () => {
const blob = new Blob(recordedChunks, { type: 'audio/webm' });
latestTrackUrl = URL.createObjectURL(blob);
const btn = document.getElementById('downloadBtn');
if (btn) btn.disabled = false;
console.log('✅ Запись сохранена, можно скачивать.');
};
mediaRecorder.start();
console.log('🎤 Запись начата...');
}
function stopRecording() {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop();
}
}
/**
* ==========================================
* УПРАВЛЕНИЕ (КНОПКИ)
* ==========================================
*/
function stopMusic() {
isPlaying = false;
clearInterval(countdownInterval);
stopRecording();
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
document.getElementById('status').style.color = '#888';
document.getElementById('status').innerText = 'Трек остановлен.';
}
document.getElementById('randomBtn').addEventListener('click', async () => {
currentProfileKey = document.getElementById('videoProfileSelect').value;
if (!audioCtx) {
audioCtx = new AudioContext();
await loadSamples();
initAudioGraph();
setupVolumeControls();
}
stopMusic(); // сброс предыдущего
startRecording();
startTimer();
scheduleNotes();
drawVisualizer();
});
document.getElementById('stopBtn').addEventListener('click', () => {
stopMusic();
});
document.getElementById('downloadBtn').addEventListener('click', () => {
if (latestTrackUrl) {
const a = document.createElement('a');
a.href = latestTrackUrl;
a.download = `track_${currentProfileKey}_${Date.now()}.webm`;
a.click();
} else {
alert('Сначала запустите трек, чтобы появилась запись для скачивания.');
}
});
</script>
</body>
</html>