From 253e2364313150b3da9d026929dd902b5ad05d8f Mon Sep 17 00:00:00 2001 From: JotaChina Date: Tue, 19 Aug 2025 21:51:45 -0300 Subject: [PATCH] Enviar arquivos para "js" --- js/main.js | 350 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 350 insertions(+) create mode 100644 js/main.js diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..8e87def --- /dev/null +++ b/js/main.js @@ -0,0 +1,350 @@ +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(); +}); \ No newline at end of file