2026-07-21 02:06:45 +02:00
|
|
|
#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<float>* 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<void()> callback) { onEmptyAreaClick = std::move(callback); }
|
|
|
|
|
void WaveformDisplay::setOnKeyClick(std::function<void(int)> 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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 13:27:26 +02:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 02:06:45 +02:00
|
|
|
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<float>(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<float>(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<float> WaveformDisplay::getWaveformArea() const
|
|
|
|
|
{
|
|
|
|
|
auto b = getLocalBounds().toFloat();
|
|
|
|
|
return b.withTrimmedLeft(keyWidth).withTrimmedBottom(scrollbarHeight);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int WaveformDisplay::getVisibleStart() const
|
|
|
|
|
{
|
|
|
|
|
int total = getNumSamples();
|
|
|
|
|
int visible = static_cast<int>(static_cast<float>(total) / zoomFactor);
|
|
|
|
|
return static_cast<int>(scrollOffset * static_cast<float>(juce::jmax(0, total - visible)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int WaveformDisplay::getVisibleEnd() const
|
|
|
|
|
{
|
|
|
|
|
int total = getNumSamples();
|
|
|
|
|
int visible = static_cast<int>(static_cast<float>(total) / zoomFactor);
|
|
|
|
|
return juce::jmin(total, getVisibleStart() + visible);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int WaveformDisplay::xToSample(int x) const
|
|
|
|
|
{
|
|
|
|
|
auto area = getWaveformArea();
|
|
|
|
|
float ratio = static_cast<float>(x - static_cast<int>(area.getX())) / area.getWidth();
|
|
|
|
|
ratio = juce::jlimit(0.0f, 1.0f, ratio);
|
|
|
|
|
int start = getVisibleStart();
|
|
|
|
|
int end = getVisibleEnd();
|
|
|
|
|
return start + static_cast<int>(ratio * static_cast<float>(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<int>(area.getX());
|
|
|
|
|
float ratio = static_cast<float>(sample - start) / static_cast<float>(range);
|
|
|
|
|
return static_cast<int>(area.getX() + ratio * area.getWidth());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int WaveformDisplay::keyRowToMidiNote(int row) const
|
|
|
|
|
{
|
2026-07-22 23:10:55 +02:00
|
|
|
const int numRows = 25;
|
2026-07-21 15:20:49 +02:00
|
|
|
const int baseMidiNote = 60;
|
2026-07-21 02:06:45 +02:00
|
|
|
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)
|
|
|
|
|
{
|
2026-07-22 23:10:55 +02:00
|
|
|
if (slices[static_cast<size_t>(i)].midiNote == note)
|
2026-07-21 02:06:45 +02:00
|
|
|
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<int>(slices.size()); ++i)
|
|
|
|
|
{
|
|
|
|
|
int markerX = sampleToX(slices[static_cast<size_t>(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);
|
2026-07-22 00:33:03 +02:00
|
|
|
g.drawText("No audio file loaded.", textArea, juce::Justification::centred);
|
2026-07-21 02:06:45 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto waveBounds = bounds.withTrimmedBottom(scrollbarHeight);
|
|
|
|
|
|
2026-07-22 23:10:55 +02:00
|
|
|
// Ruler across full width (drawn first so keys can overlap)
|
|
|
|
|
drawRuler(g, waveBounds.removeFromTop(20.0f));
|
|
|
|
|
|
2026-07-21 02:06:45 +02:00
|
|
|
// 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<float> bounds)
|
|
|
|
|
{
|
|
|
|
|
g.setColour(juce::Colour(0xff222233));
|
|
|
|
|
g.fillRect(bounds);
|
|
|
|
|
|
2026-07-22 23:10:55 +02:00
|
|
|
const int numRows = 25;
|
2026-07-21 02:06:45 +02:00
|
|
|
float rowHeight = bounds.getHeight() / static_cast<float>(numRows);
|
|
|
|
|
|
2026-07-21 15:20:49 +02:00
|
|
|
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)
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-21 02:06:45 +02:00
|
|
|
for (int row = 0; row < numRows; ++row)
|
|
|
|
|
{
|
|
|
|
|
float y = bounds.getY() + static_cast<float>(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)
|
|
|
|
|
{
|
2026-07-21 15:20:49 +02:00
|
|
|
baseCol = markerColours[sliceIdx % 12].withAlpha(0.25f);
|
2026-07-21 02:06:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
2026-07-21 15:20:49 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 02:06:45 +02:00
|
|
|
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<int>(bounds.getX() + 2.0f), static_cast<int>(y + 1.0f),
|
|
|
|
|
static_cast<int>(bounds.getWidth() - 4.0f), static_cast<int>(rowHeight - 2.0f),
|
|
|
|
|
juce::Justification::centredRight);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WaveformDisplay::drawWaveform(juce::Graphics& g, juce::Rectangle<float> 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<float>(sStart - start) / static_cast<float>(end - start) * w;
|
|
|
|
|
float x2 = bounds.getX() + static_cast<float>(sEnd - start) / static_cast<float>(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<float>(sStart - start) / static_cast<float>(end - start) * w;
|
|
|
|
|
float x2 = bounds.getX() + static_cast<float>(sEnd - start) / static_cast<float>(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<int>(w); ++x)
|
|
|
|
|
{
|
|
|
|
|
float ratio = static_cast<float>(x) / w;
|
|
|
|
|
int s0 = start + static_cast<int>(ratio * static_cast<float>(visibleRange));
|
|
|
|
|
int s1 = start + static_cast<int>((ratio + 1.0f / w) * static_cast<float>(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<float>(x), yMin);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int x = static_cast<int>(w) - 1; x >= 0; --x)
|
|
|
|
|
{
|
|
|
|
|
float ratio = static_cast<float>(x) / w;
|
|
|
|
|
int s0 = start + static_cast<int>(ratio * static_cast<float>(visibleRange));
|
|
|
|
|
int s1 = start + static_cast<int>((ratio + 1.0f / w) * static_cast<float>(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<float>(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<int>(midY), bounds.getX(), bounds.getRight());
|
|
|
|
|
|
|
|
|
|
// Playback position
|
|
|
|
|
if (playbackSample >= start && playbackSample < end)
|
|
|
|
|
{
|
|
|
|
|
float px = bounds.getX() + static_cast<float>(playbackSample - start) / static_cast<float>(end - start) * w;
|
|
|
|
|
g.setColour(juce::Colours::white);
|
|
|
|
|
g.drawVerticalLine(static_cast<int>(px), bounds.getY(), bounds.getBottom());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WaveformDisplay::drawSliceMarkers(juce::Graphics& g, juce::Rectangle<float> 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<int>(slices.size()); ++i)
|
|
|
|
|
{
|
|
|
|
|
int sPos = slices[static_cast<size_t>(i)].startSample;
|
|
|
|
|
if (sPos < start || sPos > end) continue;
|
|
|
|
|
|
|
|
|
|
float px = bounds.getX() + static_cast<float>(sPos - start) / static_cast<float>(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<int>(px + 2.0f), static_cast<int>(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<float> 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<double>(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<double>(start) / sampleRate;
|
|
|
|
|
double firstTick = std::ceil(startSec / tickInterval) * tickInterval;
|
|
|
|
|
|
|
|
|
|
for (double t = firstTick; t < static_cast<double>(end) / sampleRate; t += tickInterval)
|
|
|
|
|
{
|
|
|
|
|
int samp = static_cast<int>(t * sampleRate);
|
|
|
|
|
float px = bounds.getX() + offsetX + static_cast<float>(samp - start) / static_cast<float>(end - start) * w;
|
|
|
|
|
|
|
|
|
|
g.drawVerticalLine(static_cast<int>(px), bounds.getY() + 12.0f, bounds.getBottom());
|
|
|
|
|
|
|
|
|
|
juce::String label = juce::String(t, 2) + "s";
|
|
|
|
|
g.drawText(label, static_cast<int>(px + 2.0f), static_cast<int>(bounds.getY()),
|
|
|
|
|
40, 12, juce::Justification::centredLeft);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WaveformDisplay::resized()
|
|
|
|
|
{
|
|
|
|
|
auto bounds = getLocalBounds();
|
|
|
|
|
auto scrollBarRow = bounds.removeFromBottom(static_cast<int>(scrollbarHeight));
|
2026-07-22 13:27:26 +02:00
|
|
|
hScrollBar.setBounds(scrollBarRow);
|
2026-07-21 02:06:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void WaveformDisplay::mouseDown(const juce::MouseEvent& e)
|
|
|
|
|
{
|
|
|
|
|
if (!sampleBuffer || sampleBuffer->getNumSamples() == 0)
|
|
|
|
|
{
|
|
|
|
|
if (onEmptyAreaClick)
|
|
|
|
|
onEmptyAreaClick();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto bounds = getLocalBounds().toFloat();
|
2026-07-22 23:10:55 +02:00
|
|
|
auto keyArea = bounds.withWidth(keyWidth).withTrimmedBottom(scrollbarHeight).withTrimmedTop(20.0f);
|
2026-07-21 02:06:45 +02:00
|
|
|
|
|
|
|
|
// Check if click is in the key area
|
|
|
|
|
if (e.getPosition().getX() < static_cast<int>(keyArea.getWidth()))
|
|
|
|
|
{
|
2026-07-22 23:10:55 +02:00
|
|
|
const int numRows = 25;
|
|
|
|
|
float rowHeight = keyArea.getHeight() / static_cast<float>(numRows);
|
2026-07-21 13:16:19 +02:00
|
|
|
int row = static_cast<int>((static_cast<float>(e.getPosition().getY()) - keyArea.getY()) / rowHeight);
|
2026-07-22 23:10:55 +02:00
|
|
|
row = juce::jlimit(0, numRows - 1, row);
|
2026-07-21 02:06:45 +02:00
|
|
|
int midiNote = keyRowToMidiNote(row);
|
|
|
|
|
|
|
|
|
|
if (onKeyClick)
|
|
|
|
|
onKeyClick(midiNote);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Right click removes slice
|
|
|
|
|
if (e.mods.isRightButtonDown())
|
|
|
|
|
{
|
|
|
|
|
int sample = xToSample(static_cast<int>(e.getPosition().getX()));
|
|
|
|
|
auto area = getWaveformArea();
|
|
|
|
|
int range = getVisibleEnd() - getVisibleStart();
|
|
|
|
|
int tolerance = (range > 0) ? juce::jmax(1, static_cast<int>(10.0f * static_cast<float>(range) / area.getWidth())) : 1;
|
|
|
|
|
sliceManager.removeSliceAt(sample, tolerance);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Left click on waveform area
|
|
|
|
|
if (e.mods.isLeftButtonDown())
|
|
|
|
|
{
|
|
|
|
|
int px = static_cast<int>(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<int>(static_cast<float>(tolerancePx) * static_cast<float>(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<int>(slices.size()); ++i)
|
|
|
|
|
{
|
|
|
|
|
int dist = std::abs(slices[static_cast<size_t>(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<int>::max();
|
|
|
|
|
for (int i = 0; i < static_cast<int>(slices2.size()); ++i)
|
|
|
|
|
{
|
|
|
|
|
int dist = std::abs(slices2[static_cast<size_t>(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<int>(e.getPosition().getX()));
|
|
|
|
|
const auto& slices = sliceManager.getSlices();
|
|
|
|
|
if (draggingSliceIndex < static_cast<int>(slices.size()))
|
|
|
|
|
sliceManager.moveSlice(slices[static_cast<size_t>(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<int>(area.getX()) || e.getPosition().getX() > static_cast<int>(area.getRight()))
|
|
|
|
|
{
|
|
|
|
|
hoveredSliceIndex = -1;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int range = getVisibleEnd() - getVisibleStart();
|
|
|
|
|
int tolerancePx = 10;
|
|
|
|
|
int toleranceSamples = (range > 0) ? juce::jmax(1, static_cast<int>(static_cast<float>(tolerancePx) * static_cast<float>(range) / area.getWidth())) : 1;
|
|
|
|
|
int sample = xToSample(static_cast<int>(e.getPosition().getX()));
|
|
|
|
|
|
|
|
|
|
const auto& slices = sliceManager.getSlices();
|
|
|
|
|
int bestIdx = -1;
|
|
|
|
|
int bestDist = toleranceSamples + 1;
|
|
|
|
|
for (int i = 0; i < static_cast<int>(slices.size()); ++i)
|
|
|
|
|
{
|
|
|
|
|
int dist = std::abs(slices[static_cast<size_t>(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<float>(e.getPosition().getX()) - area.getX()) / area.getWidth();
|
|
|
|
|
mouseXRatio = juce::jlimit(0.0f, 1.0f, mouseXRatio);
|
|
|
|
|
int oldVisible = static_cast<int>(static_cast<float>(getNumSamples()) / oldZoom);
|
|
|
|
|
int newVisible = static_cast<int>(static_cast<float>(getNumSamples()) / zoomFactor);
|
|
|
|
|
float mouseSample = scrollOffset * static_cast<float>(juce::jmax(0, getNumSamples() - oldVisible)) + mouseXRatio * static_cast<float>(oldVisible);
|
|
|
|
|
scrollOffset = (mouseSample - mouseXRatio * static_cast<float>(newVisible)) / static_cast<float>(juce::jmax(1, getNumSamples() - newVisible));
|
|
|
|
|
scrollOffset = juce::jlimit(0.0f, 1.0f, scrollOffset);
|
|
|
|
|
|
|
|
|
|
syncScrollBar();
|
|
|
|
|
repaint();
|
|
|
|
|
}
|