<!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>