/* ============================================================================== 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(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); versionInfo.setJustificationType(juce::Justification::centredLeft); 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()); 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); preFXButton.setVisible(false); showFXButton.setVisible(false); 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 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 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((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(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(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 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 sampleDependentComponents = { &semitoneRotary, ¢Rotary, &waveformSemitoneRotary, &waveformCentRotary, &tuningDetectLabel, &tuningDetectButton, &attackTimeRotary, &attackCurve, &releaseTimeRotary, &releaseCurve, &gainSlider, &playStopButton, &fitButton, &pinButton, &sampleEditor, &sampleNavigator, &fxChain, &showFXButton }; juce::Array notWaveformModeComponents = { &playbackModeButton, &loopStartButton, &loopButton, &loopEndButton, &tuningDetectLabel, &tuningDetectButton }; juce::Array notLoadingDependentComponents = { &tuningDetectButton, &tuningDetectLabel, &recordButton, &deviceSettingsButton }; juce::Array notRecordingDependentComponents = { &filenameComponent, &playStopButton, &recordButton, &deviceSettingsButton, &fitButton, &pinButton, &sampleNavigator }; // Set enablement on both juce::SortedSet 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()); }