Enviar arquivos para "js"
This commit is contained in:
parent
0c716c1572
commit
253e236431
|
@ -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 = `
|
||||
<div class="track-info"><i class="fa-solid fa-gear"></i><div class="track-mute"></div><span class="track-name">${trackData.name}</span></div>
|
||||
<div class="track-controls">
|
||||
<div class="knob-container">
|
||||
<div class="knob" data-control="volume" data-track-id="${trackData.id}">
|
||||
<div class="knob-indicator"></div>
|
||||
</div>
|
||||
<span>VOL</span>
|
||||
</div>
|
||||
<div class="knob-container">
|
||||
<div class="knob" data-control="pan" data-track-id="${trackData.id}">
|
||||
<div class="knob-indicator"></div>
|
||||
</div>
|
||||
<span>PAN</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-sequencer"></div>
|
||||
`;
|
||||
|
||||
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 = `<p style="color:var(--accent-red); padding: 10px;">${error.message}</p>`; } }
|
||||
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 = `<i class="fa-solid fa-file-audio"></i> ${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 = `<i class="fa-solid fa-folder"></i> ${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();
|
||||
});
|
Loading…
Reference in New Issue