Amostras de áudio funcionais até o momento. Agulha de tempo, mute/solo e ajustado com a velocidade de reprodução

This commit is contained in:
JotaChina committed 2025-10-12 09:23:27 -03:00
1 parent 8c391412e1
commit 602df1db6d
10 files changed
+942 -689

No files matched your search

+168 -24
View File
@@ -5,17 +5,34 @@ import {
updateTrackSample,
updateTrackVolume,
updateTrackPan,
addAudioTrack,
toggleAudioTrackSolo,
} from "./state.js";
import { playSample, stopPlayback } from "./audio.js";
import { playSample, stopPlayback, seekAudioEditor } from "./audio.js";
import { getTotalSteps } from "./utils.js";
import { loadProjectFromServer } from "./file.js";
import { drawWaveform } from "./waveform.js";
import { PIXELS_PER_STEP, PIXELS_PER_BAR } from "./config.js";
export function updateAudioEditorUI() {
const playBtn = document.getElementById('audio-editor-play-btn');
if (playBtn) {
if (appState.isAudioEditorPlaying) {
playBtn.classList.remove('fa-play');
playBtn.classList.add('fa-pause');
} else {
playBtn.classList.remove('fa-pause');
playBtn.classList.add('fa-play');
}
}
}
let samplePathMap = {};
const globalPatternSelector = document.getElementById('global-pattern-selector');
if (globalPatternSelector) {
globalPatternSelector.addEventListener('change', () => {
// A linha stopPlayback() foi REMOVIDA daqui, permitindo a troca em tempo real.
stopPlayback();
appState.activePatternIndex = parseInt(globalPatternSelector.value, 10);
const firstTrack = appState.tracks[0];
@@ -105,6 +122,137 @@ function buildSamplePathMap(tree, currentPath) {
}
}
export function renderAudioEditor() {
const audioEditor = document.querySelector('.audio-editor');
const audioTrackContainer = document.getElementById('audio-track-container');
if (!audioEditor || !audioTrackContainer) return;
audioEditor.ondragover = (e) => {
e.preventDefault();
audioEditor.classList.add("drag-over");
};
audioEditor.ondragleave = () => {
audioEditor.classList.remove("drag-over");
};
audioEditor.ondrop = (e) => {
e.preventDefault();
audioEditor.classList.remove("drag-over");
const filePath = e.dataTransfer.getData("text/plain");
if (filePath) {
addAudioTrack(filePath);
}
};
audioTrackContainer.innerHTML = '';
appState.audioTracks.forEach(trackData => {
const audioTrackLane = document.createElement('div');
audioTrackLane.className = 'audio-track-lane';
audioTrackLane.dataset.trackId = trackData.id;
audioTrackLane.innerHTML = `
<div class="track-info">
<i class="fa-solid fa-gear"></i>
<div class="track-solo-btn"></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="spectrogram-view-wrapper">
<div class="spectrogram-view-grid">
<div class="playhead"></div>
</div>
</div>
`;
const grid = audioTrackLane.querySelector('.spectrogram-view-grid');
const canvas = document.createElement('canvas');
canvas.className = 'waveform-canvas';
canvas.height = 60;
grid.prepend(canvas);
audioTrackContainer.appendChild(audioTrackLane);
if (trackData.audioBuffer) {
const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120;
const sampleDuration = trackData.audioBuffer.duration;
const stepsPerSecond = (bpm / 60) * 4;
const totalSteps = sampleDuration * stepsPerSecond;
const canvasWidth = totalSteps * PIXELS_PER_STEP;
canvas.width = canvasWidth;
drawWaveform(canvas, trackData.audioBuffer, 'var(--accent-green)');
const numberOfBars = Math.ceil(canvasWidth / PIXELS_PER_BAR);
for (let i = 0; i < numberOfBars; i++) {
if (i === 0) continue;
const marker = document.createElement('div');
marker.className = 'bar-marker';
marker.textContent = i + 1;
marker.style.left = `${i * PIXELS_PER_BAR}px`;
grid.appendChild(marker);
}
}
const soloButton = audioTrackLane.querySelector('.track-solo-btn');
if (soloButton) {
if (trackData.isSoloed) {
soloButton.classList.add('active');
}
soloButton.addEventListener('click', (e) => {
e.stopPropagation();
toggleAudioTrackSolo(trackData.id);
});
}
const volumeKnob = audioTrackLane.querySelector('.knob[data-control="volume"]');
addKnobInteraction(volumeKnob);
updateKnobVisual(volumeKnob, "volume");
const panKnob = audioTrackLane.querySelector('.knob[data-control="pan"]');
addKnobInteraction(panKnob);
updateKnobVisual(panKnob, "pan");
const waveformWrapper = audioTrackLane.querySelector('.spectrogram-view-wrapper');
const handleSeek = (event) => {
const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120;
const stepsPerSecond = (bpm / 60) * 4;
const pixelsPerSecond = stepsPerSecond * PIXELS_PER_STEP;
const rect = waveformWrapper.getBoundingClientRect();
const clickX = event.clientX - rect.left;
const scrollLeft = waveformWrapper.scrollLeft;
const absoluteX = clickX + scrollLeft;
const newTime = absoluteX / pixelsPerSecond;
seekAudioEditor(newTime);
};
waveformWrapper.addEventListener('mousedown', (e) => {
e.preventDefault();
handleSeek(e);
const onMouseMove = (moveEvent) => handleSeek(moveEvent);
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
});
}
export function renderApp() {
const trackContainer = document.getElementById("track-container");
trackContainer.innerHTML = "";
@@ -139,25 +287,12 @@ export function renderApp() {
trackLane.addEventListener('click', () => {
if (appState.activeTrackId === trackData.id) return;
// A linha stopPlayback() também foi REMOVIDA daqui
stopPlayback();
appState.activeTrackId = trackData.id;
document.querySelectorAll('.track-lane').forEach(lane => lane.classList.remove('active-track'));
trackLane.classList.add('active-track');
updateGlobalPatternSelector();
// Apenas redesenha a UI, sem parar a música
const activeTrack = appState.tracks.find(t => t.id === appState.activeTrackId);
if (activeTrack) {
const activePattern = activeTrack.patterns[activeTrack.activePatternIndex];
if (activePattern) {
const stepsPerBar = 16;
const requiredBars = Math.ceil(activePattern.steps.length / stepsPerBar);
document.getElementById("bars-input").value = requiredBars > 0 ? requiredBars : 1;
redrawSequencer();
}
}
redrawSequencer();
});
trackLane.addEventListener("dragover", (e) => { e.preventDefault(); trackLane.classList.add("drag-over"); });
@@ -182,6 +317,7 @@ export function renderApp() {
updateGlobalPatternSelector();
redrawSequencer();
renderAudioEditor();
}
export function redrawSequencer() {
@@ -266,7 +402,7 @@ function addKnobInteraction(knobElement) {
if (e.button !== 0) return;
e.preventDefault();
const trackId = knobElement.dataset.trackId;
const track = appState.tracks.find((t) => t.id == trackId);
const track = appState.tracks.find((t) => t.id == trackId) || appState.audioTracks.find((t) => t.id == trackId);
if (!track) return;
const startY = e.clientY;
const startValue = controlType === "volume" ? track.volume : track.pan;
@@ -293,7 +429,7 @@ function addKnobInteraction(knobElement) {
knobElement.addEventListener("wheel", (e) => {
e.preventDefault();
const trackId = knobElement.dataset.trackId;
const track = appState.tracks.find((t) => t.id == trackId);
const track = appState.tracks.find((t) => t.id == trackId) || appState.audioTracks.find((t) => t.id == trackId);
if (!track) return;
const step = 0.05;
const direction = e.deltaY < 0 ? 1 : -1;
@@ -310,7 +446,7 @@ function addKnobInteraction(knobElement) {
function updateKnobVisual(knobElement, controlType) {
const trackId = knobElement.dataset.trackId;
const track = appState.tracks.find((t) => t.id == trackId);
const track = appState.tracks.find((t) => t.id == trackId) || appState.audioTracks.find((t) => t.id == trackId);
if (!track) return;
const indicator = knobElement.querySelector(".knob-indicator");
if (!indicator) return;
@@ -359,11 +495,7 @@ export function highlightStep(stepIndex, isActive) {
export async function loadAndRenderSampleBrowser() {
const browserContent = document.getElementById("browser-content");
try {
// --- CORREÇÃO AQUI ---
// Adiciona um timestamp à URL para evitar que o navegador use uma versão antiga (em cache) do arquivo.
const response = await fetch(`metadata/samples-manifest.json?v=${Date.now()}`);
// --- FIM DA CORREÇÃO ---
if (!response.ok) {
throw new Error("Arquivo samples-manifest.json não encontrado.");
}
@@ -459,4 +591,16 @@ export async function showOpenProjectModal() {
export function closeOpenProjectModal() {
const openProjectModal = document.getElementById("open-project-modal");
openProjectModal.classList.remove("visible");
}
export function updatePlayheadVisual(pixels) {
document.querySelectorAll('.audio-track-lane .playhead').forEach(ph => {
ph.style.left = `${pixels}px`;
});
}
export function resetPlayheadVisual() {
document.querySelectorAll('.audio-track-lane .playhead').forEach(ph => {
ph.style.left = '0px';
});
}