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:
1 parent
8c391412e1
commit
602df1db6d
10 files changed
+942
-689
No files matched your search
@@ -1,7 +1,8 @@
|
||||
// js/audio.js
|
||||
import { appState } from "./state.js";
|
||||
import { highlightStep } from "./ui.js";
|
||||
import { highlightStep, updateAudioEditorUI, updatePlayheadVisual, resetPlayheadVisual } from "./ui.js";
|
||||
import { getTotalSteps } from "./utils.js";
|
||||
import { PIXELS_PER_STEP } from "./config.js";
|
||||
|
||||
let audioContext;
|
||||
let mainGainNode;
|
||||
@@ -30,6 +31,7 @@ export function initializeAudioContext() {
|
||||
}
|
||||
}
|
||||
|
||||
// ... (funções de master volume/pan, formatTime, metronome, sample player, tick, etc. permanecem iguais)...
|
||||
export function updateMasterVolume(volume) {
|
||||
if (mainGainNode) {
|
||||
mainGainNode.gain.setValueAtTime(volume, audioContext.currentTime);
|
||||
@@ -120,7 +122,6 @@ function tick() {
|
||||
appState.tracks.forEach((track) => {
|
||||
if (!track.patterns || track.patterns.length === 0) return;
|
||||
|
||||
// Usa o índice GLOBAL para saber qual pattern tocar, sincronizando com a UI.
|
||||
const activePattern = track.patterns[appState.activePatternIndex];
|
||||
|
||||
if (activePattern && activePattern.steps[appState.currentStep] && track.samplePath) {
|
||||
@@ -190,4 +191,104 @@ export function togglePlayback() {
|
||||
appState.currentStep = 0;
|
||||
startPlayback();
|
||||
}
|
||||
}
|
||||
|
||||
function animationLoop() {
|
||||
if (!appState.isAudioEditorPlaying || !audioContext) return;
|
||||
const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120;
|
||||
const stepsPerSecond = (bpm / 60) * 4;
|
||||
const pixelsPerSecond = stepsPerSecond * PIXELS_PER_STEP;
|
||||
const totalElapsedTime = (audioContext.currentTime - appState.audioEditorStartTime) + appState.audioEditorPlaybackTime;
|
||||
const newPositionPx = totalElapsedTime * pixelsPerSecond;
|
||||
const maxDuration = appState.audioTracks.reduce((max, track) => (track.audioBuffer && track.audioBuffer.duration > max) ? track.audioBuffer.duration : max, 0);
|
||||
if (totalElapsedTime >= maxDuration && maxDuration > 0) {
|
||||
stopAudioEditorPlayback();
|
||||
appState.audioEditorPlaybackTime = 0;
|
||||
resetPlayheadVisual();
|
||||
return;
|
||||
}
|
||||
updatePlayheadVisual(newPositionPx);
|
||||
appState.audioEditorAnimationId = requestAnimationFrame(animationLoop);
|
||||
}
|
||||
|
||||
// --- LÓGICA DE REPRODUÇÃO ATUALIZADA ---
|
||||
export function startAudioEditorPlayback() {
|
||||
if (appState.isAudioEditorPlaying || appState.audioTracks.length === 0) return;
|
||||
initializeAudioContext();
|
||||
|
||||
appState.isAudioEditorPlaying = true;
|
||||
appState.activeAudioSources = [];
|
||||
updateAudioEditorUI();
|
||||
|
||||
const startTime = audioContext.currentTime;
|
||||
appState.audioEditorStartTime = startTime;
|
||||
|
||||
// Verifica se existe alguma faixa no modo "solo"
|
||||
const isAnyTrackSoloed = appState.audioTracks.some(t => t.isSoloed);
|
||||
|
||||
appState.audioTracks.forEach(track => {
|
||||
// Condições para tocar:
|
||||
// 1. A faixa deve ter um buffer de áudio.
|
||||
// 2. A faixa não pode estar mutada.
|
||||
const canPlay = track.audioBuffer && !track.isMuted;
|
||||
|
||||
// 3. Lógica de solo:
|
||||
// - Se houver alguma faixa solada, esta faixa TAMBÉM deve estar solada.
|
||||
// - Se NENHUMA faixa estiver solada, todas podem tocar.
|
||||
const shouldPlay = isAnyTrackSoloed ? track.isSoloed : true;
|
||||
|
||||
if (canPlay && shouldPlay) {
|
||||
if (appState.audioEditorPlaybackTime >= track.audioBuffer.duration) return;
|
||||
|
||||
const source = audioContext.createBufferSource();
|
||||
source.buffer = track.audioBuffer;
|
||||
source.connect(track.gainNode);
|
||||
source.start(startTime, appState.audioEditorPlaybackTime);
|
||||
appState.activeAudioSources.push(source);
|
||||
}
|
||||
});
|
||||
|
||||
if (appState.activeAudioSources.length > 0) {
|
||||
if (appState.audioEditorAnimationId) {
|
||||
cancelAnimationFrame(appState.audioEditorAnimationId);
|
||||
}
|
||||
animationLoop();
|
||||
} else {
|
||||
appState.isAudioEditorPlaying = false;
|
||||
updateAudioEditorUI();
|
||||
}
|
||||
}
|
||||
|
||||
export function stopAudioEditorPlayback() {
|
||||
if (!appState.isAudioEditorPlaying) return;
|
||||
const elapsedTime = (audioContext.currentTime - appState.audioEditorStartTime);
|
||||
appState.audioEditorPlaybackTime += elapsedTime;
|
||||
if (appState.audioEditorAnimationId) {
|
||||
cancelAnimationFrame(appState.audioEditorAnimationId);
|
||||
appState.audioEditorAnimationId = null;
|
||||
}
|
||||
appState.activeAudioSources.forEach(source => {
|
||||
try {
|
||||
source.stop(0);
|
||||
} catch (e) { /* Ignora erros */ }
|
||||
});
|
||||
appState.activeAudioSources = [];
|
||||
appState.isAudioEditorPlaying = false;
|
||||
updateAudioEditorUI();
|
||||
}
|
||||
|
||||
export function seekAudioEditor(newTime) {
|
||||
const wasPlaying = appState.isAudioEditorPlaying;
|
||||
if (wasPlaying) {
|
||||
stopAudioEditorPlayback();
|
||||
}
|
||||
appState.audioEditorPlaybackTime = newTime;
|
||||
const bpm = parseInt(document.getElementById("bpm-input").value, 10) || 120;
|
||||
const stepsPerSecond = (bpm / 60) * 4;
|
||||
const pixelsPerSecond = stepsPerSecond * PIXELS_PER_STEP;
|
||||
const newPositionPx = newTime * pixelsPerSecond;
|
||||
updatePlayheadVisual(newPositionPx);
|
||||
if (wasPlaying) {
|
||||
startAudioEditorPlayback();
|
||||
}
|
||||
}
|
||||
@@ -7,3 +7,8 @@ export const NOTE_LENGTH = 12;
|
||||
// Constantes para os valores padrão dos knobs
|
||||
export const DEFAULT_VOLUME = 0.8;
|
||||
export const DEFAULT_PAN = 0.0;
|
||||
|
||||
// --- ADICIONADO ---
|
||||
// Constantes para o layout do editor de áudio
|
||||
export const PIXELS_PER_STEP = 32; // Cada step (1/16) terá 32px de largura
|
||||
export const PIXELS_PER_BAR = 512; // 16 steps * 32px/step = 512px por compasso (bar)
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
initializeAudioContext,
|
||||
updateMasterVolume,
|
||||
updateMasterPan,
|
||||
startAudioEditorPlayback,
|
||||
stopAudioEditorPlayback,
|
||||
} from "./audio.js";
|
||||
import { handleFileLoad, generateMmpFile } from "./file.js";
|
||||
import {
|
||||
@@ -33,6 +35,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const removeInstrumentBtn = document.getElementById("remove-instrument-btn");
|
||||
const playBtn = document.getElementById("play-btn");
|
||||
const stopBtn = document.getElementById("stop-btn");
|
||||
const audioEditorPlayBtn = document.getElementById("audio-editor-play-btn");
|
||||
const audioEditorStopBtn = document.getElementById("audio-editor-stop-btn");
|
||||
const rewindBtn = document.getElementById("rewind-btn");
|
||||
const metronomeBtn = document.getElementById("metronome-btn");
|
||||
const mmpFileInput = document.getElementById("mmp-file-input");
|
||||
@@ -248,6 +252,17 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Listeners para os controles do editor de áudio
|
||||
audioEditorPlayBtn.addEventListener("click", () => {
|
||||
if (appState.isAudioEditorPlaying) {
|
||||
stopAudioEditorPlayback();
|
||||
} else {
|
||||
startAudioEditorPlayback();
|
||||
}
|
||||
});
|
||||
|
||||
audioEditorStopBtn.addEventListener("click", stopAudioEditorPlayback);
|
||||
|
||||
loadAndRenderSampleBrowser();
|
||||
renderApp();
|
||||
setupMasterKnobs();
|
||||
|
||||
@@ -5,18 +5,25 @@ import {
|
||||
getAudioContext,
|
||||
getMainGainNode,
|
||||
} from "./audio.js";
|
||||
import { renderApp } from "./ui.js";
|
||||
import { renderApp, renderAudioEditor } from "./ui.js";
|
||||
import { getTotalSteps } from "./utils.js";
|
||||
|
||||
export let appState = {
|
||||
tracks: [],
|
||||
audioTracks: [],
|
||||
activeTrackId: null,
|
||||
activePatternIndex: 0, // <-- VOLTOU A SER GLOBAL
|
||||
activePatternIndex: 0,
|
||||
isPlaying: false,
|
||||
isAudioEditorPlaying: false,
|
||||
activeAudioSources: [],
|
||||
audioEditorStartTime: 0,
|
||||
audioEditorAnimationId: null,
|
||||
audioEditorPlaybackTime: 0,
|
||||
playbackIntervalId: null,
|
||||
currentStep: 0,
|
||||
metronomeEnabled: false,
|
||||
originalXmlDoc: null,
|
||||
currentBeatBasslineName: 'Novo Projeto',
|
||||
masterVolume: DEFAULT_VOLUME,
|
||||
masterPan: DEFAULT_PAN,
|
||||
};
|
||||
@@ -37,6 +44,54 @@ export async function loadAudioForTrack(track) {
|
||||
return track;
|
||||
}
|
||||
|
||||
export function addAudioTrack(samplePath) {
|
||||
initializeAudioContext();
|
||||
const audioContext = getAudioContext();
|
||||
const mainGainNode = getMainGainNode();
|
||||
|
||||
const newAudioTrack = {
|
||||
id: Date.now() + Math.random(),
|
||||
name: samplePath.split('/').pop(),
|
||||
samplePath: samplePath,
|
||||
audioBuffer: null,
|
||||
volume: DEFAULT_VOLUME,
|
||||
pan: DEFAULT_PAN,
|
||||
isMuted: false,
|
||||
isSoloed: false, // <-- ADICIONADO: Começa como não-solada
|
||||
gainNode: audioContext.createGain(),
|
||||
pannerNode: audioContext.createStereoPanner(),
|
||||
};
|
||||
|
||||
newAudioTrack.gainNode.connect(newAudioTrack.pannerNode);
|
||||
newAudioTrack.pannerNode.connect(mainGainNode);
|
||||
newAudioTrack.gainNode.gain.value = newAudioTrack.volume;
|
||||
newAudioTrack.pannerNode.pan.value = newAudioTrack.pan;
|
||||
|
||||
appState.audioTracks.push(newAudioTrack);
|
||||
|
||||
loadAudioForTrack(newAudioTrack).then(() => {
|
||||
renderAudioEditor();
|
||||
});
|
||||
}
|
||||
|
||||
// A função de mute agora será a de solo.
|
||||
export function toggleAudioTrackSolo(trackId) {
|
||||
const track = appState.audioTracks.find(t => t.id == trackId);
|
||||
if (track) {
|
||||
track.isSoloed = !track.isSoloed;
|
||||
renderAudioEditor(); // Re-renderiza para mostrar a nova cor
|
||||
}
|
||||
}
|
||||
|
||||
// Mantemos a função de mute caso precise no futuro, mas ela não está conectada ao botão.
|
||||
export function toggleAudioTrackMute(trackId) {
|
||||
const track = appState.audioTracks.find(t => t.id == trackId);
|
||||
if (track) {
|
||||
track.isMuted = !track.isMuted;
|
||||
renderAudioEditor();
|
||||
}
|
||||
}
|
||||
|
||||
export function addTrackToState() {
|
||||
initializeAudioContext();
|
||||
const audioContext = getAudioContext();
|
||||
@@ -52,7 +107,7 @@ export function addTrackToState() {
|
||||
patterns: referenceTrack
|
||||
? referenceTrack.patterns.map(p => ({ name: p.name, steps: new Array(p.steps.length).fill(false), pos: p.pos }))
|
||||
: [{ name: "Pattern 1", steps: new Array(totalSteps).fill(false), pos: 0 }],
|
||||
// activePatternIndex foi removido daqui
|
||||
activePatternIndex: 0,
|
||||
volume: DEFAULT_VOLUME,
|
||||
pan: DEFAULT_PAN,
|
||||
gainNode: audioContext.createGain(),
|
||||
@@ -64,18 +119,12 @@ export function addTrackToState() {
|
||||
newTrack.pannerNode.pan.value = newTrack.pan;
|
||||
|
||||
appState.tracks.push(newTrack);
|
||||
if (!appState.activeTrackId) {
|
||||
appState.activeTrackId = newTrack.id;
|
||||
}
|
||||
renderApp();
|
||||
}
|
||||
|
||||
export function removeLastTrackFromState() {
|
||||
if (appState.tracks.length > 0) {
|
||||
const removedTrack = appState.tracks.pop();
|
||||
if (appState.activeTrackId === removedTrack.id) {
|
||||
appState.activeTrackId = appState.tracks[0]?.id || null;
|
||||
}
|
||||
appState.tracks.pop();
|
||||
renderApp();
|
||||
}
|
||||
}
|
||||
@@ -87,18 +136,14 @@ export async function updateTrackSample(trackId, samplePath) {
|
||||
track.name = samplePath.split("/").pop();
|
||||
track.audioBuffer = null;
|
||||
await loadAudioForTrack(track);
|
||||
const trackLane = document.querySelector(`.track-lane[data-track-id="${trackId}"] .track-name`);
|
||||
if (trackLane) {
|
||||
trackLane.textContent = track.name;
|
||||
}
|
||||
renderApp();
|
||||
}
|
||||
}
|
||||
|
||||
export function toggleStepState(trackId, stepIndex) {
|
||||
const track = appState.tracks.find((t) => t.id == trackId);
|
||||
if (track && track.patterns && track.patterns.length > 0) {
|
||||
// Usa o índice GLOBAL para saber qual pattern modificar
|
||||
const activePattern = track.patterns[appState.activePatternIndex];
|
||||
const activePattern = track.patterns[track.activePatternIndex];
|
||||
if (activePattern && activePattern.steps.length > stepIndex) {
|
||||
activePattern.steps[stepIndex] = !activePattern.steps[stepIndex];
|
||||
}
|
||||
@@ -106,7 +151,7 @@ export function toggleStepState(trackId, stepIndex) {
|
||||
}
|
||||
|
||||
export function updateTrackVolume(trackId, volume) {
|
||||
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) {
|
||||
const clampedVolume = Math.max(0, Math.min(1.5, volume));
|
||||
track.volume = clampedVolume;
|
||||
@@ -117,7 +162,7 @@ export function updateTrackVolume(trackId, volume) {
|
||||
}
|
||||
|
||||
export function updateTrackPan(trackId, pan) {
|
||||
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) {
|
||||
const clampedPan = Math.max(-1, Math.min(1, pan));
|
||||
track.pan = clampedPan;
|
||||
|
||||
+168
-24
@@ -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';
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// js/waveform.js
|
||||
|
||||
/**
|
||||
* Desenha a forma de onda de um AudioBuffer em um elemento Canvas.
|
||||
* @param {HTMLCanvasElement} canvas - O elemento canvas onde o desenho será feito.
|
||||
* @param {AudioBuffer} audioBuffer - O buffer de áudio decodificado da faixa.
|
||||
* @param {string} color - A cor da forma de onda (ex: '#2ecc71').
|
||||
*/
|
||||
export function drawWaveform(canvas, audioBuffer, color) {
|
||||
if (!canvas || !audioBuffer) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
const channelData = audioBuffer.getChannelData(0); // Pega os dados do primeiro canal
|
||||
const step = Math.ceil(channelData.length / width);
|
||||
const amp = height / 2; // Amplitude máxima do desenho
|
||||
|
||||
ctx.clearRect(0, 0, width, height); // Limpa o canvas
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
|
||||
for (let i = 0; i < width; i++) {
|
||||
let min = 1.0;
|
||||
let max = -1.0;
|
||||
|
||||
// Encontra o valor mínimo e máximo para um bloco de amostras
|
||||
for (let j = 0; j < step; j++) {
|
||||
const datum = channelData[(i * step) + j];
|
||||
if (datum < min) {
|
||||
min = datum;
|
||||
}
|
||||
if (datum > max) {
|
||||
max = datum;
|
||||
}
|
||||
}
|
||||
|
||||
// Desenha a linha vertical para aquele ponto no tempo
|
||||
const x = i;
|
||||
const y_max = (1 + max) * amp;
|
||||
const y_min = (1 + min) * amp;
|
||||
|
||||
ctx.moveTo(x, y_max);
|
||||
ctx.lineTo(x, y_min);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
Reference in new issue
Block a user