mirror of
https://codeberg.org/armin/justasample.git
synced 2026-09-01 04:10:48 +02:00
525 lines
20 KiB
C++
525 lines
20 KiB
C++
|
|
/*
|
||
|
|
==============================================================================
|
||
|
|
|
||
|
|
SampleNavigator.cpp
|
||
|
|
Created: 19 Sep 2023 4:41:12pm
|
||
|
|
Author: binya
|
||
|
|
|
||
|
|
==============================================================================
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <JuceHeader.h>
|
||
|
|
|
||
|
|
#include "SampleNavigator.h"
|
||
|
|
|
||
|
|
SampleNavigator::SampleNavigator(APVTS& apvts, PluginParameters::State& pluginState, const juce::OwnedArray<CustomSamplerVoice>& synthVoices) :
|
||
|
|
apvts(apvts), state(pluginState), dummyParam(apvts, PluginParameters::State::UI_DUMMY_PARAM),
|
||
|
|
painter(pluginState.primaryChannel, 0.2f),
|
||
|
|
gainAttachment(*apvts.getParameter(PluginParameters::SAMPLE_GAIN), [this](float newValue) { painter.setGain(juce::Decibels::decibelsToGain(newValue)); }, apvts.undoManager),
|
||
|
|
monoAttachment(*apvts.getParameter(PluginParameters::MONO_OUTPUT), [this](bool newValue) { painter.setMono(newValue); }, apvts.undoManager),
|
||
|
|
synthVoices(synthVoices),
|
||
|
|
isWavetableModeDisabled(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::DISABLE_WAVETABLE_MODE))),
|
||
|
|
isLooping(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::IS_LOOPING))),
|
||
|
|
loopHasStart(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::LOOPING_HAS_START))),
|
||
|
|
loopHasEnd(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::LOOPING_HAS_END))),
|
||
|
|
isWavetableModeDisabledAttachment(*this->isWavetableModeDisabled, [this](bool) { repaint(); }, apvts.undoManager),
|
||
|
|
loopAttachment(*isLooping, [this](bool newValue) { loopHasStartUpdate(newValue && loopHasStart->get()); loopHasEndUpdate(newValue && loopHasEnd->get()); }, apvts.undoManager),
|
||
|
|
loopStartAttachment(*loopHasStart, [this](bool newValue) { loopHasStartUpdate(newValue); }, apvts.undoManager),
|
||
|
|
loopEndAttachment(*loopHasEnd, [this](bool newValue) { loopHasEndUpdate(newValue); }, apvts.undoManager)
|
||
|
|
{
|
||
|
|
state.viewStart.addListener(this);
|
||
|
|
state.viewEnd.addListener(this);
|
||
|
|
state.sampleStart.addListener(this);
|
||
|
|
state.sampleEnd.addListener(this);
|
||
|
|
state.loopStart.addListener(this);
|
||
|
|
state.loopEnd.addListener(this);
|
||
|
|
state.pinView.addListener(this);
|
||
|
|
|
||
|
|
updatePinnedPositions();
|
||
|
|
|
||
|
|
painter.setGain(juce::Decibels::decibelsToGain(float(apvts.getParameterAsValue(PluginParameters::SAMPLE_GAIN).getValue())));
|
||
|
|
addAndMakeVisible(&painter);
|
||
|
|
|
||
|
|
painter.setInterceptsMouseClicks(false, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
SampleNavigator::~SampleNavigator()
|
||
|
|
{
|
||
|
|
state.viewStart.removeListener(this);
|
||
|
|
state.viewEnd.removeListener(this);
|
||
|
|
state.sampleStart.removeListener(this);
|
||
|
|
state.sampleEnd.removeListener(this);
|
||
|
|
state.loopStart.removeListener(this);
|
||
|
|
state.loopEnd.removeListener(this);
|
||
|
|
state.pinView.removeListener(this);
|
||
|
|
}
|
||
|
|
|
||
|
|
void SampleNavigator::valueChanged(ListenableValue<int>& source, int /*newValue*/)
|
||
|
|
{
|
||
|
|
auto& sourceA = dynamic_cast<ListenableAtomic<int>&>(source);
|
||
|
|
if (!navigatorUpdate && (sourceA == state.loopStart || sourceA == state.loopEnd || sourceA == state.sampleStart || sourceA == state.sampleEnd))
|
||
|
|
updatePinnedPositions();
|
||
|
|
|
||
|
|
safeRepaint();
|
||
|
|
}
|
||
|
|
|
||
|
|
void SampleNavigator::valueChanged(ListenableValue<bool>& source, bool newValue)
|
||
|
|
{
|
||
|
|
// When the pin is enabled, we move the bounds to within the viewport
|
||
|
|
updatePinnedPositions();
|
||
|
|
|
||
|
|
bool loopingHasStart = isLooping->get() && loopHasStart->get() && !isWaveformMode();
|
||
|
|
bool loopingHasEnd = isLooping->get() && loopHasEnd->get() && !isWaveformMode();
|
||
|
|
|
||
|
|
if (state.pinView && dynamic_cast<ListenableAtomic<bool>&>(source) == state.pinView && newValue &&
|
||
|
|
((loopingHasStart && state.loopStart < state.viewStart) || (loopingHasEnd && state.loopEnd > state.viewEnd) ||
|
||
|
|
state.sampleStart < state.viewStart || state.sampleEnd > state.viewEnd))
|
||
|
|
{
|
||
|
|
int currentViewStart = state.viewStart;
|
||
|
|
int currentViewEnd = state.viewEnd;
|
||
|
|
|
||
|
|
int newViewStart = loopingHasStart ? state.loopStart.load() : state.sampleStart.load();
|
||
|
|
int newViewEnd = loopingHasEnd ? state.loopEnd.load() : state.sampleEnd.load();
|
||
|
|
|
||
|
|
state.viewStart = newViewStart;
|
||
|
|
state.viewEnd = newViewEnd;
|
||
|
|
updatePinnedPositions();
|
||
|
|
state.viewStart = currentViewStart;
|
||
|
|
state.viewEnd = currentViewEnd;
|
||
|
|
moveBoundsToPinnedPositions(newViewStart > currentViewStart);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//==============================================================================
|
||
|
|
void SampleNavigator::setSample(const juce::AudioBuffer<float>& sampleBuffer, float bufferSampleRate, bool resetView)
|
||
|
|
{
|
||
|
|
painter.setSample(sampleBuffer);
|
||
|
|
sample = &sampleBuffer;
|
||
|
|
sampleRate = bufferSampleRate;
|
||
|
|
|
||
|
|
if (resetView || recordingMode)
|
||
|
|
{
|
||
|
|
state.viewStart = 0;
|
||
|
|
state.viewEnd = sampleBuffer.getNumSamples() - 1;
|
||
|
|
updatePinnedPositions();
|
||
|
|
}
|
||
|
|
repaint();
|
||
|
|
}
|
||
|
|
|
||
|
|
void SampleNavigator::sampleUpdated(int oldSize, int newSize)
|
||
|
|
{
|
||
|
|
painter.appendToPath(oldSize, newSize - 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
void SampleNavigator::setRecordingMode(bool recording)
|
||
|
|
{
|
||
|
|
recordingMode = recording;
|
||
|
|
}
|
||
|
|
|
||
|
|
//==============================================================================
|
||
|
|
void SampleNavigator::paintOverChildren(juce::Graphics& g)
|
||
|
|
{
|
||
|
|
if (!sample || !sample->getNumSamples())
|
||
|
|
return;
|
||
|
|
|
||
|
|
using namespace juce;
|
||
|
|
|
||
|
|
auto colors = getTheme();
|
||
|
|
|
||
|
|
// Paints the voice positions
|
||
|
|
if (!recordingMode && !isWaveformMode())
|
||
|
|
{
|
||
|
|
for (auto& voice : synthVoices)
|
||
|
|
{
|
||
|
|
if (voice->isPlaying())
|
||
|
|
{
|
||
|
|
int location = int(std::ceil(voice->getPosition()));
|
||
|
|
auto pos = sampleToPosition(location);
|
||
|
|
|
||
|
|
Path voicePath{};
|
||
|
|
voicePath.addLineSegment(Line<float>(pos, 0.f, pos, float(getHeight())), 1.f);
|
||
|
|
g.setColour(colors.light.withAlpha(voice->getEnvelopeGain()));
|
||
|
|
g.strokePath(voicePath, PathStrokeType(Layout::playheadWidth * getWidth()));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
float boundsThickness = getWidth() * Layout::navigatorBoundsWidth * 0.66f;
|
||
|
|
float sampleStartPos = sampleToPosition(state.sampleStart);
|
||
|
|
float sampleEndPos = sampleToPosition(state.sampleEnd);
|
||
|
|
float loopStartPos = sampleToPosition(state.loopStart);
|
||
|
|
float loopEndPos = sampleToPosition(state.loopEnd);
|
||
|
|
|
||
|
|
auto boundColor = isWaveformMode() ? colors.highlight : isLooping->get() ? colors.loop : colors.slate;
|
||
|
|
|
||
|
|
g.setColour(disabled(boundColor));
|
||
|
|
g.fillRect(sampleStartPos - boundsThickness, getHeight() * 0.1f, boundsThickness, getHeight() * 0.8f);
|
||
|
|
g.setColour(disabled(boundColor));
|
||
|
|
g.fillRect(sampleEndPos, getHeight() * 0.1f, boundsThickness, getHeight() * 0.8f);
|
||
|
|
|
||
|
|
g.setColour(disabled(colors.slate));
|
||
|
|
if (isLooping->get() && loopHasStart->get() && !isWaveformMode())
|
||
|
|
g.fillRect(loopStartPos - boundsThickness, getHeight() * 0.1f, boundsThickness, getHeight() * 0.8f);
|
||
|
|
if (isLooping->get() && loopHasEnd->get() && !isWaveformMode())
|
||
|
|
g.fillRect(loopEndPos, getHeight() * 0.1f, boundsThickness, getHeight() * 0.8f);
|
||
|
|
|
||
|
|
// Paints the start and stop
|
||
|
|
float startPos = sampleToPosition(recordingMode ? 0 : int(state.viewStart));
|
||
|
|
float stopPos = sampleToPosition(recordingMode ? sample->getNumSamples() - 1 : int(state.viewEnd));
|
||
|
|
g.setColour(disabled(colors.slate.withAlpha(0.15f)));
|
||
|
|
g.fillRect(startPos, 0.f, stopPos - startPos + 1.f, float(getHeight()));
|
||
|
|
|
||
|
|
float lineThickness = getWidth() * Layout::navigatorBoundsWidth;
|
||
|
|
g.setColour(disabled(colors.slate));
|
||
|
|
g.fillRect(startPos - lineThickness, 0.f, lineThickness, float(getHeight()));
|
||
|
|
g.fillRect(stopPos, 0.f, lineThickness, float(getHeight()));
|
||
|
|
}
|
||
|
|
|
||
|
|
void SampleNavigator::resized()
|
||
|
|
{
|
||
|
|
auto bounds = getLocalBounds().toFloat();
|
||
|
|
float lineThickness = bounds.getWidth() * Layout::navigatorBoundsWidth;
|
||
|
|
|
||
|
|
bounds.reduce(lineThickness, 0.f);
|
||
|
|
painter.setBounds(bounds.toNearestInt());
|
||
|
|
}
|
||
|
|
|
||
|
|
void SampleNavigator::lookAndFeelChanged()
|
||
|
|
{
|
||
|
|
auto colors = getTheme();
|
||
|
|
|
||
|
|
painter.setColour(Colors::painterColorId, colors.darkerSlate);
|
||
|
|
}
|
||
|
|
|
||
|
|
void SampleNavigator::enablementChanged()
|
||
|
|
{
|
||
|
|
painter.setEnabled(isEnabled());
|
||
|
|
painter.repaint();
|
||
|
|
}
|
||
|
|
|
||
|
|
//==============================================================================
|
||
|
|
void SampleNavigator::mouseDown(const juce::MouseEvent& event)
|
||
|
|
{
|
||
|
|
if (!sample || !isEnabled() || recordingMode)
|
||
|
|
return;
|
||
|
|
|
||
|
|
draggingTarget = getDraggingTarget(event.getMouseDownX(), event.getMouseDownY());
|
||
|
|
if (draggingTarget == Drag::SAMPLE_FULL)
|
||
|
|
dragSelectOffset = event.getMouseDownX() - sampleToPosition(state.viewStart);
|
||
|
|
dragging = draggingTarget != Drag::NONE;
|
||
|
|
lastDragOffset = 0;
|
||
|
|
|
||
|
|
if (draggingTarget == Drag::SAMPLE_START || draggingTarget == Drag::SAMPLE_END || draggingTarget == Drag::SAMPLE_FULL)
|
||
|
|
juce::Desktop::getInstance().getMainMouseSource().enableUnboundedMouseMovement(true, false);
|
||
|
|
|
||
|
|
repaint();
|
||
|
|
}
|
||
|
|
|
||
|
|
void SampleNavigator::mouseUp(const juce::MouseEvent&)
|
||
|
|
{
|
||
|
|
if (!sample || recordingMode || !dragging)
|
||
|
|
return;
|
||
|
|
|
||
|
|
if (dragging)
|
||
|
|
dummyParam.sendUIUpdate();
|
||
|
|
|
||
|
|
dragging = false;
|
||
|
|
|
||
|
|
juce::Desktop::getInstance().getMainMouseSource().enableUnboundedMouseMovement(false);
|
||
|
|
auto screenPos = getScreenBounds().toFloat();
|
||
|
|
juce::Point<float> newMousePos;
|
||
|
|
if (draggingTarget == Drag::SAMPLE_START)
|
||
|
|
newMousePos = juce::Point(screenPos.getX() + sampleToPosition(state.viewStart) * screenPos.getWidth() / getWidth(), screenPos.getCentreY());
|
||
|
|
else if (draggingTarget == Drag::SAMPLE_END)
|
||
|
|
newMousePos = juce::Point(screenPos.getX() + sampleToPosition(state.viewEnd) * screenPos.getWidth() / getWidth(), screenPos.getCentreY());
|
||
|
|
else if (draggingTarget == Drag::SAMPLE_FULL)
|
||
|
|
newMousePos = juce::Point(screenPos.getX() + (sampleToPosition(state.viewStart) + dragSelectOffset) * screenPos.getWidth() / getWidth(), screenPos.getCentreY());
|
||
|
|
juce::Desktop::getInstance().getMainMouseSource().setScreenPosition(newMousePos);
|
||
|
|
|
||
|
|
repaint();
|
||
|
|
}
|
||
|
|
|
||
|
|
void SampleNavigator::mouseMove(const juce::MouseEvent& event)
|
||
|
|
{
|
||
|
|
if (!sample || !isEnabled() || recordingMode)
|
||
|
|
{
|
||
|
|
setMouseCursor(juce::MouseCursor::NormalCursor);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (dragging) // Don't change while dragging
|
||
|
|
return;
|
||
|
|
|
||
|
|
Drag currentTarget = getDraggingTarget(event.getMouseDownX(), event.getMouseDownY());
|
||
|
|
switch (currentTarget)
|
||
|
|
{
|
||
|
|
case Drag::SAMPLE_START:
|
||
|
|
case Drag::SAMPLE_END:
|
||
|
|
setMouseCursor(juce::MouseCursor::LeftRightResizeCursor);
|
||
|
|
break;
|
||
|
|
case Drag::SAMPLE_FULL:
|
||
|
|
setMouseCursor(juce::MouseCursor::DraggingHandCursor);
|
||
|
|
break;
|
||
|
|
case Drag::NONE:
|
||
|
|
setMouseCursor(juce::MouseCursor::NormalCursor);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void SampleNavigator::mouseDrag(const juce::MouseEvent& event)
|
||
|
|
{
|
||
|
|
if (!sample || !dragging || !isEnabled() || recordingMode)
|
||
|
|
return;
|
||
|
|
|
||
|
|
// The goal is to keep the positions within their normal constraints
|
||
|
|
bool secondary = juce::ModifierKeys::currentModifiers.isAnyModifierKeyDown();
|
||
|
|
float edgeSensitivity = getDragSensitivity(secondary);
|
||
|
|
float fullSensitivity = getDragSensitivity(!secondary);
|
||
|
|
switch (draggingTarget)
|
||
|
|
{
|
||
|
|
case Drag::SAMPLE_START:
|
||
|
|
{
|
||
|
|
int difference = event.getOffsetFromDragStart().getX() - lastDragOffset;
|
||
|
|
moveStart(difference / 2.f, edgeSensitivity);
|
||
|
|
moveEnd(-difference / 2.f, edgeSensitivity);
|
||
|
|
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case Drag::SAMPLE_END:
|
||
|
|
{
|
||
|
|
int difference = event.getOffsetFromDragStart().getX() - lastDragOffset;
|
||
|
|
moveEnd(difference / 2.f, edgeSensitivity);
|
||
|
|
moveStart(-difference / 2.f, edgeSensitivity);
|
||
|
|
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case Drag::SAMPLE_FULL:
|
||
|
|
{
|
||
|
|
int change = event.getOffsetFromDragStart().getX() - lastDragOffset;
|
||
|
|
moveBoth(float(change), fullSensitivity);
|
||
|
|
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case Drag::NONE:
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
lastDragOffset = event.getOffsetFromDragStart().getX();
|
||
|
|
}
|
||
|
|
|
||
|
|
void SampleNavigator::mouseWheelMove(const juce::MouseEvent& event, const juce::MouseWheelDetails& wheel)
|
||
|
|
{
|
||
|
|
if (!sample || dragging || !isEnabled() || recordingMode)
|
||
|
|
return;
|
||
|
|
|
||
|
|
int sampleCenter = positionToSample(event.position.getX());
|
||
|
|
scrollView(wheel, sampleCenter);
|
||
|
|
}
|
||
|
|
|
||
|
|
void SampleNavigator::mouseDoubleClick(const juce::MouseEvent&)
|
||
|
|
{
|
||
|
|
if (!sample || recordingMode)
|
||
|
|
return;
|
||
|
|
|
||
|
|
state.viewStart = 0;
|
||
|
|
state.viewEnd = sample->getNumSamples() - 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
void SampleNavigator::scrollView(const juce::MouseWheelDetails& wheel, int sampleCenter, bool centerZoomOut)
|
||
|
|
{
|
||
|
|
if (!sample)
|
||
|
|
return;
|
||
|
|
|
||
|
|
float changeY = wheel.deltaY * Feel::MOUSE_SENSITIVITY;
|
||
|
|
float changeX = wheel.deltaX * Feel::MOUSE_SENSITIVITY;
|
||
|
|
|
||
|
|
// MacOS sensitivity is a little different
|
||
|
|
if (wheel.isSmooth && ((juce::SystemStats::getOperatingSystemType() & juce::SystemStats::MacOSX) == 0))
|
||
|
|
changeY *= 0.75f;
|
||
|
|
|
||
|
|
bool trackHorizontal = std::abs(wheel.deltaX) > std::abs(wheel.deltaY);
|
||
|
|
bool modifier = juce::ModifierKeys::currentModifiers.isAnyModifierKeyDown();
|
||
|
|
|
||
|
|
if (modifier || trackHorizontal)
|
||
|
|
{
|
||
|
|
float sensitivity = getDragSensitivity(false);
|
||
|
|
moveBoth(trackHorizontal ? changeX : changeY, sensitivity);
|
||
|
|
}
|
||
|
|
else if (!modifier)
|
||
|
|
{
|
||
|
|
float sensitivity = getDragSensitivity(false);
|
||
|
|
float startRatio = 1.f;
|
||
|
|
float endRatio = 1.f;
|
||
|
|
if (sampleCenter >= state.viewStart && sampleCenter <= state.viewEnd &&
|
||
|
|
((centerZoomOut && state.viewStart > 0 && state.viewEnd < sample->getNumSamples() - 1) || changeY > 0))
|
||
|
|
{
|
||
|
|
endRatio = float(state.viewEnd - sampleCenter) / (state.viewEnd - state.viewStart);
|
||
|
|
startRatio = 1.f - endRatio;
|
||
|
|
}
|
||
|
|
|
||
|
|
// The order must change depending on the direction of the wheel, since both maintain constraints and there's a minimum view size
|
||
|
|
if (changeY > 0)
|
||
|
|
{
|
||
|
|
moveEnd(-changeY * endRatio, sensitivity);
|
||
|
|
moveStart(changeY * startRatio, sensitivity);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
moveStart(changeY * startRatio, sensitivity);
|
||
|
|
moveEnd(-changeY * endRatio, sensitivity);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void SampleNavigator::fitView()
|
||
|
|
{
|
||
|
|
if (!sample)
|
||
|
|
return;
|
||
|
|
|
||
|
|
int viewStart = isLooping->get() && loopHasStart->get() && !isWaveformMode() ? state.loopStart.load() : state.sampleStart.load();
|
||
|
|
int viewEnd = isLooping->get() && loopHasEnd->get() && !isWaveformMode() ? state.loopEnd.load() : state.sampleEnd.load();
|
||
|
|
|
||
|
|
// Keep the view size larger than the minimum
|
||
|
|
if (viewEnd - viewStart + 1 < Feel::MINIMUM_VIEW)
|
||
|
|
{
|
||
|
|
viewEnd = juce::jmin<int>(sample->getNumSamples() - 1, viewStart + Feel::MINIMUM_VIEW - 1);
|
||
|
|
viewStart = juce::jmax<int>(0, viewEnd - Feel::MINIMUM_VIEW + 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
state.viewStart = viewStart;
|
||
|
|
state.viewEnd = viewEnd;
|
||
|
|
|
||
|
|
updatePinnedPositions();
|
||
|
|
|
||
|
|
repaint();
|
||
|
|
}
|
||
|
|
|
||
|
|
void SampleNavigator::moveStart(float change, float sensitivity)
|
||
|
|
{
|
||
|
|
int oldViewStart = state.viewStart;
|
||
|
|
|
||
|
|
auto viewStart = juce::jmax<int>(juce::jmin<int>(int(std::round(state.viewStart + change * sensitivity)), state.viewEnd - Feel::MINIMUM_VIEW), 0);
|
||
|
|
state.viewStart = viewStart;
|
||
|
|
|
||
|
|
if (state.pinView)
|
||
|
|
moveBoundsToPinnedPositions(viewStart < oldViewStart);
|
||
|
|
}
|
||
|
|
|
||
|
|
void SampleNavigator::moveEnd(float change, float sensitivity)
|
||
|
|
{
|
||
|
|
int oldViewEnd = state.viewEnd;
|
||
|
|
|
||
|
|
auto viewEnd = juce::jmin<int>(juce::jmax<int>(int(std::round(state.viewEnd + change * sensitivity)), state.viewStart + Feel::MINIMUM_VIEW), sample->getNumSamples() - 1);
|
||
|
|
state.viewEnd = viewEnd;
|
||
|
|
|
||
|
|
if (state.pinView)
|
||
|
|
moveBoundsToPinnedPositions(viewEnd < oldViewEnd);
|
||
|
|
}
|
||
|
|
|
||
|
|
void SampleNavigator::moveBoth(float change, float sensitivity)
|
||
|
|
{
|
||
|
|
int difference = juce::jlimit<int>(-state.viewStart, sample->getNumSamples() - 1 - state.viewEnd, int(change * sensitivity));
|
||
|
|
|
||
|
|
state.viewStart = state.viewStart + difference;
|
||
|
|
state.viewEnd = state.viewEnd + difference;
|
||
|
|
|
||
|
|
if (state.pinView)
|
||
|
|
moveBoundsToPinnedPositions(difference < 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
//==============================================================================
|
||
|
|
NavigatorParts SampleNavigator::getDraggingTarget(int x, int y) const
|
||
|
|
{
|
||
|
|
auto bounds = getLocalBounds().toFloat();
|
||
|
|
auto startPos = sampleToPosition(state.viewStart);
|
||
|
|
auto stopPos = sampleToPosition(state.viewEnd);
|
||
|
|
|
||
|
|
// A little trick to make the full sample always possible to drag
|
||
|
|
auto offset = juce::jmax<float>(30 - (stopPos - startPos), 0) / 2;
|
||
|
|
|
||
|
|
juce::Array targets = {
|
||
|
|
CompPart {Drag::SAMPLE_START, juce::Rectangle<float>{startPos - offset, bounds.getY(), 0, bounds.getHeight()}, 2},
|
||
|
|
CompPart {Drag::SAMPLE_END, juce::Rectangle<float>{stopPos + offset, bounds.getY(), 0, bounds.getHeight()}, 2},
|
||
|
|
CompPart {Drag::SAMPLE_FULL, juce::Rectangle<float>{startPos, bounds.getY(), stopPos - startPos, bounds.getHeight()}, 1}
|
||
|
|
};
|
||
|
|
return CompPart<Drag>::getClosestInRange(targets, x, y, Feel::DRAGGABLE_SNAP);
|
||
|
|
}
|
||
|
|
|
||
|
|
float SampleNavigator::sampleToPosition(int sampleIndex) const
|
||
|
|
{
|
||
|
|
float boundsWidth = getWidth() * Layout::navigatorBoundsWidth;
|
||
|
|
return juce::jmap<float>(float(sampleIndex), 0.f, float(sample->getNumSamples() - 1), 0.f, float(painter.getWidth())) + boundsWidth;
|
||
|
|
}
|
||
|
|
|
||
|
|
int SampleNavigator::positionToSample(float position) const
|
||
|
|
{
|
||
|
|
return int(juce::jmap<float>(position - getWidth() * Layout::navigatorBoundsWidth, 0.f, float(painter.getWidth()), 0.f, float(sample->getNumSamples())));
|
||
|
|
}
|
||
|
|
|
||
|
|
float SampleNavigator::getDragSensitivity(bool constant) const
|
||
|
|
{
|
||
|
|
int viewSize = state.viewEnd - state.viewStart + 1;
|
||
|
|
if (constant)
|
||
|
|
return float(sample->getNumSamples()) / getWidth();
|
||
|
|
else
|
||
|
|
return -std::log(float(viewSize) / sample->getNumSamples() / juce::MathConstants<float>::euler) * viewSize / getWidth();
|
||
|
|
}
|
||
|
|
|
||
|
|
void SampleNavigator::loopHasStartUpdate(bool newValue)
|
||
|
|
{
|
||
|
|
repaint();
|
||
|
|
|
||
|
|
if (!bool(newValue))
|
||
|
|
return;
|
||
|
|
|
||
|
|
if (state.viewStart > state.loopStart && (state.loopStart == 0 || state.pinView))
|
||
|
|
state.loopStart = state.viewStart.load();
|
||
|
|
}
|
||
|
|
|
||
|
|
void SampleNavigator::loopHasEndUpdate(bool newValue)
|
||
|
|
{
|
||
|
|
repaint();
|
||
|
|
|
||
|
|
if (!bool(newValue))
|
||
|
|
return;
|
||
|
|
|
||
|
|
if (state.viewEnd < state.loopEnd && (state.loopEnd == sample->getNumSamples() - 1 || state.pinView))
|
||
|
|
state.loopEnd = state.viewEnd.load();
|
||
|
|
}
|
||
|
|
|
||
|
|
void SampleNavigator::updatePinnedPositions()
|
||
|
|
{
|
||
|
|
int viewStart = state.viewStart;
|
||
|
|
int viewEnd = state.viewEnd;
|
||
|
|
|
||
|
|
pinnedSampleStart = juce::jlimit<float>(0.f, 1.f, float(state.sampleStart - viewStart) / (viewEnd - viewStart));
|
||
|
|
pinnedSampleEnd = juce::jlimit<float>(0.f, 1.f, float(state.sampleEnd - viewStart) / (viewEnd - viewStart));
|
||
|
|
pinnedLoopStart = juce::jlimit<float>(0.f, 1.f, float(state.loopStart - viewStart) / (viewEnd - viewStart));
|
||
|
|
pinnedLoopEnd = juce::jlimit<float>(0.f, 1.f, float(state.loopEnd - viewStart) / (viewEnd - viewStart));
|
||
|
|
}
|
||
|
|
|
||
|
|
void SampleNavigator::moveBoundsToPinnedPositions(bool startToEnd)
|
||
|
|
{
|
||
|
|
// To maintain constraints, we change the bounds in order depending on the direction of the change
|
||
|
|
navigatorUpdate = true;
|
||
|
|
if (startToEnd)
|
||
|
|
{
|
||
|
|
state.loopStart = juce::jmax(int(std::round(pinnedLoopStart * (state.viewEnd - state.viewStart))) + state.viewStart, 0);
|
||
|
|
state.sampleStart = int(std::round(pinnedSampleStart * (state.viewEnd - state.viewStart))) + state.viewStart;
|
||
|
|
state.sampleEnd = int(std::round(pinnedSampleEnd * (state.viewEnd - state.viewStart))) + state.viewStart;
|
||
|
|
state.loopEnd = juce::jmin(int(std::round(pinnedLoopEnd * (state.viewEnd - state.viewStart))) + state.viewStart, sample->getNumSamples() - 1);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
state.loopEnd = juce::jmin(int(std::round(pinnedLoopEnd * (state.viewEnd - state.viewStart))) + state.viewStart, sample->getNumSamples() - 1);
|
||
|
|
state.sampleEnd = int(std::round(pinnedSampleEnd * (state.viewEnd - state.viewStart))) + state.viewStart;
|
||
|
|
state.sampleStart = int(std::round(pinnedSampleStart * (state.viewEnd - state.viewStart))) + state.viewStart;
|
||
|
|
state.loopStart = juce::jmax(int(std::round(pinnedLoopStart * (state.viewEnd - state.viewStart))) + state.viewStart, 0);
|
||
|
|
}
|
||
|
|
navigatorUpdate = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool SampleNavigator::isWaveformMode() const
|
||
|
|
{
|
||
|
|
return CustomSamplerVoice::isWavetableModeAvailable(sampleRate, state.sampleStart, state.sampleEnd) && !isWavetableModeDisabled->get();
|
||
|
|
}
|