mirror of
https://codeberg.org/armin/justasample.git
synced 2026-09-01 20:30:48 +02:00
1204 lines
53 KiB
C++
1204 lines
53 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
PluginProcessor.cpp
|
|
Created: 5 Sep 2023
|
|
Author: binya
|
|
|
|
==============================================================================
|
|
*/
|
|
|
|
#include "PluginProcessor.h"
|
|
#include "PluginEditor.h"
|
|
#include "BuildInfo.h"
|
|
|
|
JustaSampleAudioProcessorEditor::JustaSampleAudioProcessorEditor(JustaSampleAudioProcessor& audioProcessor)
|
|
: AudioProcessorEditor(&audioProcessor), p(audioProcessor), pluginState(p.getPluginState()),
|
|
dummyParam(p.APVTS(), PluginParameters::State::UI_DUMMY_PARAM),
|
|
synthVoices(p.getSamplerVoices()),
|
|
|
|
// Modules
|
|
tuningLabel("", "Tuning"),
|
|
attackLabel("", "Attack"),
|
|
releaseLabel("", "Release"),
|
|
playbackLabel("", "Playback"),
|
|
loopingLabel("", "Loop"),
|
|
masterLabel("", "Master"),
|
|
|
|
// Tuning module
|
|
semitoneRotaryAttachment(p.APVTS(), PluginParameters::SEMITONE_TUNING, semitoneRotary),
|
|
centRotaryAttachment(p.APVTS(), PluginParameters::CENT_TUNING, centRotary),
|
|
waveformSemitoneRotaryAttachment(p.APVTS(), PluginParameters::WAVEFORM_SEMITONE_TUNING, waveformSemitoneRotary),
|
|
waveformCentRotaryAttachment(p.APVTS(), PluginParameters::WAVEFORM_CENT_TUNING, waveformCentRotary),
|
|
|
|
tuningDetectLabel("", "Detect"),
|
|
tuningDetectButton(getOutlineFromSVG(BinaryData::IconDetect_svg)),
|
|
|
|
// Attack and release modules
|
|
attackTimeAttachment(p.APVTS(), PluginParameters::ATTACK, attackTimeRotary),
|
|
attackCurveAttachment(p.APVTS(), PluginParameters::ATTACK_SHAPE, attackCurve),
|
|
releaseTimeAttachment(p.APVTS(), PluginParameters::RELEASE, releaseTimeRotary),
|
|
releaseCurveAttachment(p.APVTS(), PluginParameters::RELEASE_SHAPE, releaseCurve),
|
|
|
|
// Playback module
|
|
lofiModeAttachment(p.APVTS(), PluginParameters::SKIP_ANTIALIASING, lofiModeButton),
|
|
playbackModeButton(p.APVTS(), PluginParameters::PLAYBACK_MODE, PluginParameters::PLAYBACK_MODE_LABELS[0], PluginParameters::PLAYBACK_MODE_LABELS[1], this),
|
|
playbackModeAttachment(*p.APVTS().getParameter(PluginParameters::PLAYBACK_MODE), [this](float) { enablementChanged(); }, &p.getUndoManager()),
|
|
playbackSpeedAttachment(p.APVTS(), PluginParameters::SPEED_FACTOR, playbackSpeedRotary),
|
|
|
|
// Loop module
|
|
loopButton(true),
|
|
loopStartButton(true),
|
|
loopEndButton(true),
|
|
loopAttachment(p.APVTS(), PluginParameters::IS_LOOPING, loopButton),
|
|
loopStartAttachment(p.APVTS(), PluginParameters::LOOPING_HAS_START, loopStartButton),
|
|
loopEndAttachment(p.APVTS(), PluginParameters::LOOPING_HAS_END, loopEndButton),
|
|
|
|
// Master module
|
|
monoOutputAttachment(p.APVTS(), PluginParameters::MONO_OUTPUT, monoOutputButton),
|
|
gainSliderAttachment(p.APVTS(), PluginParameters::SAMPLE_GAIN, gainSlider),
|
|
|
|
// Sample toolbar
|
|
filenameComponent("", {}, true, false, false, p.getWildcardFilter(), "", "Select a file to load..."),
|
|
linkSampleToggle(true, true),
|
|
waveformModeLabel(true, true, this),
|
|
waveformModeLabelAttachment(*p.APVTS().getParameter(PluginParameters::DISABLE_WAVETABLE_MODE), waveformModeLabel, &p.getUndoManager()),
|
|
|
|
playStopButton({}, this),
|
|
deviceSettingsButton(getOutlineFromSVG(BinaryData::IconRecordSettings_svg)),
|
|
audioDeviceSettings(p.getDeviceManager(), 0, 2, true),
|
|
|
|
fitButton(getOutlineFromSVG(BinaryData::IconFit_svg)),
|
|
pinButton(true, true),
|
|
pinButtonAttachment(pinButton, pluginState.pinView, &dummyParam),
|
|
|
|
// Main controls
|
|
sampleEditor(p.APVTS(), p.getPluginState(), synthVoices,
|
|
[this](const juce::MouseWheelDetails& details, int center) -> void { sampleNavigator.scrollView(details, center, true); }),
|
|
sampleNavigator(p.APVTS(), p.getPluginState(), synthVoices),
|
|
|
|
fxChain(p),
|
|
|
|
// Footer
|
|
preFXAttachment(p.APVTS(), PluginParameters::PRE_FX, preFXButton),
|
|
showFXButton(true, true, this),
|
|
showFXAttachment(showFXButton, pluginState.showFX, &dummyParam),
|
|
eqEnablementAttachment(*p.APVTS().getParameter(PluginParameters::EQ_ENABLED), [this](float newValue) { eqEnabled = bool(newValue); fxEnablementChanged(); }, & p.getUndoManager()),
|
|
reverbEnablementAttachment(*p.APVTS().getParameter(PluginParameters::REVERB_ENABLED), [this](float newValue) { reverbEnabled = bool(newValue); fxEnablementChanged(); }, & p.getUndoManager()),
|
|
distortionEnablementAttachment(*p.APVTS().getParameter(PluginParameters::DISTORTION_ENABLED), [this](float newValue) { distortionEnabled = bool(newValue); fxEnablementChanged(); }, &p.getUndoManager()),
|
|
chorusEnablementAttachment(*p.APVTS().getParameter(PluginParameters::CHORUS_ENABLED), [this](float newValue) { chorusEnabled = bool(newValue); fxEnablementChanged(); }, &p.getUndoManager()),
|
|
|
|
lnf(dynamic_cast<CustomLookAndFeel&>(getLookAndFeel()))
|
|
{
|
|
// Set the plugin sizing
|
|
int width = pluginState.width;
|
|
int height = pluginState.height;
|
|
|
|
auto* desktopSize = juce::Desktop::getInstance().getDisplays().getPrimaryDisplay();
|
|
int maxWidth = desktopSize ? desktopSize->totalArea.getWidth() : 2000;
|
|
int maxHeight = desktopSize ? desktopSize->totalArea.getHeight() : 1600;
|
|
|
|
int minWidth = 250;
|
|
int minHeight = 200;
|
|
|
|
if (minWidth > width || width > maxWidth || minHeight > height || height > maxHeight)
|
|
setSize(1200, 600);
|
|
else
|
|
setSize(width, height);
|
|
setResizable(true, true);
|
|
setResizeLimits(minWidth, minHeight, maxWidth, maxHeight);
|
|
|
|
// Controls toolbar
|
|
juce::Array moduleLabels{ &tuningLabel, &tuningDetectLabel, &attackLabel, &releaseLabel, &playbackLabel, &loopingLabel, &masterLabel };
|
|
for (juce::Label* label : moduleLabels)
|
|
{
|
|
label->setJustificationType(juce::Justification::centred);
|
|
addAndMakeVisible(label);
|
|
}
|
|
|
|
std::function convertWithSecondaryUnit = [](double value) {
|
|
if (value >= 1000)
|
|
return juce::String(value / 1000.f, 1);
|
|
return juce::String(juce::roundToInt(value));
|
|
};
|
|
|
|
semitoneRotary.getProperties().set(ComponentProps::ROTARY_UNIT, PluginParameters::SEMITONE_UNIT);
|
|
waveformSemitoneRotary.getProperties().set(ComponentProps::ROTARY_UNIT, PluginParameters::SEMITONE_UNIT);
|
|
centRotary.getProperties().set(ComponentProps::ROTARY_UNIT, PluginParameters::CENT_UNIT);
|
|
waveformCentRotary.getProperties().set(ComponentProps::ROTARY_UNIT, PluginParameters::CENT_UNIT);
|
|
|
|
tuningDetectButton.onClick = [this] { promptPitchDetection(); };
|
|
tuningDetectButton.setHelpText("Auto-tune to 440hz (experimental)");
|
|
addAndMakeVisible(&tuningDetectButton);
|
|
|
|
attackTimeRotary.getProperties().set(ComponentProps::ROTARY_UNIT, PluginParameters::TIME_UNIT);
|
|
attackTimeRotary.getProperties().set(ComponentProps::ROTARY_GREATER_UNIT, PluginParameters::TIME_UNIT_LONG);
|
|
attackTimeRotary.textFromValueFunction = convertWithSecondaryUnit;
|
|
attackTimeRotary.updateText();
|
|
|
|
attackCurve.setLookAndFeel(&attackCurveLNF);
|
|
attackCurve.setSliderStyle(juce::Slider::SliderStyle::RotaryHorizontalVerticalDrag);
|
|
attackCurve.setMouseDragSensitivity(100);
|
|
attackCurve.setTextBoxStyle(juce::Slider::NoTextBox, false, 0, 0);
|
|
addAndMakeVisible(&attackCurve);
|
|
|
|
releaseTimeRotary.getProperties().set(ComponentProps::ROTARY_UNIT, PluginParameters::TIME_UNIT);
|
|
releaseTimeRotary.getProperties().set(ComponentProps::ROTARY_GREATER_UNIT, PluginParameters::TIME_UNIT_LONG);
|
|
releaseTimeRotary.textFromValueFunction = convertWithSecondaryUnit;
|
|
releaseTimeRotary.updateText();
|
|
|
|
releaseCurve.setLookAndFeel(&releaseCurveLNF);
|
|
releaseCurve.setSliderStyle(juce::Slider::SliderStyle::RotaryHorizontalVerticalDrag);
|
|
releaseCurve.setMouseDragSensitivity(100);
|
|
releaseCurve.setTextBoxStyle(juce::Slider::NoTextBox, false, 0, 0);
|
|
addAndMakeVisible(&releaseCurve);
|
|
|
|
lofiModeButton.useShape(getOutlineFromSVG(BinaryData::IconLofi_svg));
|
|
lofiModeButton.setHelpText("\"Lo-fi\" resampling");
|
|
addAndMakeVisible(&lofiModeButton);
|
|
|
|
playbackModeButton.setHelpText("Clear sound, higher pitches play quicker", "High CPU, maintains speed");
|
|
addAndMakeVisible(&playbackModeButton);
|
|
|
|
playbackSpeedRotary.getProperties().set(ComponentProps::ROTARY_ICON, new ReferenceCountedPath(getOutlineFromSVG(BinaryData::IconSpeed_svg)));
|
|
playbackSpeedRotary.getProperties().set(ComponentProps::ROTARY_UNIT, PluginParameters::SPEED_UNIT);
|
|
|
|
loopButton.useShape(getOutlineFromSVG(BinaryData::IconLoop_svg));
|
|
loopButton.setHelpText("Loop sample");
|
|
addAndMakeVisible(&loopButton);
|
|
|
|
loopStartButton.useShape(getOutlineFromSVG(BinaryData::IconLoopStart_svg));
|
|
loopStartButton.setOwnerButton(&loopButton);
|
|
loopStartButton.setHelpText("Loop with start portion");
|
|
addAndMakeVisible(&loopStartButton);
|
|
|
|
juce::Path loopEndIcon{ getOutlineFromSVG(BinaryData::IconLoopStart_svg) };
|
|
loopEndIcon.applyTransform(juce::AffineTransform::scale(-1, 1));
|
|
loopEndButton.useShape(loopEndIcon);
|
|
loopEndButton.setOwnerButton(&loopButton);
|
|
loopEndButton.setHelpText("Loop with release portion");
|
|
addAndMakeVisible(&loopEndButton);
|
|
|
|
monoOutputButton.useShape(getOutlineFromSVG(BinaryData::IconMono_svg));
|
|
monoOutputButton.setHelpText("Mix output to mono");
|
|
addAndMakeVisible(&monoOutputButton);
|
|
|
|
gainSlider.setLookAndFeel(&gainSliderLNF);
|
|
gainSlider.setSliderStyle(juce::Slider::SliderStyle::LinearHorizontal);
|
|
gainSlider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 0, 0);
|
|
gainSlider.setTextValueSuffix(" " + PluginParameters::VOLUME_UNIT);
|
|
gainSlider.setHelpText("Sample gain");
|
|
addAndMakeVisible(&gainSlider);
|
|
|
|
rotaries = { &semitoneRotary, &waveformSemitoneRotary, ¢Rotary, &waveformCentRotary, &attackTimeRotary, &releaseTimeRotary, &playbackSpeedRotary };
|
|
for (auto rotary : rotaries)
|
|
{
|
|
rotary->setSliderStyle(juce::Slider::RotaryVerticalDrag);
|
|
rotary->setMouseDragSensitivity(150);
|
|
rotary->setTextBoxStyle(juce::Slider::TextBoxBelow, false, 0, 0);
|
|
rotary->addMouseListener(this, true); // We need this to pass drag events through the labels
|
|
addAndMakeVisible(rotary);
|
|
}
|
|
|
|
// Editor and navigator
|
|
addAndMakeVisible(sampleEditor);
|
|
|
|
sampleNavigator.setHelpText("Drag or scroll to navigate");
|
|
addAndMakeVisible(sampleNavigator);
|
|
|
|
statusLabel.setJustificationType(juce::Justification::centred);
|
|
statusLabel.setInterceptsMouseClicks(false, false);
|
|
addAndMakeVisible(statusLabel);
|
|
|
|
// Sample controls
|
|
editorOverlay.setInterceptsMouseClicks(false, false);
|
|
addAndMakeVisible(editorOverlay);
|
|
|
|
juce::StringArray recentFiles{ pluginState.recentFiles };
|
|
for (int i = recentFiles.size() - 1; i >= 0; i--)
|
|
filenameComponent.addRecentlyUsedFile(juce::File(recentFiles[i]));
|
|
filenameComponent.addListener(this);
|
|
filenameComponent.setHelpText("Choose a file");
|
|
addAndMakeVisible(filenameComponent);
|
|
|
|
waveformModeLabel.useShape(getOutlineFromSVG(BinaryData::IconWaveformMode_svg));
|
|
waveformModeLabel.onStateChange = [this]
|
|
{
|
|
waveformModeLabel.setCustomHelpText(p.p(PluginParameters::DISABLE_WAVETABLE_MODE) ? "Waveform mode disabled" : "Waveform mode enabled");
|
|
editorOverlay.setWaveformModeDisabled(waveformModeLabel.getToggleState() ^ waveformModeLabel.isDown());
|
|
};
|
|
waveformModeLabel.onStateChange();
|
|
addChildComponent(waveformModeLabel);
|
|
|
|
linkSampleToggle.useShape(getOutlineFromSVG(BinaryData::IconLinkEnabled_svg));
|
|
linkSampleToggle.onClick = [this] { toggleLinkSample(); };
|
|
linkSampleToggle.setHelpText("Link preset to file");
|
|
addAndMakeVisible(linkSampleToggle);
|
|
|
|
playPath = getOutlineFromSVG(BinaryData::IconPlay_svg);
|
|
stopPath.addRoundedRectangle(juce::Rectangle(0, 0, 10, 10), 2.f);
|
|
playStopButton.setShape(playPath);
|
|
playStopButton.onClick = [this]
|
|
{
|
|
if (!currentlyPlaying)
|
|
p.playVoice();
|
|
else
|
|
p.haltVoices();
|
|
};
|
|
playStopButton.setHelpText(playHelpText);
|
|
addAndMakeVisible(playStopButton);
|
|
|
|
juce::Path recordIcon;
|
|
recordIcon.addEllipse(0, 0, 10, 10);
|
|
recordButton.setShape(recordIcon);
|
|
recordButton.onClick = [this] { startRecording(true); };
|
|
recordButton.setHelpText("Record a sample");
|
|
addAndMakeVisible(recordButton);
|
|
|
|
deviceSettingsButton.onClick = [this] { promptDeviceSettings(); };
|
|
deviceSettingsButton.setHelpText("Open input device settings");
|
|
addAndMakeVisible(deviceSettingsButton);
|
|
|
|
fitButton.onClick = [this] { sampleNavigator.fitView(); dummyParam.sendUIUpdate(); };
|
|
fitButton.setHelpText("Fit viewport to bounds");
|
|
addAndMakeVisible(fitButton);
|
|
|
|
pinButton.useShape(getOutlineFromSVG(BinaryData::IconPin_svg));
|
|
pinButton.setHelpText("Pin bounds to viewport");
|
|
addAndMakeVisible(pinButton);
|
|
|
|
// FX
|
|
addChildComponent(fxChain);
|
|
|
|
// Footer
|
|
preFXButton.useShape(getOutlineFromSVG(BinaryData::IconPreFX_svg));
|
|
preFXButton.setHelpText("Apply FX before Attack/Release");
|
|
addAndMakeVisible(&preFXButton);
|
|
|
|
showFXButton.onStateChange = [this, minHeight, maxHeight]
|
|
{
|
|
auto constrainedBounds = getConstrainedBounds();
|
|
bool matchesBounds = constrainedBounds.getHeight() == getHeight();
|
|
|
|
if ((fxChain.isVisible() != pluginState.showFX) && matchesBounds)
|
|
{
|
|
auto change = fxChain.isVisible() ? -scalei(Layout::fxChainHeight) : scalei(Layout::fxChainHeight);
|
|
setSize(getWidth(), juce::jlimit(minHeight, maxHeight, getHeight() + int(change)));
|
|
}
|
|
|
|
showFXButton.setHelpText(pluginState.showFX ? "Hide the effects chain" : "Show the effects chain");
|
|
resized();
|
|
};
|
|
|
|
showFXButton.useShape(getOutlineFromSVG(BinaryData::IconHideFX_svg), getOutlineFromSVG(BinaryData::IconShowFX_svg), juce::Justification::centredRight);
|
|
addAndMakeVisible(showFXButton);
|
|
|
|
versionInfo.setText("Build: " + juce::String(JAS_GIT_INFO) + " (" + JAS_BUILD_TIME + ")", juce::dontSendNotification);
|
|
addAndMakeVisible(versionInfo);
|
|
|
|
eqEnablementAttachment.sendInitialUpdate();
|
|
reverbEnablementAttachment.sendInitialUpdate();
|
|
distortionEnablementAttachment.sendInitialUpdate();
|
|
chorusEnablementAttachment.sendInitialUpdate();
|
|
|
|
addChildComponent(audioDeviceSettings);
|
|
|
|
prompt.setHelpText("Click to close");
|
|
addAndMakeVisible(prompt);
|
|
|
|
// Update theme
|
|
setTheme(darkTheme);
|
|
|
|
addMouseListener(this, true);
|
|
startTimerHz(PluginParameters::FRAME_RATE);
|
|
|
|
enablementChanged();
|
|
}
|
|
|
|
JustaSampleAudioProcessorEditor::~JustaSampleAudioProcessorEditor()
|
|
{
|
|
attackCurve.setLookAndFeel(nullptr);
|
|
releaseCurve.setLookAndFeel(nullptr);
|
|
gainSlider.setLookAndFeel(nullptr);
|
|
}
|
|
|
|
//==============================================================================
|
|
void JustaSampleAudioProcessorEditor::timerCallback()
|
|
{
|
|
// If the processor has a new sample loaded, update the editor
|
|
if (pluginState.sampleHash != expectedHash && p.getSampleBuffer().getNumSamples())
|
|
{
|
|
loadSample();
|
|
|
|
// The file path should be updated by this point, in which case the filenameComponent should be updated
|
|
if (filenameComponent.getCurrentFileText() != pluginState.filePath)
|
|
{
|
|
filenameComponent.setCurrentFile(juce::File(pluginState.filePath), true, juce::dontSendNotification);
|
|
pluginState.recentFiles = filenameComponent.getRecentlyUsedFilenames();
|
|
}
|
|
|
|
resized();
|
|
}
|
|
|
|
// If playback state has changed, update the sampleEditor and sampleNavigator
|
|
bool wasPlaying = currentlyPlaying;
|
|
currentlyPlaying = std::ranges::any_of(synthVoices, [](auto* voice) -> bool{ return bool(voice->getCurrentlyPlayingSound()); });
|
|
|
|
if ((wasPlaying && !currentlyPlaying) || currentlyPlaying)
|
|
{
|
|
sampleEditor.repaint();
|
|
sampleNavigator.repaint();
|
|
}
|
|
|
|
if (wasPlaying != currentlyPlaying)
|
|
{
|
|
playStopButton.setShape(currentlyPlaying ? stopPath : playPath);
|
|
playStopButton.setCustomHelpText(currentlyPlaying ? stopHelpText : playHelpText);
|
|
}
|
|
|
|
editorOverlay.setWaveformMode(CustomSamplerVoice::isWavetableModeAvailable(p.getBufferSampleRate(), pluginState.sampleStart, pluginState.sampleEnd) && !p.getRecorder().shouldRecordDevice());
|
|
|
|
// If a new device manager state has been loaded from setStateInformation, this will trigger a refresh
|
|
if (audioDeviceSettings.isVisible())
|
|
p.initializeDeviceManager();
|
|
|
|
// Handle recording changes (this must take place in a timer callback, since the recorder does not update synchronously)
|
|
if (p.getRecorder().isRecordingDevice())
|
|
{
|
|
if (!sampleEditor.isRecordingMode() || !prompt.isPromptVisible())
|
|
{
|
|
sampleEditor.setRecordingMode(true);
|
|
sampleNavigator.setRecordingMode(true);
|
|
prompt.openPrompt({ &sampleEditor }, [this] {
|
|
p.getRecorder().stopRecording();
|
|
if (!pendingRecordingBuffer.getNumSamples()) // We'd rather wait until the sample is loaded to stop showing recording
|
|
{
|
|
sampleEditor.setRecordingMode(false);
|
|
sampleNavigator.setRecordingMode(false);
|
|
}
|
|
enablementChanged();
|
|
});
|
|
}
|
|
handleActiveRecording();
|
|
}
|
|
else if (sampleEditor.isRecordingMode())
|
|
{
|
|
prompt.closePrompt();
|
|
}
|
|
|
|
// We detect sample view scroll gestures as series of continuous scroll events while the view is changing and the mouse is up
|
|
if (zoomGestureEndCallbacks >= 0)
|
|
zoomGestureEndCallbacks--;
|
|
if (mouseWheelDetected && pluginState.viewStart != lastViewStart && pluginState.viewEnd != lastViewEnd && !isMouseButtonDownAnywhere())
|
|
zoomGestureEndCallbacks = maxCallbacksSinceInZoomGesture;
|
|
|
|
if (zoomGestureEndCallbacks == 0)
|
|
dummyParam.sendUIUpdate();
|
|
|
|
mouseWheelDetected = false;
|
|
lastViewStart = pluginState.viewStart;
|
|
lastViewEnd = pluginState.viewEnd;
|
|
|
|
enablementChanged();
|
|
|
|
// This is to fix a lag issue found with Reaper and the regular software renderer
|
|
if (hostType.isReaper() && getPeer() && getPeer()->getCurrentRenderingEngine() == 0 && !openGLContext.isAttached())
|
|
openGLContext.attachTo(*getTopLevelComponent());
|
|
}
|
|
|
|
void JustaSampleAudioProcessorEditor::paint(juce::Graphics& g)
|
|
{
|
|
auto colors = getTheme();
|
|
|
|
g.fillAll(theme.light);
|
|
|
|
auto bounds = getConstrainedBounds().toFloat();
|
|
|
|
g.setColour(theme.background);
|
|
g.fillRect(bounds);
|
|
|
|
auto controls = bounds.removeFromTop(scalei(Layout::controlsHeight));
|
|
|
|
juce::Path toolbarBackground;
|
|
toolbarBackground.addRectangle(controls.toNearestInt());
|
|
|
|
g.setColour(theme.foreground);
|
|
g.fillPath(toolbarBackground);
|
|
|
|
float divTop = controls.getY() + controls.getHeight() * 0.125f;
|
|
float divBot = controls.getY() + controls.getHeight() * 0.875f;
|
|
|
|
g.setColour(theme.slate.withAlpha(0.2f));
|
|
g.drawVerticalLine((tuningDetectButton.getRight() + attackTimeRotary.getX()) / 2, divTop, divBot);
|
|
g.drawVerticalLine((attackCurve.getRight() + releaseTimeRotary.getX()) / 2, divTop, divBot);
|
|
g.drawVerticalLine((releaseCurve.getRight() + lofiModeButton.getX()) / 2, divTop, divBot);
|
|
g.drawVerticalLine((playbackSpeedRotary.getRight() + loopStartButton.getX()) / 2, divTop, divBot);
|
|
g.drawVerticalLine((loopEndButton.getRight() + monoOutputButton.getX()) / 2, divTop, divBot);
|
|
|
|
auto footerBounds = bounds.removeFromBottom(scalei(Layout::footerHeight));
|
|
g.setColour(theme.foreground);
|
|
g.fillRect(footerBounds.toNearestInt());
|
|
|
|
g.setColour(theme.slate.withAlpha(0.2f));
|
|
g.drawVerticalLine(int(std::round(showFXButton.getX() - scalei(1.f))), footerBounds.getY() + scalei(13.f), footerBounds.getBottom() - scalei(13.f));
|
|
|
|
bool fxEnabled = eqEnabled || reverbEnabled || distortionEnabled || chorusEnabled;
|
|
if (fxEnabled)
|
|
{
|
|
auto fxBounds = showFXButton.getBounds().toFloat();
|
|
auto indictatorBounds = fxBounds.removeFromTop(scalei(17.f)).removeFromRight(scalei(17.f)).translated(scalei(12.f), scalei(6.f));
|
|
g.setColour(theme.highlight);
|
|
g.fillEllipse(indictatorBounds);
|
|
}
|
|
|
|
auto sampleLoaded = bool(p.getSampleBuffer().getNumSamples());
|
|
if (pluginState.showFX)
|
|
{
|
|
bounds.removeFromBottom(scalei(Layout::fxChainHeight));
|
|
}
|
|
else if (sampleLoaded)
|
|
{
|
|
float border = scalei(2.5f);
|
|
|
|
g.setColour(theme.slate.withAlpha(0.5f));
|
|
g.fillRect(bounds.getX(), bounds.getBottom() - border / 2.f, bounds.getWidth(), border);
|
|
}
|
|
|
|
if (sampleLoaded || pluginState.showFX)
|
|
{
|
|
auto navigatorBounds = bounds.removeFromBottom(scalei(Layout::sampleNavigatorHeight));
|
|
g.setColour(theme.foreground);
|
|
g.fillRect(navigatorBounds.toNearestInt());
|
|
}
|
|
}
|
|
|
|
void EditorOverlay::paint(juce::Graphics& g)
|
|
{
|
|
auto theme = getTheme();
|
|
|
|
auto bounds = getLocalBounds().toFloat();
|
|
bounds.removeFromTop(scale(Layout::sampleControlsMargin.getY()));
|
|
|
|
auto sampleControls = bounds.removeFromTop(scale(Layout::sampleControlsHeight)).reduced(scale(Layout::sampleControlsMargin.getX()), 0.f);
|
|
auto fileControls = sampleControls.removeFromLeft(scale(Layout::fileControlsWidth));
|
|
auto playbackControls = sampleControls.removeFromRight(scale(Layout::playbackControlsWidth));
|
|
|
|
juce::Path sampleControlRegionsPath;
|
|
sampleControlRegionsPath.addRoundedRectangle(fileControls, scale(11.f));
|
|
sampleControlRegionsPath.addRoundedRectangle(playbackControls, scale(11.f));
|
|
|
|
sampleControlShadow.render(g, sampleControlRegionsPath);
|
|
g.setColour(theme.foreground);
|
|
g.fillPath(sampleControlRegionsPath);
|
|
|
|
if (waveformModeAvailable)
|
|
{
|
|
sampleControls.removeFromLeft(scale(Layout::sampleControlsMargin.x));
|
|
auto waveformModeBounds = sampleControls.removeFromLeft(scale(Layout::waveformModeWidth));
|
|
|
|
sampleControlRegionsPath.clear();
|
|
sampleControlRegionsPath.addRoundedRectangle(waveformModeBounds, scale(11.f));
|
|
|
|
sampleControlShadow.render(g, sampleControlRegionsPath);
|
|
g.setColour(disabled(theme.highlight, waveformModeDisabled));
|
|
g.fillPath(sampleControlRegionsPath);
|
|
}
|
|
|
|
auto navControls = bounds.removeFromBottom(scale(Layout::navigatorControlsSize.y)).removeFromRight(scale(Layout::navigatorControlsSize.x))
|
|
.getSmallestIntegerContainer().toFloat();
|
|
|
|
juce::Path navControlRegionsPath;
|
|
navControlRegionsPath.addRoundedRectangle(navControls.getX(), navControls.getY(),
|
|
navControls.getWidth(), navControls.getHeight(), scale(20.f), scale(20.f),
|
|
true, false, false, false);
|
|
|
|
navControlShadow.render(g, navControlRegionsPath);
|
|
g.setColour(theme.foreground);
|
|
g.fillPath(navControlRegionsPath);
|
|
}
|
|
|
|
void EditorOverlay::resized()
|
|
{
|
|
sampleControlShadow.setOffset({ int(scale(2.f)), int(scale(2.f)) });
|
|
navControlShadow.setOffset({ int(-scale(2.f)), int(-scale(2.f)) });
|
|
}
|
|
|
|
void EditorOverlay::lookAndFeelChanged()
|
|
{
|
|
auto colors = getTheme();
|
|
|
|
sampleControlShadow.setColor(colors.slate.withAlpha(0.125f));
|
|
navControlShadow.setColor(colors.slate.withAlpha(0.125f));
|
|
}
|
|
|
|
void JustaSampleAudioProcessorEditor::resized()
|
|
{
|
|
pluginState.width = getWidth();
|
|
pluginState.height = getHeight();
|
|
|
|
auto bounds = getConstrainedBounds().toFloat();
|
|
|
|
prompt.setBounds(bounds.toNearestInt());
|
|
|
|
audioDeviceSettings.setBounds(juce::Rectangle(scalef(1200.f), scalef(700.f)).withCentre(bounds.getCentre()).toNearestInt());
|
|
|
|
// Layout the controls toolbar
|
|
auto controls = bounds.removeFromTop(scalei(Layout::controlsHeight));
|
|
controls.removeFromLeft(scalei(Layout::controlsPaddingX));
|
|
controls.removeFromTop(scalei(8));
|
|
controls.removeFromBottom(scalei(10));
|
|
|
|
juce::Array labels = { &tuningLabel, &attackLabel, &releaseLabel, &playbackLabel, &loopingLabel, &masterLabel };
|
|
for (juce::Label* label : labels)
|
|
label->setFont(getInriaSans().withHeight(scalef(34.3f)));
|
|
|
|
// Tuning module
|
|
auto tuningModule = controls.removeFromLeft(scalei(Layout::tuningWidth));
|
|
|
|
auto tuningLabelBounds = tuningModule.removeFromTop(scalei(Layout::moduleLabelHeight));
|
|
tuningLabelBounds.reduce(0, scalei(Layout::moduleLabelPadding));
|
|
tuningLabel.setBounds(tuningLabelBounds.toNearestInt());
|
|
tuningModule.removeFromTop(scalei(Layout::moduleLabelGap));
|
|
|
|
// Rotaries take up a bit of extra space for the thumb, so we need to compensate by subtracting from other values
|
|
float rotarySize = scalei(Layout::standardRotarySize);
|
|
float rotaryPadding = rotarySize * Layout::rotaryPadding;
|
|
|
|
tuningModule.reduce(scalei(16.f) - rotaryPadding, scalei(6.f) - rotaryPadding);
|
|
|
|
auto semitoneBounds = tuningModule.removeFromLeft(rotarySize + 2 * rotaryPadding);
|
|
semitoneRotary.setBounds(semitoneBounds.toNearestInt());
|
|
semitoneRotary.sendLookAndFeelChange();
|
|
waveformSemitoneRotary.setBounds(semitoneBounds.toNearestInt());
|
|
waveformSemitoneRotary.sendLookAndFeelChange();
|
|
tuningModule.removeFromLeft(scalei(Layout::moduleControlsGap) - 2 * rotaryPadding);
|
|
|
|
auto centBounds = tuningModule.removeFromLeft(rotarySize + 2 * rotaryPadding);
|
|
centRotary.setBounds(centBounds.toNearestInt());
|
|
centRotary.sendLookAndFeelChange();
|
|
waveformCentRotary.setBounds(centBounds.toNearestInt());
|
|
waveformCentRotary.sendLookAndFeelChange();
|
|
tuningModule.removeFromLeft(scalei(Layout::moduleControlsGap) - rotaryPadding);
|
|
|
|
auto detectTuningBounds = tuningModule.removeFromLeft(scalei(71.f)).reduced(0.f, rotaryPadding);
|
|
tuningDetectLabel.setBounds(detectTuningBounds.removeFromTop(scalei(29.f)).expanded(scalei(0.01f), 0.f).toNearestInt());
|
|
tuningDetectLabel.setFont(getInriaSansBold().withHeight(scalef(27.5f)));
|
|
detectTuningBounds.removeFromTop(scalei(5.f));
|
|
tuningDetectButton.setBounds(detectTuningBounds.removeFromTop(scalei(46.f)).toNearestInt());
|
|
|
|
controls.removeFromLeft(scalei(Layout::moduleGap));
|
|
|
|
// Attack module
|
|
auto attackModule = controls.removeFromLeft(scalei(Layout::attackWidth));
|
|
|
|
auto attackLabelBounds = attackModule.removeFromTop(scalei(Layout::moduleLabelHeight));
|
|
attackLabelBounds.reduce(0, scalei(Layout::moduleLabelPadding));
|
|
attackLabel.setBounds(attackLabelBounds.toNearestInt());
|
|
|
|
attackModule.removeFromTop(scalei(Layout::moduleLabelGap));
|
|
attackModule.reduce(scalei(16.f) - rotaryPadding, scalei(6.f) - rotaryPadding);
|
|
|
|
auto attackTimeBounds = attackModule.removeFromLeft(rotarySize + 2 * rotaryPadding);
|
|
attackTimeRotary.setBounds(attackTimeBounds.toNearestInt());
|
|
attackTimeRotary.sendLookAndFeelChange();
|
|
attackModule.removeFromLeft(scalei(Layout::moduleControlsGap) - 2 * rotaryPadding);
|
|
|
|
auto attackCurveBounds = attackModule.removeFromLeft(scalei(58.f) + 2 * rotaryPadding).reduced(0.f, scalei(16.5f));
|
|
attackCurve.setBounds(attackCurveBounds.toNearestInt());
|
|
|
|
controls.removeFromLeft(scalei(Layout::moduleGap));
|
|
|
|
// Release module
|
|
auto releaseModule = controls.removeFromLeft(scalei(Layout::releaseWidth));
|
|
|
|
auto releaseLabelBounds = releaseModule.removeFromTop(scalei(Layout::moduleLabelHeight));
|
|
releaseLabelBounds.reduce(0, scalei(Layout::moduleLabelPadding));
|
|
releaseLabel.setBounds(releaseLabelBounds.toNearestInt());
|
|
|
|
releaseModule.removeFromTop(scalei(Layout::moduleLabelGap));
|
|
releaseModule.reduce(scalei(16.f) - rotaryPadding, scalei(6.f) - rotaryPadding);
|
|
|
|
auto releaseTimeBounds = releaseModule.removeFromLeft(rotarySize + 2 * rotaryPadding);
|
|
releaseTimeRotary.setBounds(releaseTimeBounds.toNearestInt());
|
|
releaseTimeRotary.sendLookAndFeelChange();
|
|
releaseModule.removeFromLeft(scalei(Layout::moduleControlsGap) - 2 * rotaryPadding);
|
|
|
|
auto releaseCurveBounds = releaseModule.removeFromLeft(scalei(58.f) + 2 * rotaryPadding).reduced(0.f, scalei(16.5f));
|
|
releaseCurve.setBounds(releaseCurveBounds.toNearestInt());
|
|
|
|
controls.removeFromLeft(scalei(Layout::moduleGap));
|
|
|
|
// Playback module
|
|
auto playbackModule = controls.removeFromLeft(scalei(Layout::playbackWidth));
|
|
|
|
auto playbackLabelBounds = playbackModule.removeFromTop(scalei(Layout::moduleLabelHeight));
|
|
playbackLabelBounds.reduce(0, scalei(Layout::moduleLabelPadding));
|
|
playbackLabel.setBounds(playbackLabelBounds.toNearestInt());
|
|
|
|
playbackModule.removeFromTop(scalei(Layout::moduleLabelGap));
|
|
playbackModule.reduce(scalei(16.f) - rotaryPadding, scalei(6.f) - rotaryPadding);
|
|
playbackModule.removeFromLeft(rotaryPadding);
|
|
|
|
auto lofiButtonBounds = playbackModule.removeFromLeft(scalei(71.f)).reduced(0.f, rotaryPadding + scalei(7.5f));
|
|
lofiModeButton.setBounds(lofiButtonBounds.toNearestInt());
|
|
lofiModeButton.setBorder(scalef(2.5f), scalef(6.f));
|
|
lofiModeButton.setPadding(scalef(12.f));
|
|
|
|
playbackModule.removeFromLeft(scalei(Layout::moduleControlsGap));
|
|
playbackModeButton.setBounds(playbackModule.removeFromLeft(scalef(300.f)).reduced(0.f, rotaryPadding).toNearestInt());
|
|
playbackModeButton.setBorder(scalef(2.5f), scalef(6.f));
|
|
|
|
playbackModule.removeFromLeft(scalei(Layout::moduleControlsGap) - rotaryPadding);
|
|
playbackSpeedRotary.setBounds(playbackModule.removeFromLeft(rotarySize + 2 * rotaryPadding).toNearestInt());
|
|
playbackSpeedRotary.sendLookAndFeelChange();
|
|
|
|
controls.removeFromLeft(scalei(Layout::moduleGap));
|
|
|
|
// Loop module
|
|
auto loopModule = controls.removeFromLeft(scalei(Layout::loopWidth));
|
|
|
|
auto loopingLabelBounds = loopModule.removeFromTop(scalei(Layout::moduleLabelHeight));
|
|
loopingLabelBounds.reduce(0, scalei(Layout::moduleLabelPadding));
|
|
loopingLabel.setBounds(loopingLabelBounds.toNearestInt());
|
|
|
|
loopModule.removeFromTop(scalei(Layout::moduleLabelGap));
|
|
loopModule.reduce(scalei(16.f), scalei(6.f));
|
|
|
|
loopStartButton.setBounds(loopModule.removeFromLeft(scalei(34.f)).toNearestInt());
|
|
loopStartButton.setBorder(scalef(2.5f), scalef(6.f), true, false, true, false);
|
|
loopStartButton.setPadding(scalef(8.f), scalef(4.f), scalef(7.5f), scalef(7.5f));
|
|
|
|
loopModule.removeFromLeft(scalei(6.f));
|
|
loopButton.setBounds(loopModule.removeFromLeft(scalei(105.f)).toNearestInt());
|
|
loopButton.setBorder(scalef(2.5f));
|
|
loopButton.setPadding(scalef(13.f));
|
|
|
|
loopModule.removeFromLeft(scalei(6.f));
|
|
loopEndButton.setBounds(loopModule.removeFromLeft(scalei(34.f)).toNearestInt());
|
|
loopEndButton.setBorder(scalef(2.5f), scalef(6.f), false, true, false, true);
|
|
loopEndButton.setPadding(scalef(4.f), scalef(8.f), scalef(7.5f), scalef(7.5f));
|
|
|
|
controls.removeFromLeft(scalei(Layout::moduleGap));
|
|
|
|
// Master module
|
|
auto masterModule = controls.removeFromLeft(scalei(Layout::masterWidth));
|
|
|
|
auto masterLabelBounds = masterModule.removeFromTop(scalei(Layout::moduleLabelHeight));
|
|
masterLabelBounds.reduce(0, scalei(Layout::moduleLabelPadding));
|
|
masterLabel.setBounds(masterLabelBounds.toNearestInt());
|
|
|
|
masterModule.removeFromTop(scalei(Layout::moduleLabelGap));
|
|
masterModule.reduce(scalei(16.f), 0.f);
|
|
|
|
auto monoButtonBounds = masterModule.removeFromLeft(scalei(92.f)).reduced(0.f, scalei(27.5f));
|
|
monoOutputButton.setBounds(monoButtonBounds.toNearestInt());
|
|
monoOutputButton.setBorder(scalef(2.5f), scalef(6.f));
|
|
monoOutputButton.setPadding(scalef(12.f));
|
|
|
|
masterModule.removeFromLeft(scalei(Layout::moduleControlsGap));
|
|
gainSlider.setBounds(masterModule.removeFromLeft(scalei(153.f)).toNearestInt());
|
|
gainSlider.sendLookAndFeelChange();
|
|
|
|
// Footer
|
|
auto footer = bounds.removeFromBottom(scalei(Layout::footerHeight));
|
|
footer.removeFromBottom(scalei(2.f));
|
|
|
|
footer.reduce(scalei(25.f), 0.f);
|
|
|
|
auto showFXButtonBounds = footer.removeFromRight(scalei(205.f));
|
|
showFXButton.setPadding(0.f, 0.f, scalei(19.f), scalei(19.f));
|
|
showFXButton.setBounds(showFXButtonBounds.toNearestInt());
|
|
|
|
footer.removeFromRight(scalei(30.f));
|
|
|
|
auto preFXButtonBounds = footer.removeFromRight(scalei(100.f)).reduced(0.f, scalei(11.f));
|
|
preFXButton.setBounds(preFXButtonBounds.toNearestInt());
|
|
preFXButton.setBorder(scalef(2.5f), scalef(6.f));
|
|
preFXButton.setPadding(scalef(11.f));
|
|
|
|
versionInfo.setFont(getInter().withHeight(scalef(28.f)));
|
|
versionInfo.setBounds(footer.reduced(0.f, scalei(18.f)).toNearestInt());
|
|
|
|
// FX
|
|
if (pluginState.showFX)
|
|
{
|
|
auto fxChainBounds = bounds.removeFromBottom(scalei(Layout::fxChainHeight));
|
|
fxChain.setBounds(fxChainBounds.toNearestInt());
|
|
}
|
|
fxChain.setVisible(pluginState.showFX);
|
|
|
|
// Navigator
|
|
auto sampleLoaded = bool(p.getSampleBuffer().getNumSamples());
|
|
if (sampleLoaded || pluginState.showFX)
|
|
{
|
|
auto sampleNavigatorBounds = bounds.removeFromBottom(scalei(Layout::sampleNavigatorHeight));
|
|
sampleNavigator.setBounds(sampleNavigatorBounds.toNearestInt());
|
|
}
|
|
|
|
// Editor
|
|
auto editorBounds = bounds;
|
|
sampleEditor.setBounds(editorBounds.toNearestInt());
|
|
editorOverlay.setBounds(editorBounds.toNearestInt());
|
|
|
|
statusLabel.setFont(getInter().withHeight(scalef(50.f)));
|
|
updateLabel();
|
|
|
|
// Sample controls
|
|
editorBounds.removeFromTop(scalei(Layout::sampleControlsMargin.getY()));
|
|
|
|
auto sampleControls = editorBounds.removeFromTop(scalei(Layout::sampleControlsHeight)).reduced(scalei(Layout::sampleControlsMargin.getX()), 0.f);
|
|
auto fileControls = sampleControls.removeFromLeft(scalei(Layout::fileControlsWidth));
|
|
auto playbackControls = sampleControls.removeFromRight(scalei(Layout::playbackControlsWidth));
|
|
|
|
fileControls.reduce(scalei(12.f), scalei(2.1f));
|
|
auto filenameComponentBounds = fileControls.removeFromLeft(scalei(1095.f));
|
|
filenameComponent.setBounds(filenameComponentBounds.toNearestInt());
|
|
|
|
fileControls.removeFromLeft(scalei(20.f));
|
|
auto linkSampleToggleBounds = fileControls.removeFromLeft(scalei(48.f)).reduced(0.f, scalei(2.85f));
|
|
linkSampleToggle.setBounds(linkSampleToggleBounds.toNearestInt());
|
|
linkSampleToggle.setPadding(scalef(3.f));
|
|
linkSampleToggle.setBorder(0.f, scalef(5.f));
|
|
|
|
sampleControls.removeFromLeft(scalei(Layout::sampleControlsMargin.x));
|
|
auto waveformModeBounds = sampleControls.removeFromLeft(scalei(Layout::waveformModeWidth));
|
|
|
|
waveformModeLabel.setBounds(waveformModeBounds.toNearestInt());
|
|
waveformModeLabel.setPadding(scalef(12.1f));
|
|
|
|
playbackControls.reduce(scalei(12.f), scalei(2.1f));
|
|
auto playStopButtonBounds = playbackControls.removeFromLeft(scalei(40.32f)).reduced(scalei(4.98f), scalei(3.36f));
|
|
playStopButton.setBounds(playStopButtonBounds.toNearestInt());
|
|
|
|
playbackControls.removeFromLeft(scalei(14.f));
|
|
auto recordControlBounds = playbackControls.removeFromLeft(scalei(58.6f)).reduced(scalei(2.f), scalei(3.36f));
|
|
auto recordButtonBounds = recordControlBounds.removeFromLeft(scalei(33.6f));
|
|
recordButton.setBounds(recordButtonBounds.toNearestInt());
|
|
|
|
recordControlBounds.removeFromLeft(scalei(4.f));
|
|
auto deviceSettingsButtonBounds = recordControlBounds.removeFromTop(scalei(23.f));
|
|
deviceSettingsButton.setBounds(deviceSettingsButtonBounds.toNearestInt());
|
|
|
|
auto navControlBounds = bounds.removeFromBottom(scalei(Layout::navigatorControlsSize.y)).removeFromRight(scalei(Layout::navigatorControlsSize.x));
|
|
navControlBounds.removeFromLeft(scalei(20.f));
|
|
|
|
auto fitButtonBounds = navControlBounds.removeFromLeft(scalei(34.f));
|
|
fitButtonBounds.removeFromTop(scalei(10.f));
|
|
fitButtonBounds.removeFromBottom(scalei(5.f));
|
|
fitButton.setBounds(fitButtonBounds.toNearestInt());
|
|
|
|
navControlBounds.removeFromLeft(scalei(12.f));
|
|
auto pinButtonBounds = navControlBounds.removeFromLeft(scalei(44.f));
|
|
pinButtonBounds.removeFromTop(scalei(7.5f));
|
|
pinButtonBounds.removeFromBottom(scalei(2.5f));
|
|
pinButton.setBounds(pinButtonBounds.toNearestInt());
|
|
pinButton.setBorder(0.f, scalef(5.f));
|
|
pinButton.setPadding(scalef(5.f));
|
|
|
|
repaint();
|
|
}
|
|
|
|
void JustaSampleAudioProcessorEditor::lookAndFeelChanged()
|
|
{
|
|
const auto shadow = theme.dark.withAlpha(0.25f);
|
|
|
|
lofiModeButton.setColors(theme.dark, theme.light, shadow);
|
|
loopButton.setColors(theme.dark, theme.loop, shadow);
|
|
loopStartButton.setColors(theme.dark, theme.loop, shadow);
|
|
loopEndButton.setColors(theme.dark, theme.loop, shadow);
|
|
monoOutputButton.setColors(theme.dark, theme.light, shadow);
|
|
linkSampleToggle.setColors(theme.dark, theme.slate.withAlpha(0.5f), shadow);
|
|
waveformModeLabel.setColors(theme.foreground, theme.slate.withAlpha(0.f), shadow);
|
|
pinButton.setColors(theme.dark, theme.slate.withAlpha(0.5f), shadow);
|
|
preFXButton.setColors(theme.darkerSlate, theme.light, shadow);
|
|
showFXButton.setColors(theme.dark, theme.slate.withAlpha(0.f), shadow);
|
|
|
|
tuningDetectButton.setColor(theme.dark);
|
|
playStopButton.setColor(theme.dark);
|
|
recordButton.setColor(theme.highlight);
|
|
deviceSettingsButton.setColor(theme.dark);
|
|
fitButton.setColor(theme.dark);
|
|
|
|
versionInfo.setColour(juce::Label::textColourId, theme.dark);
|
|
|
|
playbackModeButton.setColors(theme.slate, theme.light, shadow);
|
|
|
|
statusLabel.setColour(juce::Label::textColourId, theme.dark);
|
|
statusLabel.setColour(juce::Label::backgroundColourId, theme.background.withAlpha(0.85f));
|
|
sampleNavigator.setColour(Colors::painterColorId, theme.darkerSlate);
|
|
|
|
juce::Array<Component*> foregroundComponents = {
|
|
&tuningLabel, &attackLabel, &releaseLabel, &playbackLabel, &loopingLabel, &masterLabel, &semitoneRotary, &waveformSemitoneRotary, ¢Rotary, &waveformCentRotary, &tuningDetectLabel, &tuningDetectButton,
|
|
&attackTimeRotary, &attackCurve, &releaseTimeRotary, &releaseCurve, &lofiModeButton, &playbackModeButton, &playbackSpeedRotary, &loopStartButton, &loopButton, &loopEndButton,
|
|
&monoOutputButton, &gainSlider, &filenameComponent, &linkSampleToggle, &playStopButton, &recordButton, &deviceSettingsButton, &fitButton, &pinButton, &sampleNavigator, &preFXButton, &showFXButton
|
|
};
|
|
|
|
for (Component* component : foregroundComponents)
|
|
component->setColour(Colors::backgroundColorId, theme.foreground);
|
|
}
|
|
|
|
void JustaSampleAudioProcessorEditor::mouseDown(const juce::MouseEvent& event)
|
|
{
|
|
auto parent = event.originalComponent->getParentComponent();
|
|
for (auto rotary : rotaries)
|
|
if (parent == rotary)
|
|
rotary->mouseDown(event.getEventRelativeTo(rotary));
|
|
}
|
|
|
|
void JustaSampleAudioProcessorEditor::mouseUp(const juce::MouseEvent& event)
|
|
{
|
|
auto parent = event.originalComponent->getParentComponent();
|
|
for (auto rotary : rotaries)
|
|
if (parent == rotary)
|
|
rotary->mouseUp(event.getEventRelativeTo(rotary));
|
|
}
|
|
|
|
void JustaSampleAudioProcessorEditor::mouseDrag(const juce::MouseEvent& event)
|
|
{
|
|
auto parent = event.originalComponent->getParentComponent();
|
|
for (auto rotary : rotaries)
|
|
if (parent == rotary)
|
|
rotary->mouseDrag(event.getEventRelativeTo(rotary));
|
|
|
|
}
|
|
|
|
void JustaSampleAudioProcessorEditor::mouseWheelMove(const juce::MouseEvent&, const juce::MouseWheelDetails&)
|
|
{
|
|
mouseWheelDetected = true;
|
|
}
|
|
|
|
void JustaSampleAudioProcessorEditor::setTheme(const Colors newTheme)
|
|
{
|
|
theme = newTheme;
|
|
lnf.setTheme(theme);
|
|
attackCurveLNF.setTheme(theme);
|
|
releaseCurveLNF.setTheme(theme);
|
|
gainSliderLNF.setTheme(theme);
|
|
|
|
enablementChanged();
|
|
sendLookAndFeelChange();
|
|
}
|
|
|
|
Colors JustaSampleAudioProcessorEditor::getTheme() const
|
|
{
|
|
return theme;
|
|
}
|
|
|
|
juce::Rectangle<int> JustaSampleAudioProcessorEditor::getConstrainedBounds() const
|
|
{
|
|
auto fxSpace = !pluginState.showFX ? scalef(Layout::fxChainHeight) : 0;
|
|
|
|
auto bounds = getLocalBounds();
|
|
auto width = getWidth();
|
|
auto height = getHeight();
|
|
auto constrainedWidth = int(juce::jlimit<float>((height + fxSpace) * 0.8f, (height + fxSpace) * 2.f, float(width)));
|
|
auto constrainedHeight = int(width / 0.8f - fxSpace);
|
|
|
|
if (constrainedWidth < width)
|
|
bounds.reduce((width - constrainedWidth) / 2, 0);
|
|
else if (constrainedHeight < height)
|
|
bounds.reduce(0, (height - constrainedHeight) / 2);
|
|
|
|
return bounds;
|
|
}
|
|
|
|
//==============================================================================
|
|
void JustaSampleAudioProcessorEditor::loadSample()
|
|
{
|
|
bool sampleLoaded = p.getSampleBuffer().getNumSamples();
|
|
if (sampleLoaded)
|
|
{
|
|
expectedHash = pluginState.sampleHash;
|
|
linkSampleToggle.setToggleState(pluginState.usingFileReference, juce::dontSendNotification);
|
|
|
|
bool userLoad = userDraggedSample || p.hasLoadedFromReaper();
|
|
sampleEditor.setSample(p.getSampleBuffer(), p.getBufferSampleRate(), userLoad);
|
|
sampleNavigator.setSample(p.getSampleBuffer(), p.getBufferSampleRate(), userLoad);
|
|
dummyParam.sendUIUpdate();
|
|
}
|
|
sampleEditor.setRecordingMode(false);
|
|
sampleNavigator.setRecordingMode(false);
|
|
prompt.closePrompt();
|
|
userDraggedSample = false;
|
|
p.setLoadedFromReaper(false);
|
|
}
|
|
|
|
void JustaSampleAudioProcessorEditor::handleActiveRecording()
|
|
{
|
|
auto& recordingBuffer = p.getRecorder().getRecordingBufferQueue();
|
|
RecordingBufferChange* bufferChange = recordingBuffer.peek();
|
|
|
|
// These variables are to deal with the GUI sample updates
|
|
int previousSampleSize = recordingBufferSize;
|
|
int previousBufferSize = pendingRecordingBuffer.getNumSamples();
|
|
int iterations = 0;
|
|
while (bufferChange && iterations < 5) // 5 is a safety measure
|
|
{
|
|
iterations++;
|
|
switch (bufferChange->type)
|
|
{
|
|
case RecordingBufferChange::CLEAR:
|
|
pendingRecordingBuffer.setSize(1, 0);
|
|
recordingBufferSize = 0;
|
|
break;
|
|
case RecordingBufferChange::ADD:
|
|
pendingRecordingBuffer.setSize(
|
|
1,
|
|
pendingRecordingBuffer.getNumSamples() < recordingBufferSize + bufferChange->addedBuffer.getNumSamples() ? juce::jmax<int>(recordingBufferSize + bufferChange->addedBuffer.getNumSamples(), 2 * pendingRecordingBuffer.getNumSamples()) :
|
|
pendingRecordingBuffer.getNumSamples(),
|
|
true, true, false
|
|
);
|
|
pendingRecordingBuffer.copyFrom(0, recordingBufferSize, bufferChange->addedBuffer, 0, 0, bufferChange->addedBuffer.getNumSamples());
|
|
recordingBufferSize += bufferChange->addedBuffer.getNumSamples();
|
|
break;
|
|
}
|
|
recordingBuffer.pop();
|
|
bufferChange = recordingBuffer.peek();
|
|
}
|
|
|
|
if (pendingRecordingBuffer.getNumSamples() != previousBufferSize) // Update when the buffer size changes
|
|
{
|
|
sampleEditor.setSample(pendingRecordingBuffer, 0.f, false);
|
|
sampleNavigator.setSample(pendingRecordingBuffer, 0.f, false);
|
|
}
|
|
else if (previousSampleSize != recordingBufferSize) // Update the rendered paths when more data is added to the buffer (not changing the size)
|
|
{
|
|
sampleEditor.sampleUpdated(previousSampleSize, recordingBufferSize);
|
|
sampleNavigator.sampleUpdated(previousSampleSize, recordingBufferSize);
|
|
}
|
|
}
|
|
|
|
void JustaSampleAudioProcessorEditor::toggleLinkSample()
|
|
{
|
|
// If the file path is empty or the file doesn't exist, prompt the user to save the sample, otherwise toggle the parameter
|
|
if (!pluginState.usingFileReference && (pluginState.filePath.load().isEmpty() || !juce::File(pluginState.filePath).existsAsFile()))
|
|
{
|
|
p.openFileChooser("Save the sample to a file",
|
|
juce::FileBrowserComponent::saveMode | juce::FileBrowserComponent::canSelectFiles, [this](const juce::FileChooser& chooser) -> void {
|
|
juce::File file = chooser.getResult();
|
|
auto fileStream = std::make_unique<juce::FileOutputStream>(file);
|
|
if (file.hasWriteAccess() && fileStream->openedOk())
|
|
{
|
|
// Write the sample to file
|
|
fileStream->setPosition(0);
|
|
fileStream->truncate();
|
|
|
|
juce::WavAudioFormat wavFormat;
|
|
auto options = juce::AudioFormatWriterOptions{}
|
|
.withSampleRate(p.getBufferSampleRate())
|
|
.withNumChannels(p.getSampleBuffer().getNumChannels())
|
|
.withBitsPerSample(PluginParameters::STORED_BITRATE);
|
|
|
|
std::unique_ptr<juce::OutputStream> outputStream = std::move(fileStream);
|
|
auto formatWriter = wavFormat.createWriterFor(outputStream, options);
|
|
formatWriter->writeFromAudioSampleBuffer(p.getSampleBuffer(), 0, p.getSampleBuffer().getNumSamples());
|
|
outputStream.release();
|
|
|
|
const juce::String& filename = file.getFullPathName();
|
|
pluginState.filePath = filename;
|
|
linkSampleToggle.setToggleState(true, juce::dontSendNotification);
|
|
filenameComponent.setCurrentFile(file, true, juce::dontSendNotification);
|
|
pluginState.usingFileReference = linkSampleToggle.getToggleState();
|
|
}
|
|
}, true);
|
|
linkSampleToggle.setToggleState(false, juce::dontSendNotification);
|
|
}
|
|
else
|
|
{
|
|
pluginState.usingFileReference = linkSampleToggle.getToggleState();
|
|
}
|
|
}
|
|
|
|
void JustaSampleAudioProcessorEditor::startRecording(bool promptSettings)
|
|
{
|
|
if (p.getRecorder().shouldRecordDevice())
|
|
return;
|
|
|
|
p.initializeDeviceManager();
|
|
|
|
if (p.getDeviceManager().getCurrentAudioDevice() && p.getDeviceManager().getCurrentAudioDevice()->getActiveInputChannels().countNumberOfSetBits())
|
|
{
|
|
p.getRecorder().startRecording(true);
|
|
}
|
|
else if (promptSettings)
|
|
{
|
|
promptDeviceSettings(true);
|
|
}
|
|
}
|
|
|
|
void JustaSampleAudioProcessorEditor::promptPitchDetection()
|
|
{
|
|
prompt.openPrompt({ &sampleEditor }, [this] {
|
|
sampleEditor.cancelBoundsSelection();
|
|
enablementChanged();
|
|
});
|
|
sampleEditor.promptBoundsSelection([this](int startPos, int endPos) -> void {
|
|
p.startPitchDetectionRoutine(startPos, endPos);
|
|
enablementChanged();
|
|
prompt.closePrompt();
|
|
});
|
|
}
|
|
|
|
void JustaSampleAudioProcessorEditor::promptDeviceSettings(bool recordOnClose)
|
|
{
|
|
audioDeviceSettings.setVisible(true);
|
|
prompt.openPrompt({ &audioDeviceSettings }, [this, recordOnClose]() -> void
|
|
{
|
|
audioDeviceSettings.setVisible(false);
|
|
if (recordOnClose)
|
|
startRecording(false);
|
|
dummyParam.sendUIUpdate();
|
|
});
|
|
}
|
|
|
|
void JustaSampleAudioProcessorEditor::enablementChanged()
|
|
{
|
|
auto isSampleLoaded = bool(p.getSampleBuffer().getNumSamples());
|
|
auto isRecording = p.getRecorder().shouldRecordDevice() || sampleEditor.isInBoundsSelection(); // For UI purposes, treat bounds selection like recording
|
|
auto isLoading = p.getSampleLoader().isLoading();
|
|
auto isWaveformModeAvailable = CustomSamplerVoice::isWavetableModeAvailable(p.getBufferSampleRate(), pluginState.sampleStart, pluginState.sampleEnd) && !isRecording;
|
|
auto isWaveformMode = isWaveformModeAvailable && !p.p(PluginParameters::DISABLE_WAVETABLE_MODE);
|
|
|
|
// Note, it is very important that a sample's enablement is set to its correct value in one step, since internally JUCE repaints when the enablement changes
|
|
juce::Array<Component*> sampleDependentComponents = {
|
|
&semitoneRotary, ¢Rotary, &waveformSemitoneRotary, &waveformCentRotary, &tuningDetectLabel, &tuningDetectButton,
|
|
&attackTimeRotary, &attackCurve, &releaseTimeRotary, &releaseCurve, &gainSlider,
|
|
&playStopButton, &fitButton, &pinButton, &sampleEditor, &sampleNavigator, &fxChain, &showFXButton
|
|
};
|
|
|
|
juce::Array<Component*> notWaveformModeComponents = { &playbackModeButton, &loopStartButton, &loopButton, &loopEndButton, &tuningDetectLabel, &tuningDetectButton };
|
|
|
|
juce::Array<Component*> notLoadingDependentComponents = { &tuningDetectButton, &tuningDetectLabel, &recordButton, &deviceSettingsButton };
|
|
|
|
juce::Array<Component*> notRecordingDependentComponents = { &filenameComponent, &playStopButton, &recordButton, &deviceSettingsButton, &fitButton, &pinButton, &sampleNavigator };
|
|
|
|
// Set enablement on both
|
|
juce::SortedSet<Component*> all;
|
|
all.addArray(sampleDependentComponents.getRawDataPointer(), sampleDependentComponents.size());
|
|
all.addArray(notWaveformModeComponents.getRawDataPointer(), notWaveformModeComponents.size());
|
|
all.addArray(notLoadingDependentComponents.getRawDataPointer(), notLoadingDependentComponents.size());
|
|
all.addArray(notRecordingDependentComponents.getRawDataPointer(), notRecordingDependentComponents.size());
|
|
|
|
for (auto component : all)
|
|
{
|
|
auto sampleDependent = sampleDependentComponents.contains(component);
|
|
auto notRecordingDependent = notRecordingDependentComponents.contains(component);
|
|
auto notLoadingDependent = notLoadingDependentComponents.contains(component);
|
|
auto notWaveformModeDependent = notWaveformModeComponents.contains(component);
|
|
|
|
component->setEnabled((isSampleLoaded || !sampleDependent) && (!isRecording || !notRecordingDependent) &&
|
|
((isSampleLoaded && !isWaveformMode) || !notWaveformModeDependent) && (!isLoading || !notLoadingDependent));
|
|
}
|
|
|
|
// Some controls need more specialized rules...
|
|
semitoneRotary.setVisible(!isWaveformMode);
|
|
centRotary.setVisible(!isWaveformMode);
|
|
waveformSemitoneRotary.setVisible(isWaveformMode);
|
|
waveformCentRotary.setVisible(isWaveformMode);
|
|
|
|
lofiModeButton.setEnabled(isSampleLoaded && (!playbackModeButton.getChoice() || isWaveformMode));
|
|
|
|
playbackSpeedRotary.setEnabled(isSampleLoaded && playbackModeButton.getChoice() && !isWaveformMode);
|
|
monoOutputButton.setEnabled(isSampleLoaded && p.getSampleBuffer().getNumChannels() > 1);
|
|
|
|
linkSampleToggle.setEnabled(isSampleLoaded && !isRecording && !p.sampleBufferNeedsReference());
|
|
|
|
waveformModeLabel.setVisible(isSampleLoaded && isWaveformModeAvailable);
|
|
editorOverlay.setWaveformMode(isSampleLoaded && isWaveformModeAvailable);
|
|
|
|
const auto navigatorWasVisible = sampleNavigator.isVisible();
|
|
sampleNavigator.setVisible(isSampleLoaded || pluginState.showFX);
|
|
if (navigatorWasVisible != sampleNavigator.isVisible())
|
|
resized();
|
|
|
|
preFXButton.setEnabled(isSampleLoaded && (reverbEnabled || distortionEnabled || chorusEnabled || eqEnabled)),
|
|
|
|
// Handle status label
|
|
statusLabel.setVisible(!p.getSampleBuffer().getNumSamples() || p.getSampleLoader().isLoading() || sampleEditor.isInBoundsSelection() || sampleEditor.isRecordingMode() || fileDragging);
|
|
statusLabel.setColour(juce::Label::outlineColourId, theme.dark.withAlpha(float(fileDragging)));
|
|
if (p.getSampleLoader().isLoading())
|
|
updateLabel("Loading...");
|
|
else if (sampleEditor.isInBoundsSelection())
|
|
updateLabel("Select a region to analyze");
|
|
else if (sampleEditor.isRecordingMode())
|
|
updateLabel("Recording...");
|
|
else if (!p.getSampleBuffer().getNumSamples())
|
|
updateLabel("Drop a sample!");
|
|
else if (fileDragging)
|
|
updateLabel("Replace sample");
|
|
}
|
|
|
|
void JustaSampleAudioProcessorEditor::fxEnablementChanged()
|
|
{
|
|
enablementChanged();
|
|
repaint(showFXButton.getBounds().expanded(int(scalei(15.f)), 0));
|
|
}
|
|
|
|
//==============================================================================
|
|
bool JustaSampleAudioProcessorEditor::isInterestedInFile(const juce::String& file) const
|
|
{
|
|
return p.canLoadFileExtension(file) && !prompt.isPromptVisible();
|
|
}
|
|
|
|
bool JustaSampleAudioProcessorEditor::isInterestedInFileDrag(const juce::StringArray& files)
|
|
{
|
|
return std::ranges::any_of(files, [this](const auto& f) { return isInterestedInFile(f); }) && !p.getRecorder().shouldRecordDevice();
|
|
}
|
|
|
|
void JustaSampleAudioProcessorEditor::filesDropped(const juce::StringArray& files, int, int)
|
|
{
|
|
for (const juce::String& file : files)
|
|
{
|
|
if (isInterestedInFileDrag(file))
|
|
{
|
|
fileDragging = false;
|
|
enablementChanged();
|
|
userDraggedSample = true;
|
|
p.loadSampleFromPath(file, true, "", false,
|
|
[this](bool loaded) { if (!loaded) userDraggedSample = false; });
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void JustaSampleAudioProcessorEditor::filenameComponentChanged(juce::FilenameComponent* fileComponentThatHasChanged)
|
|
{
|
|
juce::File file = fileComponentThatHasChanged->getCurrentFile();
|
|
userDraggedSample = true;
|
|
p.loadSampleFromPath(file.getFullPathName(), true, "", false, [this, fileComponentThatHasChanged, file](bool fileLoaded) -> void
|
|
{
|
|
if (!fileLoaded)
|
|
{
|
|
fileComponentThatHasChanged->setCurrentFile(juce::File(pluginState.filePath), false, juce::dontSendNotification);
|
|
auto recentFiles = fileComponentThatHasChanged->getRecentlyUsedFilenames();
|
|
recentFiles.removeString(file.getFullPathName());
|
|
fileComponentThatHasChanged->setRecentlyUsedFilenames(recentFiles);
|
|
userDraggedSample = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
void JustaSampleAudioProcessorEditor::fileDragEnter(const juce::StringArray& files, int /*x*/, int /*y*/)
|
|
{
|
|
if (isInterestedInFileDrag(files))
|
|
{
|
|
fileDragging = true;
|
|
enablementChanged();
|
|
}
|
|
}
|
|
|
|
void JustaSampleAudioProcessorEditor::fileDragExit(const juce::StringArray&)
|
|
{
|
|
fileDragging = false;
|
|
enablementChanged();
|
|
}
|
|
|
|
void JustaSampleAudioProcessorEditor::updateLabel(const juce::String& text)
|
|
{
|
|
if (text.isNotEmpty())
|
|
statusLabel.setText(text, juce::dontSendNotification);
|
|
|
|
auto labelWidth = juce::TextLayout::getStringWidth(statusLabel.getFont(), statusLabel.getText());
|
|
statusLabel.setBounds(juce::Rectangle(0, 0, int(labelWidth), int(statusLabel.getFont().getHeight()))
|
|
.withCentre(sampleEditor.getBounds().getCentre()).expanded(int(scalei(40.f))).toNearestInt());
|
|
}
|