2026-07-23 23:40:48 +02:00
/*
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
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 ) ,
2026-07-24 13:09:52 +02:00
pingPongButton ( true ) ,
2026-07-23 23:40:48 +02:00
loopAttachment ( p . APVTS ( ) , PluginParameters : : IS_LOOPING , loopButton ) ,
loopStartAttachment ( p . APVTS ( ) , PluginParameters : : LOOPING_HAS_START , loopStartButton ) ,
loopEndAttachment ( p . APVTS ( ) , PluginParameters : : LOOPING_HAS_END , loopEndButton ) ,
2026-07-24 13:09:52 +02:00
pingPongAttachment ( p . APVTS ( ) , PluginParameters : : IS_PING_PONG , pingPongButton ) ,
crossfadeAttachment ( p . APVTS ( ) , PluginParameters : : CROSSFADE_SAMPLES , crossfadeRotary ) ,
2026-07-23 23:40:48 +02:00
// 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 ) ;
2026-07-24 13:09:52 +02:00
pingPongButton . useShape ( getOutlineFromSVG ( BinaryData : : IconPingPong_svg ) ) ;
pingPongButton . setOwnerButton ( & loopButton ) ;
pingPongButton . setHelpText ( " Ping-pong loop " ) ;
addAndMakeVisible ( & pingPongButton ) ;
crossfadeRotary . getProperties ( ) . set ( ComponentProps : : ROTARY_UNIT , " samp " ) ;
crossfadeRotary . setHelpText ( " Loop crossfade length " ) ;
addAndMakeVisible ( & crossfadeRotary ) ;
2026-07-23 23:40:48 +02:00
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 ) ;
2026-07-24 13:09:52 +02:00
rotaries = { & semitoneRotary , & waveformSemitoneRotary , & centRotary , & waveformCentRotary , & attackTimeRotary , & releaseTimeRotary , & playbackSpeedRotary , & crossfadeRotary } ;
2026-07-23 23:40:48 +02:00
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 ) ;
2026-07-24 02:59:31 +02:00
versionInfo . setJustificationType ( juce : : Justification : : centredLeft ) ;
2026-07-23 23:40:48 +02:00
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 ) ) ;
2026-07-24 13:09:52 +02:00
loopModule . reduce ( scalei ( 16.f ) - rotaryPadding , scalei ( 6.f ) - rotaryPadding ) ;
2026-07-23 23:40:48 +02:00
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 ) ) ;
2026-07-24 13:09:52 +02:00
loopButton . setBounds ( loopModule . removeFromLeft ( scalei ( 65.f ) ) . toNearestInt ( ) ) ;
2026-07-23 23:40:48 +02:00
loopButton . setBorder ( scalef ( 2.5f ) ) ;
loopButton . setPadding ( scalef ( 13.f ) ) ;
2026-07-24 13:09:52 +02:00
loopModule . removeFromLeft ( scalei ( 3.f ) ) ;
pingPongButton . setBounds ( loopModule . removeFromLeft ( scalei ( 34.f ) ) . toNearestInt ( ) ) ;
pingPongButton . setBorder ( scalef ( 2.5f ) , scalef ( 6.f ) ) ;
pingPongButton . setPadding ( scalef ( 8.f ) ) ;
2026-07-23 23:40:48 +02:00
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 ) ) ;
2026-07-24 13:09:52 +02:00
loopModule . removeFromLeft ( scalei ( 6.f ) ) ;
auto crossfadeRotaryBounds = loopModule . removeFromLeft ( rotarySize + 2 * rotaryPadding ) ;
crossfadeRotary . setBounds ( crossfadeRotaryBounds . toNearestInt ( ) ) ;
crossfadeRotary . sendLookAndFeelChange ( ) ;
2026-07-23 23:40:48 +02:00
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 ) ;
2026-07-24 02:59:31 +02:00
preFXButton . setVisible ( false ) ;
showFXButton . setVisible ( false ) ;
2026-07-23 23:40:48 +02:00
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 ) ;
2026-07-24 13:09:52 +02:00
pingPongButton . setColors ( theme . dark , theme . loop , shadow ) ;
2026-07-23 23:40:48 +02:00
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 , & centRotary , & waveformCentRotary , & tuningDetectLabel , & tuningDetectButton ,
2026-07-24 13:09:52 +02:00
& attackTimeRotary , & attackCurve , & releaseTimeRotary , & releaseCurve , & lofiModeButton , & playbackModeButton , & playbackSpeedRotary , & loopStartButton , & loopButton , & loopEndButton , & pingPongButton , & crossfadeRotary ,
2026-07-23 23:40:48 +02:00
& 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 , & centRotary , & waveformSemitoneRotary , & waveformCentRotary , & tuningDetectLabel , & tuningDetectButton ,
& attackTimeRotary , & attackCurve , & releaseTimeRotary , & releaseCurve , & gainSlider ,
& playStopButton , & fitButton , & pinButton , & sampleEditor , & sampleNavigator , & fxChain , & showFXButton
} ;
2026-07-24 13:09:52 +02:00
juce : : Array < Component * > notWaveformModeComponents = { & playbackModeButton , & loopStartButton , & loopButton , & loopEndButton , & pingPongButton , & crossfadeRotary , & tuningDetectLabel , & tuningDetectButton } ;
2026-07-23 23:40:48 +02:00
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 ( ) ) ;
}