document.addEventListener('DOMContentLoaded', () => { // --- ESTADO CENTRAL DA APLICAÇÃO --- let appState = { tracks: [], isPlaying: false, playbackIntervalId: null, currentStep: 0, metronomeEnabled: false }; // --- Seletores de Elementos --- const trackContainer = document.getElementById('track-container'); const addInstrumentBtn = document.getElementById('add-instrument-btn'); const removeInstrumentBtn = document.getElementById('remove-instrument-btn'); const compassoAInput = document.getElementById('compasso-a-input'); const compassoBInput = document.getElementById('compasso-b-input'); const bpmInput = document.getElementById('bpm-input'); const playBtn = document.getElementById('play-btn'); const stopBtn = document.getElementById('stop-btn'); const rewindBtn = document.getElementById('rewind-btn'); const metronomeBtn = document.getElementById('metronome-btn'); const browserContent = document.getElementById('browser-content'); const sidebarToggle = document.getElementById('sidebar-toggle'); let audioContext; let mainGainNode; // --- FUNÇÕES DE ÁUDIO --- function initializeAudioContext() { if (!audioContext) { audioContext = new (window.AudioContext || window.webkitAudioContext)(); mainGainNode = audioContext.createGain(); mainGainNode.connect(audioContext.destination); } if (audioContext.state === 'suspended') { audioContext.resume(); } } function playMetronomeSound(isDownbeat) { initializeAudioContext(); const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); const frequency = isDownbeat ? 1000 : 800; oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime); oscillator.type = 'sine'; gainNode.gain.setValueAtTime(1, audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.00001, audioContext.currentTime + 0.05); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.05); } function playSample(filePath, trackId) { initializeAudioContext(); if (!filePath) return; const track = trackId ? appState.tracks.find(t => t.id == trackId) : null; const audio = new Audio(filePath); const source = audioContext.createMediaElementSource(audio); if (track && track.gainNode) { source.connect(track.gainNode); } else { source.connect(mainGainNode); } audio.play(); } // --- FUNÇÕES DE MANIPULAÇÃO DE DADOS (MODEL) --- function addTrackToState() { initializeAudioContext(); const newTrack = { id: Date.now(), name: 'novo instrumento', samplePath: null, steps: [], volume: 0.8, pan: 0.0, gainNode: audioContext.createGain(), pannerNode: audioContext.createStereoPanner() }; newTrack.gainNode.connect(newTrack.pannerNode); newTrack.pannerNode.connect(mainGainNode); newTrack.gainNode.gain.value = newTrack.volume; newTrack.pannerNode.pan.value = newTrack.pan; appState.tracks.push(newTrack); renderApp(); } function removeLastTrackFromState() { appState.tracks.pop(); renderApp(); } function updateTrackSample(trackId, samplePath) { const track = appState.tracks.find(t => t.id == trackId); if (track) { track.samplePath = samplePath; track.name = samplePath.split('/').pop(); } renderApp(); } function toggleStepState(trackId, stepIndex) { const track = appState.tracks.find(t => t.id == trackId); if (track) { track.steps[stepIndex] = !track.steps[stepIndex]; } } function updateTrackVolume(trackId, volume) { const track = appState.tracks.find(t => t.id == trackId); if (track) { const clampedVolume = Math.max(0, Math.min(1.5, volume)); track.volume = clampedVolume; if (track.gainNode) { track.gainNode.gain.setValueAtTime(clampedVolume, audioContext.currentTime); } } const knobElement = document.querySelector(`.knob[data-track-id='${trackId}'][data-control='volume']`); if(knobElement) updateKnobVisual(knobElement, 'volume'); } function updateTrackPan(trackId, pan) { const track = appState.tracks.find(t => t.id == trackId); if (track) { const clampedPan = Math.max(-1, Math.min(1, pan)); track.pan = clampedPan; if (track.pannerNode) { track.pannerNode.pan.setValueAtTime(clampedPan, audioContext.currentTime); } } const knobElement = document.querySelector(`.knob[data-track-id='${trackId}'][data-control='pan']`); if(knobElement) updateKnobVisual(knobElement, 'pan'); } // --- FUNÇÃO DE RENDERIZAÇÃO PRINCIPAL (VIEW) --- function renderApp() { trackContainer.innerHTML = ''; appState.tracks.forEach(trackData => { const trackLane = document.createElement('div'); trackLane.className = 'track-lane'; trackLane.dataset.trackId = trackData.id; trackLane.innerHTML = `
${trackData.name}
VOL
PAN
`; trackLane.addEventListener('dragover', (e) => { e.preventDefault(); trackLane.classList.add('drag-over'); }); trackLane.addEventListener('dragleave', () => trackLane.classList.remove('drag-over')); trackLane.addEventListener('drop', (e) => { e.preventDefault(); trackLane.classList.remove('drag-over'); const filePath = e.dataTransfer.getData('text/plain'); if (filePath) { updateTrackSample(trackData.id, filePath); } }); trackContainer.appendChild(trackLane); const volumeKnob = trackLane.querySelector(".knob[data-control='volume']"); addKnobInteraction(volumeKnob); updateKnobVisual(volumeKnob, 'volume'); const panKnob = trackLane.querySelector(".knob[data-control='pan']"); addKnobInteraction(panKnob); updateKnobVisual(panKnob, 'pan'); }); redrawSequencer(); } // --- LÓGICA DE INTERAÇÃO DOS KNOBS --- function addKnobInteraction(knobElement) { const controlType = knobElement.dataset.control; knobElement.addEventListener('mousedown', (e) => { if (e.button === 1) { // clique com o scroll e.preventDefault(); const trackId = knobElement.dataset.trackId; const defaultValue = (controlType === 'volume') ? 0.8 : 0.0; if (controlType === 'volume') { updateTrackVolume(trackId, defaultValue); } else { updateTrackPan(trackId, defaultValue); } } }); knobElement.addEventListener('mousedown', (e) => { if (e.button !== 0) return; e.preventDefault(); const trackId = knobElement.dataset.trackId; const track = appState.tracks.find(t => t.id == trackId); if (!track) return; const startY = e.clientY; const startValue = (controlType === 'volume') ? track.volume : track.pan; document.body.classList.add('knob-dragging'); function onMouseMove(moveEvent) { const deltaY = startY - moveEvent.clientY; const sensitivity = (controlType === 'volume') ? 150 : 200; const newValue = startValue + (deltaY / sensitivity); if (controlType === 'volume') { updateTrackVolume(trackId, newValue); } else { updateTrackPan(trackId, newValue); } } function onMouseUp() { document.body.classList.remove('knob-dragging'); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); } document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }); knobElement.addEventListener('wheel', (e) => { e.preventDefault(); const trackId = knobElement.dataset.trackId; const track = appState.tracks.find(t => t.id == trackId); if (!track) return; const step = 0.05; const direction = e.deltaY < 0 ? 1 : -1; if (controlType === 'volume') { const newValue = track.volume + (direction * step); updateTrackVolume(trackId, newValue); } else { const newValue = track.pan + (direction * step); updateTrackPan(trackId, newValue); } }); } function updateKnobVisual(knobElement, controlType) { const trackId = knobElement.dataset.trackId; const track = appState.tracks.find(t => t.id == trackId); if (!track) return; const indicator = knobElement.querySelector('.knob-indicator'); if (!indicator) return; const minAngle = -135; const maxAngle = 135; let percentage = 0.5; let title = ''; if (controlType === 'volume') { const value = track.volume; const clampedValue = Math.max(0, Math.min(1.5, value)); percentage = clampedValue / 1.5; title = `Volume: ${Math.round(clampedValue * 100)}%`; } else { // pan const value = track.pan; const clampedValue = Math.max(-1, Math.min(1, value)); percentage = (clampedValue + 1) / 2; const panDisplay = Math.round(clampedValue * 100); title = `Pan: ${panDisplay === 0 ? 'Centro' : (panDisplay < 0 ? `${-panDisplay} L` : `${panDisplay} R`)}`; } const angle = minAngle + (percentage * (maxAngle - minAngle)); indicator.style.transform = `translateX(-50%) rotate(${angle}deg)`; knobElement.title = title; } // --- RESTANTE DO SCRIPT --- function redrawSequencer() { const beatsPerBar = parseInt(compassoAInput.value, 10) || 4; const noteValue = parseInt(compassoBInput.value, 10) || 4; const subdivisions = Math.round(16 / noteValue); const totalSteps = beatsPerBar * subdivisions; document.querySelectorAll('.step-sequencer').forEach(container => { const parentTrackElement = container.closest('.track-lane'); const trackId = parentTrackElement.dataset.trackId; const trackData = appState.tracks.find(t => t.id == trackId); if (trackData && trackData.steps.length !== totalSteps) { const newStepsState = new Array(totalSteps).fill(false); for (let i = 0; i < Math.min(trackData.steps.length, totalSteps); i++) { newStepsState[i] = trackData.steps[i]; } trackData.steps = newStepsState; } container.innerHTML = ''; for (let i = 0; i < totalSteps; i++) { const stepWrapper = document.createElement('div'); stepWrapper.className = 'step-wrapper'; const stepElement = document.createElement('div'); stepElement.className = 'step'; if (trackData && trackData.steps[i] === true) { stepElement.classList.add('active'); } stepElement.addEventListener('click', () => { toggleStepState(trackData.id, i); stepElement.classList.toggle('active'); if (trackData && trackData.samplePath) { playSample(trackData.samplePath, trackData.id); } }); const groupIndex = Math.floor(i / beatsPerBar); if (groupIndex % 2 === 0) { stepElement.classList.add('step-dark'); } if (i > 0 && i % beatsPerBar === 0) { const marker = document.createElement('div'); marker.className = 'step-marker'; marker.textContent = i; stepWrapper.appendChild(marker); } stepWrapper.appendChild(stepElement); container.appendChild(stepWrapper); } }); } function tick() { const totalSteps = getTotalSteps(); if (totalSteps === 0) { stopPlayback(); return; } const lastStepIndex = (appState.currentStep === 0) ? totalSteps - 1 : appState.currentStep - 1; highlightStep(lastStepIndex, false); if (appState.metronomeEnabled) { const noteValue = parseInt(compassoBInput.value, 10) || 4; const stepsPerBeat = 16 / noteValue; if (appState.currentStep % stepsPerBeat === 0) { playMetronomeSound(appState.currentStep === 0); } } appState.tracks.forEach(track => { if (track.steps[appState.currentStep] && track.samplePath) { playSample(track.samplePath, track.id); } }); highlightStep(appState.currentStep, true); appState.currentStep = (appState.currentStep + 1) % totalSteps; } function startPlayback() { if (appState.isPlaying || appState.tracks.length === 0) return; initializeAudioContext(); const bpm = parseInt(bpmInput.value, 10) || 120; const stepInterval = (60 * 1000) / (bpm * 4); if (appState.playbackIntervalId) clearInterval(appState.playbackIntervalId); appState.isPlaying = true; playBtn.classList.remove('fa-play'); playBtn.classList.add('fa-pause'); tick(); appState.playbackIntervalId = setInterval(tick, stepInterval); } function stopPlayback() { clearInterval(appState.playbackIntervalId); appState.playbackIntervalId = null; appState.isPlaying = false; highlightStep(appState.currentStep - 1, false); appState.currentStep = 0; playBtn.classList.remove('fa-pause'); playBtn.classList.add('fa-play'); } function rewindPlayback() { appState.currentStep = 0; if(!appState.isPlaying) { document.querySelectorAll('.step.playing').forEach(s => s.classList.remove('playing')); } } function togglePlayback() { if (appState.isPlaying) { stopPlayback(); } else { startPlayback(); } } function highlightStep(stepIndex, isActive) { if (stepIndex < 0) return; document.querySelectorAll('.track-lane').forEach(track => { const stepWrapper = track.querySelector(`.step-sequencer .step-wrapper:nth-child(${stepIndex + 1})`); if (stepWrapper) { const stepElement = stepWrapper.querySelector('.step'); if (stepElement) { stepElement.classList.toggle('playing', isActive); } } }); } function getTotalSteps() { const beatsPerBar = parseInt(compassoAInput.value, 10) || 4; const noteValue = parseInt(compassoBInput.value, 10) || 4; const subdivisions = Math.round(16 / noteValue); return beatsPerBar * subdivisions; } sidebarToggle.addEventListener('click', () => { document.body.classList.toggle('sidebar-hidden'); const icon = sidebarToggle.querySelector('i'); icon.className = document.body.classList.contains('sidebar-hidden') ? 'fa-solid fa-caret-right' : 'fa-solid fa-caret-left'; }); async function loadAndRenderSampleBrowser() { try { const response = await fetch('samples-manifest.json'); if (!response.ok) { throw new Error('Arquivo samples-manifest.json não encontrado.'); } const fileTree = await response.json(); renderFileTree(fileTree, browserContent, 'samples'); } catch (error) { console.error("Erro ao carregar samples:", error); browserContent.innerHTML = `

${error.message}

`; } } function renderFileTree(tree, parentElement, currentPath) { parentElement.innerHTML = ''; const ul = document.createElement('ul'); const sortedKeys = Object.keys(tree).sort((a, b) => { const aIsFile = tree[a]._isFile; const bIsFile = tree[b]._isFile; if (aIsFile === bIsFile) return a.localeCompare(b); return aIsFile ? 1 : -1; }); for (const key of sortedKeys) { const node = tree[key]; const li = document.createElement('li'); const newPath = `${currentPath}/${key}`; if (node._isFile) { li.innerHTML = ` ${key}`; li.setAttribute('draggable', true); li.addEventListener('click', (e) => { e.stopPropagation(); playSample(newPath, null); }); li.addEventListener('dragstart', (e) => { e.dataTransfer.setData('text/plain', newPath); e.dataTransfer.effectAllowed = 'copy'; }); ul.appendChild(li); } else { li.className = 'directory'; li.innerHTML = ` ${key}`; const nestedUl = document.createElement('ul'); renderFileTree(node, nestedUl, newPath); li.appendChild(nestedUl); li.addEventListener('click', (e) => { e.stopPropagation(); li.classList.toggle('open'); }); ul.appendChild(li); } } parentElement.appendChild(ul); } function enforceNumericInput(event) { event.target.value = event.target.value.replace(/[^0-9]/g, ''); } function adjustValue(inputElement, step) { let currentValue = parseInt(inputElement.value, 10) || 0; let min = parseInt(inputElement.dataset.min, 10); let max = parseInt(inputElement.dataset.max, 10); let newValue = currentValue + step; if (!isNaN(min) && newValue < min) newValue = min; if (!isNaN(max) && newValue > max) newValue = max; inputElement.value = newValue; if (appState.isPlaying && inputElement.id.startsWith('compasso-')) { stopPlayback(); } if (inputElement.id.startsWith('compasso-')) { redrawSequencer(); } } const inputs = document.querySelectorAll('.value-input'); inputs.forEach(input => { input.addEventListener('input', (event) => { enforceNumericInput(event); if (appState.isPlaying && event.target.id.startsWith('compasso-')) { stopPlayback(); } if (event.target.id.startsWith('compasso-')) { redrawSequencer(); } }); input.addEventListener('wheel', (event) => { event.preventDefault(); const step = event.deltaY < 0 ? 1 : -1; adjustValue(event.target, step); }); }); const buttons = document.querySelectorAll('.adjust-btn'); buttons.forEach(button => { button.addEventListener('click', () => { const targetId = button.dataset.target + '-input'; const targetInput = document.getElementById(targetId); const step = parseInt(button.dataset.step, 10); if (targetInput) { adjustValue(targetInput, step); } }); }); // --- INICIALIZAÇÃO E EVENTOS FINAIS --- addInstrumentBtn.addEventListener('click', addTrackToState); removeInstrumentBtn.addEventListener('click', removeLastTrackFromState); playBtn.addEventListener('click', togglePlayback); stopBtn.addEventListener('click', stopPlayback); rewindBtn.addEventListener('click', rewindPlayback); metronomeBtn.addEventListener('click', () => { initializeAudioContext(); appState.metronomeEnabled = !appState.metronomeEnabled; metronomeBtn.classList.toggle('active', appState.metronomeEnabled); }); loadAndRenderSampleBrowser(); renderApp(); });