#include "WaveformDisplay.h" WaveformDisplay::WaveformDisplay(SliceManager& sm) : sliceManager(sm) { addAndMakeVisible(hScrollBar); hScrollBar.addListener(this); hScrollBar.setRangeLimits(0.0, 1.0); hScrollBar.setCurrentRange(0.0, 1.0); startTimerHz(30); } WaveformDisplay::~WaveformDisplay() { stopTimer(); hScrollBar.removeListener(this); } void WaveformDisplay::timerCallback() { repaint(); } void WaveformDisplay::setSampleBuffer(const juce::AudioBuffer* buffer, double sr) { sampleBuffer = buffer; sampleRate = sr; zoomFactor = 1.0f; scrollOffset = 0.0f; overviewDirty = true; rebuildOverview(); syncScrollBar(); repaint(); } void WaveformDisplay::setPlaybackPosition(int sample) { playbackSample = sample; } void WaveformDisplay::setActiveSliceIndex(int index) { activeSliceIndex = index; } void WaveformDisplay::setSelectedSlice(int index) { selectedSliceIndex = index; } void WaveformDisplay::setOnEmptyAreaClick(std::function callback) { onEmptyAreaClick = std::move(callback); } void WaveformDisplay::setOnKeyClick(std::function callback) { onKeyClick = std::move(callback); } void WaveformDisplay::zoomIn() { zoomFactor *= 1.5f; zoomFactor = juce::jlimit(1.0f, 64.0f, zoomFactor); syncScrollBar(); repaint(); } void WaveformDisplay::zoomOut() { zoomFactor /= 1.5f; zoomFactor = juce::jlimit(1.0f, 64.0f, zoomFactor); syncScrollBar(); repaint(); } void WaveformDisplay::zoomReset() { zoomFactor = 1.0f; scrollOffset = 0.0f; syncScrollBar(); repaint(); } void WaveformDisplay::scrollLeft() { scrollOffset -= 1.0f / zoomFactor; scrollOffset = juce::jlimit(0.0f, 1.0f, scrollOffset); syncScrollBar(); repaint(); } void WaveformDisplay::scrollRight() { scrollOffset += 1.0f / zoomFactor; scrollOffset = juce::jlimit(0.0f, 1.0f, scrollOffset); syncScrollBar(); repaint(); } void WaveformDisplay::syncScrollBar() { if (updatingScroll) return; updatingScroll = true; float visibleRatio = 1.0f / zoomFactor; hScrollBar.setCurrentRange(scrollOffset, visibleRatio); updatingScroll = false; } void WaveformDisplay::scrollBarMoved(juce::ScrollBar*, double) { if (updatingScroll) return; updatingScroll = true; scrollOffset = static_cast(hScrollBar.getCurrentRangeStart()); scrollOffset = juce::jlimit(0.0f, 1.0f, scrollOffset); updatingScroll = false; repaint(); } void WaveformDisplay::rebuildOverview() { if (!sampleBuffer || sampleBuffer->getNumSamples() == 0) return; const int numSamples = sampleBuffer->getNumSamples(); const int numChannels = sampleBuffer->getNumChannels(); const int blockSize = juce::jmax(1, numSamples / overviewResolution); overviewBuffer.setSize(2, overviewResolution, false, false, false); for (int i = 0; i < overviewResolution; ++i) { int start = i * blockSize; int end = juce::jmin(start + blockSize, numSamples); float minVal = 0.0f, maxVal = 0.0f; for (int s = start; s < end; ++s) { float avg = 0.0f; for (int ch = 0; ch < numChannels; ++ch) avg += sampleBuffer->getSample(ch, s); avg /= static_cast(juce::jmax(1, numChannels)); minVal = juce::jmin(minVal, avg); maxVal = juce::jmax(maxVal, avg); } overviewBuffer.setSample(0, i, minVal); overviewBuffer.setSample(1, i, maxVal); } overviewDirty = false; } juce::Rectangle WaveformDisplay::getWaveformArea() const { auto b = getLocalBounds().toFloat(); return b.withTrimmedLeft(keyWidth).withTrimmedBottom(scrollbarHeight); } int WaveformDisplay::getVisibleStart() const { int total = getNumSamples(); int visible = static_cast(static_cast(total) / zoomFactor); return static_cast(scrollOffset * static_cast(juce::jmax(0, total - visible))); } int WaveformDisplay::getVisibleEnd() const { int total = getNumSamples(); int visible = static_cast(static_cast(total) / zoomFactor); return juce::jmin(total, getVisibleStart() + visible); } int WaveformDisplay::xToSample(int x) const { auto area = getWaveformArea(); float ratio = static_cast(x - static_cast(area.getX())) / area.getWidth(); ratio = juce::jlimit(0.0f, 1.0f, ratio); int start = getVisibleStart(); int end = getVisibleEnd(); return start + static_cast(ratio * static_cast(end - start)); } int WaveformDisplay::sampleToX(int sample) const { auto area = getWaveformArea(); int start = getVisibleStart(); int end = getVisibleEnd(); int range = end - start; if (range <= 0) return static_cast(area.getX()); float ratio = static_cast(sample - start) / static_cast(range); return static_cast(area.getX() + ratio * area.getWidth()); } int WaveformDisplay::keyRowToMidiNote(int row) const { const int numRows = 25; const int baseMidiNote = 60; return baseMidiNote + (numRows - 1 - row); } int WaveformDisplay::midiNoteToSliceIndex(int note) const { const auto& slices = sliceManager.getSlices(); int numSlices = sliceManager.getNumSlices(); for (int i = 0; i < numSlices; ++i) { if (slices[static_cast(i)].midiNote == note) return i; } return -1; } int WaveformDisplay::findNearestSliceAtPixel(int pixelX, int tolerancePx) const { const auto& slices = sliceManager.getSlices(); int bestIdx = -1; int bestDist = tolerancePx + 1; for (int i = 0; i < static_cast(slices.size()); ++i) { int markerX = sampleToX(slices[static_cast(i)].startSample); int dist = std::abs(pixelX - markerX); if (dist < bestDist) { bestDist = dist; bestIdx = i; } } return bestIdx; } void WaveformDisplay::paint(juce::Graphics& g) { auto bounds = getLocalBounds().toFloat(); g.fillAll(juce::Colour(0xff1a1a2e)); if (!sampleBuffer || sampleBuffer->getNumSamples() == 0) { auto textArea = bounds.withTrimmedBottom(scrollbarHeight); g.setColour(juce::Colours::grey); g.setFont(14.0f); g.drawText("No audio file loaded.", textArea, juce::Justification::centred); return; } auto waveBounds = bounds.withTrimmedBottom(scrollbarHeight); // Ruler across full width (drawn first so keys can overlap) drawRuler(g, waveBounds.removeFromTop(20.0f)); // Left key area (matches piano roll) drawKeys(g, waveBounds.withWidth(keyWidth)); // Waveform and markers in the area to the right of keys auto waveArea = waveBounds.withTrimmedLeft(keyWidth); drawWaveform(g, waveArea); drawSliceMarkers(g, waveArea); } void WaveformDisplay::drawKeys(juce::Graphics& g, juce::Rectangle bounds) { g.setColour(juce::Colour(0xff222233)); g.fillRect(bounds); const int numRows = 25; float rowHeight = bounds.getHeight() / static_cast(numRows); juce::Colour markerColours[] = { juce::Colour(0xffff6b6b), juce::Colour(0xff4ecdc4), juce::Colour(0xffffe66d), juce::Colour(0xffa8e6cf), juce::Colour(0xff8b94ff), juce::Colour(0xffff8b94), juce::Colour(0xffdda0dd), juce::Colour(0xff87ceeb), juce::Colour(0xff98fb98), juce::Colour(0xffffb347), juce::Colour(0xffc39bd3), juce::Colour(0xff76d7c4) }; for (int row = 0; row < numRows; ++row) { float y = bounds.getY() + static_cast(row) * rowHeight; int noteInOctave = (numRows - 1 - row) % 12; bool isBlack = (noteInOctave == 1 || noteInOctave == 3 || noteInOctave == 6 || noteInOctave == 8 || noteInOctave == 10); int midiNote = keyRowToMidiNote(row); juce::Colour baseCol = isBlack ? juce::Colour(0xff333344) : juce::Colour(0xff555566); int sliceIdx = midiNoteToSliceIndex(midiNote); if (sliceIdx >= 0 && sliceIdx == activeSliceIndex) { baseCol = markerColours[sliceIdx % 12].withAlpha(0.25f); } g.setColour(baseCol); g.fillRect(bounds.getX(), y, bounds.getWidth(), rowHeight); g.setColour(juce::Colour(0xffaaaaaa)); g.drawRect(bounds.getX(), y, bounds.getWidth(), rowHeight, 0.5f); if (sliceIdx >= 0) { float dotR = rowHeight * 0.2f; float dotX = bounds.getX() + bounds.getWidth() * 0.5f; float dotY = y + rowHeight * 0.5f; g.setColour(markerColours[sliceIdx % 12].withAlpha(0.7f)); g.fillEllipse(dotX - dotR, dotY - dotR, dotR * 2.0f, dotR * 2.0f); } const char* noteNames[] = { "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" }; int noteIdx = midiNote % 12; int octave = midiNote / 12 - 1; juce::String name = juce::String(noteNames[noteIdx]) + juce::String(octave); g.setColour(juce::Colours::white.withAlpha(0.6f)); g.setFont(juce::Font(juce::FontOptions(8.0f))); g.drawText(name, static_cast(bounds.getX() + 2.0f), static_cast(y + 1.0f), static_cast(bounds.getWidth() - 4.0f), static_cast(rowHeight - 2.0f), juce::Justification::centredRight); } } void WaveformDisplay::drawWaveform(juce::Graphics& g, juce::Rectangle bounds) { if (!sampleBuffer || sampleBuffer->getNumSamples() == 0) return; int start = getVisibleStart(); int end = getVisibleEnd(); int total = getNumSamples(); float w = bounds.getWidth(); float h = bounds.getHeight(); float midY = bounds.getY() + h * 0.5f; // Highlight active slice region if (activeSliceIndex >= 0 && activeSliceIndex < sliceManager.getNumSlices()) { const auto& slice = sliceManager.getSlice(activeSliceIndex); int sStart = juce::jmax(slice.startSample, start); int sEnd = juce::jmin(slice.endSample, end); if (sEnd > sStart) { float x1 = bounds.getX() + static_cast(sStart - start) / static_cast(end - start) * w; float x2 = bounds.getX() + static_cast(sEnd - start) / static_cast(end - start) * w; juce::Colour markerColours[] = { juce::Colour(0xffff6b6b), juce::Colour(0xff4ecdc4), juce::Colour(0xffffe66d), juce::Colour(0xffa8e6cf), juce::Colour(0xffff8b94), juce::Colour(0xffdda0dd) }; g.setColour(markerColours[activeSliceIndex % 6].withAlpha(0.12f)); g.fillRect(x1, bounds.getY(), x2 - x1, h); } } // Highlight selected slice region (subtler than active) if (selectedSliceIndex >= 0 && selectedSliceIndex < sliceManager.getNumSlices() && selectedSliceIndex != activeSliceIndex) { const auto& slice = sliceManager.getSlice(selectedSliceIndex); int sStart = juce::jmax(slice.startSample, start); int sEnd = juce::jmin(slice.endSample, end); if (sEnd > sStart) { float x1 = bounds.getX() + static_cast(sStart - start) / static_cast(end - start) * w; float x2 = bounds.getX() + static_cast(sEnd - start) / static_cast(end - start) * w; juce::Colour markerColours[] = { juce::Colour(0xffff6b6b), juce::Colour(0xff4ecdc4), juce::Colour(0xffffe66d), juce::Colour(0xffa8e6cf), juce::Colour(0xffff8b94), juce::Colour(0xffdda0dd) }; g.setColour(markerColours[selectedSliceIndex % 6].withAlpha(0.07f)); g.fillRect(x1, bounds.getY(), x2 - x1, h); } } // Draw waveform directly from sample buffer for crisp rendering const int numChannels = sampleBuffer->getNumChannels(); int visibleRange = end - start; if (visibleRange <= 0) return; juce::Path filledPath; for (int x = 0; x < static_cast(w); ++x) { float ratio = static_cast(x) / w; int s0 = start + static_cast(ratio * static_cast(visibleRange)); int s1 = start + static_cast((ratio + 1.0f / w) * static_cast(visibleRange)); s0 = juce::jlimit(0, total - 1, s0); s1 = juce::jlimit(0, total, s1); if (s1 <= s0) s1 = s0 + 1; float maxVal = 0.0f; for (int s = s0; s < s1; ++s) { for (int ch = 0; ch < numChannels; ++ch) { float val = std::abs(sampleBuffer->getSample(ch, s)); if (val > maxVal) maxVal = val; } } float yMin = midY - maxVal * h * 0.45f; if (x == 0) filledPath.startNewSubPath(bounds.getX(), yMin); filledPath.lineTo(bounds.getX() + static_cast(x), yMin); } for (int x = static_cast(w) - 1; x >= 0; --x) { float ratio = static_cast(x) / w; int s0 = start + static_cast(ratio * static_cast(visibleRange)); int s1 = start + static_cast((ratio + 1.0f / w) * static_cast(visibleRange)); s0 = juce::jlimit(0, total - 1, s0); s1 = juce::jlimit(0, total, s1); if (s1 <= s0) s1 = s0 + 1; float minVal = 0.0f; for (int s = s0; s < s1; ++s) { for (int ch = 0; ch < numChannels; ++ch) { float val = sampleBuffer->getSample(ch, s); if (val < minVal) minVal = val; } } float yMax = midY - minVal * h * 0.45f; filledPath.lineTo(bounds.getX() + static_cast(x), yMax); } filledPath.closeSubPath(); juce::ColourGradient gradient(juce::Colour(0xff4488ff), bounds.getX(), bounds.getY(), juce::Colour(0xff2244aa), bounds.getX(), bounds.getBottom(), false); g.setGradientFill(gradient); g.fillPath(filledPath); // Center line g.setColour(juce::Colour(0x40ffffff)); g.drawHorizontalLine(static_cast(midY), bounds.getX(), bounds.getRight()); // Playback position if (playbackSample >= start && playbackSample < end) { float px = bounds.getX() + static_cast(playbackSample - start) / static_cast(end - start) * w; g.setColour(juce::Colours::white); g.drawVerticalLine(static_cast(px), bounds.getY(), bounds.getBottom()); } } void WaveformDisplay::drawSliceMarkers(juce::Graphics& g, juce::Rectangle bounds) { const auto& slices = sliceManager.getSlices(); float w = bounds.getWidth(); int start = getVisibleStart(); int end = getVisibleEnd(); juce::Colour markerColours[] = { juce::Colour(0xffff6b6b), juce::Colour(0xff4ecdc4), juce::Colour(0xffffe66d), juce::Colour(0xffa8e6cf), juce::Colour(0xffff8b94), juce::Colour(0xffdda0dd) }; for (int i = 0; i < static_cast(slices.size()); ++i) { int sPos = slices[static_cast(i)].startSample; if (sPos < start || sPos > end) continue; float px = bounds.getX() + static_cast(sPos - start) / static_cast(end - start) * w; juce::Colour col = markerColours[i % 6]; bool isActive = (i == activeSliceIndex); bool isSelected = (i == selectedSliceIndex); bool isHovered = (i == hoveredSliceIndex); if (isHovered && !isActive) { g.setColour(col.brighter(0.6f)); g.drawLine(px, bounds.getY(), px, bounds.getBottom(), 3.0f); } else if (isSelected && !isActive) { g.setColour(col.brighter(0.4f)); float lineThickness = 2.0f; float y = bounds.getY(); float bottom = bounds.getBottom(); while (y < bottom) { float dashEnd = juce::jmin(y + 6.0f, bottom); g.drawLine(px, y, px, dashEnd, lineThickness); y += 10.0f; } } else { g.setColour(isActive ? col : col.withAlpha(0.7f)); float lineThickness = isActive ? 2.0f : 1.0f; g.drawLine(px, bounds.getY(), px, bounds.getBottom(), lineThickness); } // Label g.setColour(col); g.setFont(isActive ? juce::Font(juce::FontOptions(11.0f, juce::Font::bold)) : juce::Font(juce::FontOptions(10.0f))); g.drawText(juce::String(i + 1), static_cast(px + 2.0f), static_cast(bounds.getY()), 20, 14, juce::Justification::centredLeft); // Triangle at top float triSize = (isActive || isSelected) ? 5.0f : 4.0f; juce::Colour triCol = isSelected ? col.brighter(0.3f) : col; g.setColour(isActive ? col : triCol); juce::Path tri; tri.addTriangle(px - triSize, bounds.getY(), px + triSize, bounds.getY(), px, bounds.getY() + triSize * 2.0f); g.fillPath(tri); } } void WaveformDisplay::drawRuler(juce::Graphics& g, juce::Rectangle bounds) { g.setColour(juce::Colour(0xff2a2a4a)); g.fillRect(bounds); // Key area in ruler too g.setColour(juce::Colour(0xff222233)); g.fillRect(bounds.withWidth(keyWidth)); if (!sampleBuffer || sampleRate <= 0) return; int start = getVisibleStart(); int end = getVisibleEnd(); float w = bounds.getWidth() - keyWidth; float offsetX = keyWidth; g.setColour(juce::Colour(0xff888888)); g.setFont(9.0f); double visibleSeconds = static_cast(end - start) / sampleRate; double tickInterval = 0.1; if (visibleSeconds > 5.0) tickInterval = 1.0; else if (visibleSeconds > 2.0) tickInterval = 0.5; else if (visibleSeconds > 0.5) tickInterval = 0.1; else tickInterval = 0.01; double startSec = static_cast(start) / sampleRate; double firstTick = std::ceil(startSec / tickInterval) * tickInterval; for (double t = firstTick; t < static_cast(end) / sampleRate; t += tickInterval) { int samp = static_cast(t * sampleRate); float px = bounds.getX() + offsetX + static_cast(samp - start) / static_cast(end - start) * w; g.drawVerticalLine(static_cast(px), bounds.getY() + 12.0f, bounds.getBottom()); juce::String label = juce::String(t, 2) + "s"; g.drawText(label, static_cast(px + 2.0f), static_cast(bounds.getY()), 40, 12, juce::Justification::centredLeft); } } void WaveformDisplay::resized() { auto bounds = getLocalBounds(); auto scrollBarRow = bounds.removeFromBottom(static_cast(scrollbarHeight)); hScrollBar.setBounds(scrollBarRow); } void WaveformDisplay::mouseDown(const juce::MouseEvent& e) { if (!sampleBuffer || sampleBuffer->getNumSamples() == 0) { if (onEmptyAreaClick) onEmptyAreaClick(); return; } auto bounds = getLocalBounds().toFloat(); auto keyArea = bounds.withWidth(keyWidth).withTrimmedBottom(scrollbarHeight).withTrimmedTop(20.0f); // Check if click is in the key area if (e.getPosition().getX() < static_cast(keyArea.getWidth())) { const int numRows = 25; float rowHeight = keyArea.getHeight() / static_cast(numRows); int row = static_cast((static_cast(e.getPosition().getY()) - keyArea.getY()) / rowHeight); row = juce::jlimit(0, numRows - 1, row); int midiNote = keyRowToMidiNote(row); if (onKeyClick) onKeyClick(midiNote); return; } // Right click removes slice if (e.mods.isRightButtonDown()) { int sample = xToSample(static_cast(e.getPosition().getX())); auto area = getWaveformArea(); int range = getVisibleEnd() - getVisibleStart(); int tolerance = (range > 0) ? juce::jmax(1, static_cast(10.0f * static_cast(range) / area.getWidth())) : 1; sliceManager.removeSliceAt(sample, tolerance); return; } // Left click on waveform area if (e.mods.isLeftButtonDown()) { int px = static_cast(e.getPosition().getX()); int sample = xToSample(px); auto area = getWaveformArea(); int range = getVisibleEnd() - getVisibleStart(); int tolerancePx = 10; int toleranceSamples = (range > 0) ? juce::jmax(1, static_cast(static_cast(tolerancePx) * static_cast(range) / area.getWidth())) : 1; // Check if clicking near an existing marker — drag it const auto& slices = sliceManager.getSlices(); int nearIdx = -1; int nearDist = toleranceSamples + 1; for (int i = 0; i < static_cast(slices.size()); ++i) { int dist = std::abs(slices[static_cast(i)].startSample - sample); if (dist < nearDist) { nearDist = dist; nearIdx = i; } } if (nearIdx >= 0) { draggingSliceIndex = nearIdx; } else { sliceManager.addSliceManual(sample); // Enter drag mode for the newly placed marker const auto& slices2 = sliceManager.getSlices(); int bestIdx = -1; int bestDist = std::numeric_limits::max(); for (int i = 0; i < static_cast(slices2.size()); ++i) { int dist = std::abs(slices2[static_cast(i)].startSample - sample); if (dist < bestDist) { bestDist = dist; bestIdx = i; } } draggingSliceIndex = bestIdx; } } } void WaveformDisplay::mouseDrag(const juce::MouseEvent& e) { if (draggingSliceIndex >= 0) { int sample = xToSample(static_cast(e.getPosition().getX())); const auto& slices = sliceManager.getSlices(); if (draggingSliceIndex < static_cast(slices.size())) sliceManager.moveSlice(slices[static_cast(draggingSliceIndex)].startSample, sample); } } void WaveformDisplay::mouseUp(const juce::MouseEvent&) { draggingSliceIndex = -1; } void WaveformDisplay::mouseMove(const juce::MouseEvent& e) { if (!sampleBuffer || sampleBuffer->getNumSamples() == 0) { hoveredSliceIndex = -1; return; } auto area = getWaveformArea(); if (e.getPosition().getX() < static_cast(area.getX()) || e.getPosition().getX() > static_cast(area.getRight())) { hoveredSliceIndex = -1; return; } int range = getVisibleEnd() - getVisibleStart(); int tolerancePx = 10; int toleranceSamples = (range > 0) ? juce::jmax(1, static_cast(static_cast(tolerancePx) * static_cast(range) / area.getWidth())) : 1; int sample = xToSample(static_cast(e.getPosition().getX())); const auto& slices = sliceManager.getSlices(); int bestIdx = -1; int bestDist = toleranceSamples + 1; for (int i = 0; i < static_cast(slices.size()); ++i) { int dist = std::abs(slices[static_cast(i)].startSample - sample); if (dist < bestDist) { bestDist = dist; bestIdx = i; } } hoveredSliceIndex = bestIdx; } void WaveformDisplay::mouseWheelMove(const juce::MouseEvent& e, const juce::MouseWheelDetails& wheel) { auto area = getWaveformArea(); // Handle horizontal scrolling (trackpad 2-finger horizontal drag) if (std::abs(wheel.deltaX) > 0.001f) { scrollOffset -= wheel.deltaX * 0.02f / zoomFactor; scrollOffset = juce::jlimit(0.0f, 1.0f, scrollOffset); syncScrollBar(); repaint(); return; } // Handle zoom via vertical scroll float oldZoom = zoomFactor; zoomFactor *= (1.0f + wheel.deltaY * 0.3f); zoomFactor = juce::jlimit(1.0f, 64.0f, zoomFactor); float mouseXRatio = (static_cast(e.getPosition().getX()) - area.getX()) / area.getWidth(); mouseXRatio = juce::jlimit(0.0f, 1.0f, mouseXRatio); int oldVisible = static_cast(static_cast(getNumSamples()) / oldZoom); int newVisible = static_cast(static_cast(getNumSamples()) / zoomFactor); float mouseSample = scrollOffset * static_cast(juce::jmax(0, getNumSamples() - oldVisible)) + mouseXRatio * static_cast(oldVisible); scrollOffset = (mouseSample - mouseXRatio * static_cast(newVisible)) / static_cast(juce::jmax(1, getNumSamples() - newVisible)); scrollOffset = juce::jlimit(0.0f, 1.0f, scrollOffset); syncScrollBar(); repaint(); }