From 3b5c5d2593e5b1caffc223dbebd2c54f1e304561 Mon Sep 17 00:00:00 2001 From: Jim Vanaria <22546376+jvanaria@users.noreply.github.com> Date: Sun, 12 Apr 2026 16:43:23 -0500 Subject: [PATCH] Added UI to SupaTrigga --- cmake/CommonSettings.cmake | 1 + supatrigga/CMakeLists.txt | 1 + supatrigga/Source/PluginProcessor.cpp | 919 ++++++++++++------------- supatrigga/Source/PluginProcessor.h | 196 +++--- supatrigga/Source/SupaTriggaEditor.cpp | 326 +++++++++ supatrigga/Source/SupaTriggaEditor.h | 78 +++ supatrigga/Source/Theme.h | 34 + 7 files changed, 989 insertions(+), 566 deletions(-) create mode 100644 supatrigga/Source/SupaTriggaEditor.cpp create mode 100644 supatrigga/Source/SupaTriggaEditor.h create mode 100644 supatrigga/Source/Theme.h diff --git a/cmake/CommonSettings.cmake b/cmake/CommonSettings.cmake index 82620b0..a1841c1 100644 --- a/cmake/CommonSettings.cmake +++ b/cmake/CommonSettings.cmake @@ -3,6 +3,7 @@ # macOS deployment target (must be before project()) set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13" CACHE STRING "Minimum macOS version") +set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "Build architectures for Mac OS X") # Build universal binaries on macOS (arm64 + x86_64) set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "Build architectures for Mac OS X") diff --git a/supatrigga/CMakeLists.txt b/supatrigga/CMakeLists.txt index 3c09a98..a4ac146 100644 --- a/supatrigga/CMakeLists.txt +++ b/supatrigga/CMakeLists.txt @@ -22,6 +22,7 @@ juce_add_plugin(SupaTrigga target_sources(SupaTrigga PRIVATE Source/PluginProcessor.cpp + Source/SupaTriggaEditor.cpp ) smartelectronix_plugin_common(SupaTrigga) diff --git a/supatrigga/Source/PluginProcessor.cpp b/supatrigga/Source/PluginProcessor.cpp index 2100c42..0eb180b 100644 --- a/supatrigga/Source/PluginProcessor.cpp +++ b/supatrigga/Source/PluginProcessor.cpp @@ -1,131 +1,118 @@ #include "PluginProcessor.h" +#include "SupaTriggaEditor.h" #include SupaTriggaProcessor::SupaTriggaProcessor() - : AudioProcessor(BusesProperties() - .withInput("Input", juce::AudioChannelSet::stereo(), true) - .withOutput("Output", juce::AudioChannelSet::stereo(), true)), - apvts(*this, nullptr, "Parameters", createParameterLayout()) -{ - std::srand(static_cast(std::time(nullptr))); - - // Allocate buffers - leftBuffer = std::make_unique(MAXSIZE); - rightBuffer = std::make_unique(MAXSIZE); - - for (size_t i = 0; i < MAXSIZE; i++) - { - leftBuffer[i] = 0.0f; - rightBuffer[i] = 0.0f; - } - - // Initialize sequencer - for (int i = 0; i < MAXSLIDES; i++) - { - sequencer[i].offset = 0; - sequencer[i].reverse = false; - sequencer[i].stop = false; - sequencer[i].silence = false; - } - - // Get parameter pointers - granularityParam = apvts.getRawParameterValue(GRANULARITY_ID); - speedParam = apvts.getRawParameterValue(SPEED_ID); - probReverseParam = apvts.getRawParameterValue(PROB_REVERSE_ID); - probSpeedParam = apvts.getRawParameterValue(PROB_SPEED_ID); - probRearrangeParam = apvts.getRawParameterValue(PROB_REARRANGE_ID); - probSilenceParam = apvts.getRawParameterValue(PROB_SILENCE_ID); - probRepeatParam = apvts.getRawParameterValue(PROB_REPEAT_ID); - instantReverseParam = apvts.getRawParameterValue(INSTANT_REVERSE_ID); - instantSpeedParam = apvts.getRawParameterValue(INSTANT_SPEED_ID); - instantRepeatParam = apvts.getRawParameterValue(INSTANT_REPEAT_ID); - - randomize(); -} - -SupaTriggaProcessor::~SupaTriggaProcessor() -{ + : AudioProcessor( + BusesProperties() + .withInput("Input", juce::AudioChannelSet::stereo(), true) + .withOutput("Output", juce::AudioChannelSet::stereo(), true)), + apvts(*this, nullptr, "Parameters", createParameterLayout()) { + std::srand(static_cast(std::time(nullptr))); + + // Allocate buffers + leftBuffer = std::make_unique(MAXSIZE); + rightBuffer = std::make_unique(MAXSIZE); + + for (size_t i = 0; i < MAXSIZE; i++) { + leftBuffer[i] = 0.0f; + rightBuffer[i] = 0.0f; + } + + // Initialize sequencer + for (int i = 0; i < MAXSLIDES; i++) { + sequencer[i].offset = 0; + sequencer[i].reverse = false; + sequencer[i].stop = false; + sequencer[i].silence = false; + } + + // Get parameter pointers + granularityParam = apvts.getRawParameterValue(GRANULARITY_ID); + speedParam = apvts.getRawParameterValue(SPEED_ID); + probReverseParam = apvts.getRawParameterValue(PROB_REVERSE_ID); + probSpeedParam = apvts.getRawParameterValue(PROB_SPEED_ID); + probRearrangeParam = apvts.getRawParameterValue(PROB_REARRANGE_ID); + probSilenceParam = apvts.getRawParameterValue(PROB_SILENCE_ID); + probRepeatParam = apvts.getRawParameterValue(PROB_REPEAT_ID); + instantReverseParam = apvts.getRawParameterValue(INSTANT_REVERSE_ID); + instantSpeedParam = apvts.getRawParameterValue(INSTANT_SPEED_ID); + instantRepeatParam = apvts.getRawParameterValue(INSTANT_REPEAT_ID); + + randomize(); } -juce::AudioProcessorValueTreeState::ParameterLayout SupaTriggaProcessor::createParameterLayout() -{ - std::vector> params; - - // Granularity: displays as number of slices (1, 2, 4, 8, 16, 32, 64, 128) - params.push_back(std::make_unique( - juce::ParameterID(GRANULARITY_ID, 1), "Slices", - juce::NormalisableRange(0.0f, 1.0f, 0.001f), - 0.3f, - juce::String(), - juce::AudioProcessorParameter::genericParameter, - [](float value, int) { - int slices = 1 << static_cast(value * (BITSLIDES + 0.5f)); - return juce::String(slices) + " slices/measure"; - }, - nullptr)); - - // Speed: displays as speed multiplier - params.push_back(std::make_unique( - juce::ParameterID(SPEED_ID, 1), "Slow Speed", - juce::NormalisableRange(0.0f, 1.0f, 0.001f), - 0.25f, - juce::String(), - juce::AudioProcessorParameter::genericParameter, - [](float value, int) { - float speedVal = (1.0f - value + 0.01f) * 4.0f; - return juce::String(speedVal, 2) + "x"; - }, - nullptr)); - - // Probability parameters: display as percentage - auto probToString = [](float value, int) { - return juce::String(static_cast(value * 100.0f)) + "%"; - }; - - params.push_back(std::make_unique( - juce::ParameterID(PROB_REVERSE_ID, 1), "Reverse Prob", - juce::NormalisableRange(0.0f, 1.0f, 0.01f), - 0.15f, juce::String(), juce::AudioProcessorParameter::genericParameter, - probToString, nullptr)); - - params.push_back(std::make_unique( - juce::ParameterID(PROB_SPEED_ID, 1), "Slow Prob", - juce::NormalisableRange(0.0f, 1.0f, 0.01f), - 0.05f, juce::String(), juce::AudioProcessorParameter::genericParameter, - probToString, nullptr)); - - params.push_back(std::make_unique( - juce::ParameterID(PROB_REARRANGE_ID, 1), "Rearrange Prob", - juce::NormalisableRange(0.0f, 1.0f, 0.01f), - 0.95f, juce::String(), juce::AudioProcessorParameter::genericParameter, - probToString, nullptr)); - - params.push_back(std::make_unique( - juce::ParameterID(PROB_SILENCE_ID, 1), "Silence Prob", - juce::NormalisableRange(0.0f, 1.0f, 0.01f), - 0.0f, juce::String(), juce::AudioProcessorParameter::genericParameter, - probToString, nullptr)); - - params.push_back(std::make_unique( - juce::ParameterID(PROB_REPEAT_ID, 1), "Repeat Prob", - juce::NormalisableRange(0.0f, 1.0f, 0.01f), - 0.4f, juce::String(), juce::AudioProcessorParameter::genericParameter, - probToString, nullptr)); - - // Instant toggles - params.push_back(std::make_unique( - juce::ParameterID(INSTANT_REVERSE_ID, 1), "Instant Reverse", false)); - params.push_back(std::make_unique( - juce::ParameterID(INSTANT_SPEED_ID, 1), "Instant Slow", false)); - params.push_back(std::make_unique( - juce::ParameterID(INSTANT_REPEAT_ID, 1), "Instant Repeat", false)); - - return { params.begin(), params.end() }; +SupaTriggaProcessor::~SupaTriggaProcessor() {} + +juce::AudioProcessorValueTreeState::ParameterLayout +SupaTriggaProcessor::createParameterLayout() { + std::vector> params; + + // Granularity: displays as number of slices (1, 2, 4, 8, 16, 32, 64, 128) + params.push_back(std::make_unique( + juce::ParameterID(GRANULARITY_ID, 1), "Slices", + juce::NormalisableRange(0.0f, 1.0f, 0.001f), 0.3f, juce::String(), + juce::AudioProcessorParameter::genericParameter, + [](float value, int) { + int slices = 1 << static_cast(value * (BITSLIDES + 0.5f)); + return juce::String(slices); + }, + nullptr)); + + // Speed: displays as speed multiplier + params.push_back(std::make_unique( + juce::ParameterID(SPEED_ID, 1), "Slow Speed", + juce::NormalisableRange(0.0f, 1.0f, 0.001f), 0.25f, juce::String(), + juce::AudioProcessorParameter::genericParameter, + [](float value, int) { + float speedVal = (1.0f - value + 0.01f) * 4.0f; + return juce::String(speedVal, 2) + "x"; + }, + nullptr)); + + // Probability parameters: display as percentage + auto probToString = [](float value, int) { + return juce::String(static_cast(value * 100.0f)) + "%"; + }; + + params.push_back(std::make_unique( + juce::ParameterID(PROB_REVERSE_ID, 1), "Reverse Prob", + juce::NormalisableRange(0.0f, 1.0f, 0.01f), 0.15f, juce::String(), + juce::AudioProcessorParameter::genericParameter, probToString, nullptr)); + + params.push_back(std::make_unique( + juce::ParameterID(PROB_SPEED_ID, 1), "Slow Prob", + juce::NormalisableRange(0.0f, 1.0f, 0.01f), 0.05f, juce::String(), + juce::AudioProcessorParameter::genericParameter, probToString, nullptr)); + + params.push_back(std::make_unique( + juce::ParameterID(PROB_REARRANGE_ID, 1), "Rearrange Prob", + juce::NormalisableRange(0.0f, 1.0f, 0.01f), 0.95f, juce::String(), + juce::AudioProcessorParameter::genericParameter, probToString, nullptr)); + + params.push_back(std::make_unique( + juce::ParameterID(PROB_SILENCE_ID, 1), "Silence Prob", + juce::NormalisableRange(0.0f, 1.0f, 0.01f), 0.0f, juce::String(), + juce::AudioProcessorParameter::genericParameter, probToString, nullptr)); + + params.push_back(std::make_unique( + juce::ParameterID(PROB_REPEAT_ID, 1), "Repeat Prob", + juce::NormalisableRange(0.0f, 1.0f, 0.01f), 0.4f, juce::String(), + juce::AudioProcessorParameter::genericParameter, probToString, nullptr)); + + // Instant toggles + params.push_back(std::make_unique( + juce::ParameterID(INSTANT_REVERSE_ID, 1), "Instant Reverse", false)); + params.push_back(std::make_unique( + juce::ParameterID(INSTANT_SPEED_ID, 1), "Instant Slow", false)); + params.push_back(std::make_unique( + juce::ParameterID(INSTANT_REPEAT_ID, 1), "Instant Repeat", false)); + + return {params.begin(), params.end()}; } -const juce::String SupaTriggaProcessor::getName() const -{ - return JucePlugin_Name; +const juce::String SupaTriggaProcessor::getName() const { + return JucePlugin_Name; } bool SupaTriggaProcessor::acceptsMidi() const { return false; } @@ -136,400 +123,396 @@ double SupaTriggaProcessor::getTailLengthSeconds() const { return 0.0; } int SupaTriggaProcessor::getNumPrograms() { return 1; } int SupaTriggaProcessor::getCurrentProgram() { return 0; } void SupaTriggaProcessor::setCurrentProgram(int /*index*/) {} -const juce::String SupaTriggaProcessor::getProgramName(int /*index*/) { return {}; } -void SupaTriggaProcessor::changeProgramName(int /*index*/, const juce::String& /*newName*/) {} - -void SupaTriggaProcessor::prepareToPlay(double sampleRate, int /*samplesPerBlock*/) -{ - currentSampleRate = static_cast(sampleRate); - fadeCoeff = std::exp(std::log(0.01f) / FADETIME); - - // Reset state - positionInMeasure = 0; - previousSliceIndex = 0xffffffff; - granularityMask = 0; - granularity = 0; - gain = 0.0f; - speed = 0.0f; - first = true; - wasPlaying = false; - - // Clear buffers - for (size_t i = 0; i < MAXSIZE; i++) - { - leftBuffer[i] = 0.0f; - rightBuffer[i] = 0.0f; - } - - randomize(); -} - -void SupaTriggaProcessor::releaseResources() -{ +const juce::String SupaTriggaProcessor::getProgramName(int /*index*/) { + return {}; } - -bool SupaTriggaProcessor::isBusesLayoutSupported(const BusesLayout& layouts) const -{ - if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo()) - return false; - if (layouts.getMainInputChannelSet() != juce::AudioChannelSet::stereo()) - return false; - return true; +void SupaTriggaProcessor::changeProgramName(int /*index*/, + const juce::String & /*newName*/) {} + +void SupaTriggaProcessor::prepareToPlay(double sampleRate, + int /*samplesPerBlock*/) { + currentSampleRate = static_cast(sampleRate); + fadeCoeff = std::exp(std::log(0.01f) / FADETIME); + + // Reset state + positionInMeasure = 0; + previousSliceIndex = 0xffffffff; + granularityMask = 0; + granularity = 0; + gain = 0.0f; + speed = 0.0f; + first = true; + wasPlaying = false; + + // Clear buffers + for (size_t i = 0; i < MAXSIZE; i++) { + leftBuffer[i] = 0.0f; + rightBuffer[i] = 0.0f; + } + + randomize(); } -void SupaTriggaProcessor::processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer& /*midiMessages*/) -{ - juce::ScopedNoDenormals noDenormals; - - auto* in1 = buffer.getReadPointer(0); - auto* in2 = buffer.getReadPointer(1); - auto* out1 = buffer.getWritePointer(0); - auto* out2 = buffer.getWritePointer(1); - const int sampleFrames = buffer.getNumSamples(); +void SupaTriggaProcessor::releaseResources() {} - // Get transport info from host - auto* playHead = getPlayHead(); - if (playHead == nullptr) - { - // No playhead, pass through - for (int i = 0; i < sampleFrames; i++) - { - out1[i] = in1[i]; - out2[i] = in2[i]; - } - return; - } +bool SupaTriggaProcessor::isBusesLayoutSupported( + const BusesLayout &layouts) const { + if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo()) + return false; + if (layouts.getMainInputChannelSet() != juce::AudioChannelSet::stereo()) + return false; + return true; +} - auto posInfo = playHead->getPosition(); - if (!posInfo.hasValue()) - { - // No position info, pass through - for (int i = 0; i < sampleFrames; i++) - { - out1[i] = in1[i]; - out2[i] = in2[i]; - } - return; +void SupaTriggaProcessor::processBlock(juce::AudioBuffer &buffer, + juce::MidiBuffer & /*midiMessages*/) { + juce::ScopedNoDenormals noDenormals; + + auto *in1 = buffer.getReadPointer(0); + auto *in2 = buffer.getReadPointer(1); + auto *out1 = buffer.getWritePointer(0); + auto *out2 = buffer.getWritePointer(1); + const int sampleFrames = buffer.getNumSamples(); + + // Get transport info from host + auto *playHead = getPlayHead(); + if (playHead == nullptr) { + // No playhead, pass through + for (int i = 0; i < sampleFrames; i++) { + out1[i] = in1[i]; + out2[i] = in2[i]; } - - // Check if we have all required info - bool isPlaying = posInfo->getIsPlaying(); - auto bpmOpt = posInfo->getBpm(); - auto timeSigOpt = posInfo->getTimeSignature(); - auto ppqPosOpt = posInfo->getPpqPosition(); - auto barPosOpt = posInfo->getPpqPositionOfLastBarStart(); - - if (!isPlaying || !bpmOpt.hasValue() || !timeSigOpt.hasValue() || !ppqPosOpt.hasValue()) - { - positionInMeasure = 0xffffffff; - for (int i = 0; i < sampleFrames; i++) - { - out1[i] = in1[i]; - out2[i] = in2[i]; - } - wasPlaying = isPlaying; - return; + return; + } + + auto posInfo = playHead->getPosition(); + if (!posInfo.hasValue()) { + // No position info, pass through + for (int i = 0; i < sampleFrames; i++) { + out1[i] = in1[i]; + out2[i] = in2[i]; } - - double tempo = *bpmOpt; - if (tempo < 20.0) - { - // Ridiculous tempo - for (int i = 0; i < sampleFrames; i++) - { - out1[i] = in1[i]; - out2[i] = in2[i]; - } - return; + return; + } + + // Check if we have all required info + bool isPlaying = posInfo->getIsPlaying(); + auto bpmOpt = posInfo->getBpm(); + auto timeSigOpt = posInfo->getTimeSignature(); + auto ppqPosOpt = posInfo->getPpqPosition(); + auto barPosOpt = posInfo->getPpqPositionOfLastBarStart(); + + if (!isPlaying || !bpmOpt.hasValue() || !timeSigOpt.hasValue() || + !ppqPosOpt.hasValue()) { + positionInMeasure = 0xffffffff; + for (int i = 0; i < sampleFrames; i++) { + out1[i] = in1[i]; + out2[i] = in2[i]; } - - auto timeSig = *timeSigOpt; - double ppqPos = *ppqPosOpt; - double barStartPos = barPosOpt.hasValue() ? *barPosOpt : ppqPos; - - // Calculate samples in measure - double tempoBPS = tempo / 60.0; - double numSamplesInBeat = currentSampleRate / tempoBPS; - double samplesInMeasureDouble = std::ceil(numSamplesInBeat * timeSig.numerator / (timeSig.denominator / 4.0)); - unsigned long samplesInMeasure = static_cast(samplesInMeasureDouble); - - // Detect transport changes - bool playbackChanged = (isPlaying != wasPlaying); wasPlaying = isPlaying; - - // Calculate distance to next bar - double beatsPerBar = static_cast(timeSig.numerator); - double distanceToNextBarPPQ; - if (std::fabs(barStartPos - ppqPos) < 1e-10) - distanceToNextBarPPQ = 0.0; - else - distanceToNextBarPPQ = barStartPos + beatsPerBar - ppqPos; - - while (distanceToNextBarPPQ < 0.0) - distanceToNextBarPPQ += beatsPerBar; - while (distanceToNextBarPPQ >= beatsPerBar) - distanceToNextBarPPQ -= beatsPerBar; - - double numSamplesToNextBar = (distanceToNextBarPPQ * currentSampleRate * 60.0) / tempo; - if (numSamplesToNextBar < 0.0) - numSamplesToNextBar = 0.0; - - unsigned long samplesToNextBar = static_cast(std::ceil(numSamplesToNextBar)); - - // If playback changed, recalculate position - if (playbackChanged) - { - positionInMeasure = static_cast(std::floor(samplesInMeasureDouble - numSamplesToNextBar)); - if (positionInMeasure >= samplesInMeasure) - positionInMeasure = samplesInMeasure - 1; + return; + } + + double tempo = *bpmOpt; + if (tempo < 20.0) { + // Ridiculous tempo + for (int i = 0; i < sampleFrames; i++) { + out1[i] = in1[i]; + out2[i] = in2[i]; + } + return; + } + + auto timeSig = *timeSigOpt; + double ppqPos = *ppqPosOpt; + double barStartPos = barPosOpt.hasValue() ? *barPosOpt : ppqPos; + + // Calculate samples in measure + double tempoBPS = tempo / 60.0; + double numSamplesInBeat = currentSampleRate / tempoBPS; + double samplesInMeasureDouble = std::ceil( + numSamplesInBeat * timeSig.numerator / (timeSig.denominator / 4.0)); + unsigned long samplesInMeasure = + static_cast(samplesInMeasureDouble); + + // Detect transport changes + bool playbackChanged = (isPlaying != wasPlaying); + wasPlaying = isPlaying; + + // Calculate distance to next bar + double beatsPerBar = static_cast(timeSig.numerator); + double distanceToNextBarPPQ; + if (std::fabs(barStartPos - ppqPos) < 1e-10) + distanceToNextBarPPQ = 0.0; + else + distanceToNextBarPPQ = barStartPos + beatsPerBar - ppqPos; + + while (distanceToNextBarPPQ < 0.0) + distanceToNextBarPPQ += beatsPerBar; + while (distanceToNextBarPPQ >= beatsPerBar) + distanceToNextBarPPQ -= beatsPerBar; + + double numSamplesToNextBar = + (distanceToNextBarPPQ * currentSampleRate * 60.0) / tempo; + if (numSamplesToNextBar < 0.0) + numSamplesToNextBar = 0.0; + + unsigned long samplesToNextBar = + static_cast(std::ceil(numSamplesToNextBar)); + + // If playback changed, recalculate position + if (playbackChanged) { + positionInMeasure = static_cast( + std::floor(samplesInMeasureDouble - numSamplesToNextBar)); + if (positionInMeasure >= samplesInMeasure) + positionInMeasure = samplesInMeasure - 1; + } + + // Get parameter values + float granularityRaw = granularityParam->load(); + float speedRaw = speedParam->load(); + + unsigned long granularityTmp = + static_cast(granularityRaw * (BITSLIDES + 0.5f)); + unsigned long granularityMaskTmp = 0xffffffff << (BITSLIDES - granularityTmp); + + float speedDiff = std::exp(std::log(0.01f) / (((1.0f - speedRaw) + 0.01f) * + currentSampleRate * 4.0f)); + + for (int i = 0; i < sampleFrames; i++) { + // Reset position at bar border + if (static_cast(i) == samplesToNextBar) + positionInMeasure = 0; + + // Store input in buffer + if (positionInMeasure < MAXSIZE) { + leftBuffer[positionInMeasure] = in1[i]; + rightBuffer[positionInMeasure] = in2[i]; } - // Get parameter values - float granularityRaw = granularityParam->load(); - float speedRaw = speedParam->load(); - - unsigned long granularityTmp = static_cast(granularityRaw * (BITSLIDES + 0.5f)); - unsigned long granularityMaskTmp = 0xffffffff << (BITSLIDES - granularityTmp); + unsigned long sliceIndex = + ((positionInMeasure * MAXSLIDES) / samplesInMeasure) & granularityMask; + + if (instantRepeat && sliceIndex != 0) + sequencer[sliceIndex].offset = + (sequencer[(sliceIndex - 1) & granularityMaskTmp].offset + + (sliceIndex - ((sliceIndex - 1) & granularityMaskTmp))) & + granularityMask; + + unsigned long displacement = sequencer[sliceIndex].offset & granularityMask; + + if (granularityMaskTmp != granularityMask) { + unsigned long sliceIndexTmp = + ((positionInMeasure * MAXSLIDES) / samplesInMeasure) & + granularityMaskTmp; + + if ((granularityTmp < granularity && sliceIndex == 0) || + (granularityTmp > granularity && sliceIndexTmp == 0) || + (sliceIndex == 0 && sliceIndexTmp == 0)) { + granularityMask = granularityMaskTmp; + granularity = granularityTmp; + sliceIndex = sliceIndexTmp; + displacement = sequencer[sliceIndex].offset & granularityMask; + } + } - float speedDiff = std::exp(std::log(0.01f) / (((1.0f - speedRaw) + 0.01f) * currentSampleRate * 4.0f)); + if (sliceIndex != previousSliceIndex) { + instantRepeat = instantRepeatParam->load() > 0.5f; + instantReverse = instantReverseParam->load() > 0.5f; + instantSlow = instantSpeedParam->load() > 0.5f; + previousSliceIndex = sliceIndex; + } - for (int i = 0; i < sampleFrames; i++) + // Gain calculation { - // Reset position at bar border - if (static_cast(i) == samplesToNextBar) - positionInMeasure = 0; - - // Store input in buffer - if (positionInMeasure < MAXSIZE) - { - leftBuffer[positionInMeasure] = in1[i]; - rightBuffer[positionInMeasure] = in2[i]; - } + unsigned long sliceIndexFar = + ((((positionInMeasure + FADETIME) % samplesInMeasure) * MAXSLIDES) / + samplesInMeasure) & + granularityMask; + unsigned long displacementFar = + sequencer[sliceIndexFar].offset & granularityMask; + bool reverseFar = sequencer[sliceIndexFar].reverse; - unsigned long sliceIndex = ((positionInMeasure * MAXSLIDES) / samplesInMeasure) & granularityMask; + float targetGain = 1.0f; - if (instantRepeat && sliceIndex != 0) - sequencer[sliceIndex].offset = (sequencer[(sliceIndex - 1) & granularityMaskTmp].offset + - (sliceIndex - ((sliceIndex - 1) & granularityMaskTmp))) & granularityMask; + if (sequencer[sliceIndex].silence) + targetGain = 0.0f; - unsigned long displacement = sequencer[sliceIndex].offset & granularityMask; + if (displacementFar != displacement || + positionInMeasure + FADETIME > samplesInMeasure || + reverseFar != sequencer[sliceIndex].reverse) + targetGain = 0.0f; - if (granularityMaskTmp != granularityMask) - { - unsigned long sliceIndexTmp = ((positionInMeasure * MAXSLIDES) / samplesInMeasure) & granularityMaskTmp; + gain = fadeCoeff * gain + (1.0f - fadeCoeff) * targetGain; + } - if ((granularityTmp < granularity && sliceIndex == 0) || - (granularityTmp > granularity && sliceIndexTmp == 0) || - (sliceIndex == 0 && sliceIndexTmp == 0)) - { - granularityMask = granularityMaskTmp; - granularity = granularityTmp; - sliceIndex = sliceIndexTmp; - displacement = sequencer[sliceIndex].offset & granularityMask; - } + if ((sequencer[sliceIndex].reverse || instantReverse) && + displacement != 0) { + if (!(sequencer[sliceIndex].stop || instantSlow)) { + unsigned long sliceSize = samplesInMeasure >> granularity; + unsigned long sliceStart = (positionInMeasure / sliceSize) * sliceSize; + unsigned long sliceDiff = positionInMeasure - sliceStart; + unsigned long sliceEnd = (sliceStart + sliceSize) - sliceDiff; + unsigned long difference = + (displacement * samplesInMeasure) / MAXSLIDES; + unsigned long bufferIndex = + difference <= sliceEnd ? sliceEnd - difference : 0; + + if (bufferIndex < MAXSIZE) { + out1[i] = leftBuffer[bufferIndex] * gain; + out2[i] = rightBuffer[bufferIndex] * gain; } - - if (sliceIndex != previousSliceIndex) - { - instantRepeat = instantRepeatParam->load() > 0.5f; - instantReverse = instantReverseParam->load() > 0.5f; - instantSlow = instantSpeedParam->load() > 0.5f; - previousSliceIndex = sliceIndex; + first = true; + } else { + if (first) { + unsigned long sliceSize = samplesInMeasure >> granularity; + unsigned long sliceStart = + (positionInMeasure / sliceSize) * sliceSize; + unsigned long sliceDiff = positionInMeasure - sliceStart; + unsigned long sliceEnd = (sliceStart + sliceSize) - sliceDiff; + unsigned long difference = + (displacement * samplesInMeasure) / MAXSLIDES; + + position = static_cast( + difference <= sliceEnd ? sliceEnd - difference : 0); + speed = 1.0f / speedDiff; + first = false; } - // Gain calculation - { - unsigned long sliceIndexFar = ((((positionInMeasure + FADETIME) % samplesInMeasure) * MAXSLIDES) / samplesInMeasure) & granularityMask; - unsigned long displacementFar = sequencer[sliceIndexFar].offset & granularityMask; - bool reverseFar = sequencer[sliceIndexFar].reverse; + speed *= speedDiff; + position -= speed; - float targetGain = 1.0f; + if (position < 0.0f) + position = 0.0f; - if (sequencer[sliceIndex].silence) - targetGain = 0.0f; + unsigned long bufferIndex = + static_cast(std::floor(position)); + float alpha = position - bufferIndex; - if (displacementFar != displacement || - positionInMeasure + FADETIME > samplesInMeasure || - reverseFar != sequencer[sliceIndex].reverse) - targetGain = 0.0f; - - gain = fadeCoeff * gain + (1.0f - fadeCoeff) * targetGain; + if (bufferIndex < MAXSIZE - 3) { + out1[i] = hermiteInverse(leftBuffer.get(), bufferIndex, alpha) * gain; + out2[i] = + hermiteInverse(rightBuffer.get(), bufferIndex, alpha) * gain; } - - if ((sequencer[sliceIndex].reverse || instantReverse) && displacement != 0) - { - if (!(sequencer[sliceIndex].stop || instantSlow)) - { - unsigned long sliceSize = samplesInMeasure >> granularity; - unsigned long sliceStart = (positionInMeasure / sliceSize) * sliceSize; - unsigned long sliceDiff = positionInMeasure - sliceStart; - unsigned long sliceEnd = (sliceStart + sliceSize) - sliceDiff; - unsigned long difference = (displacement * samplesInMeasure) / MAXSLIDES; - unsigned long bufferIndex = difference <= sliceEnd ? sliceEnd - difference : 0; - - if (bufferIndex < MAXSIZE) - { - out1[i] = leftBuffer[bufferIndex] * gain; - out2[i] = rightBuffer[bufferIndex] * gain; - } - first = true; - } - else - { - if (first) - { - unsigned long sliceSize = samplesInMeasure >> granularity; - unsigned long sliceStart = (positionInMeasure / sliceSize) * sliceSize; - unsigned long sliceDiff = positionInMeasure - sliceStart; - unsigned long sliceEnd = (sliceStart + sliceSize) - sliceDiff; - unsigned long difference = (displacement * samplesInMeasure) / MAXSLIDES; - - position = static_cast(difference <= sliceEnd ? sliceEnd - difference : 0); - speed = 1.0f / speedDiff; - first = false; - } - - speed *= speedDiff; - position -= speed; - - if (position < 0.0f) - position = 0.0f; - - unsigned long bufferIndex = static_cast(std::floor(position)); - float alpha = position - bufferIndex; - - if (bufferIndex < MAXSIZE - 3) - { - out1[i] = hermiteInverse(leftBuffer.get(), bufferIndex, alpha) * gain; - out2[i] = hermiteInverse(rightBuffer.get(), bufferIndex, alpha) * gain; - } - } + } + } else { + if (!(sequencer[sliceIndex].stop || instantSlow) || displacement == 0) { + unsigned long difference = + (displacement * samplesInMeasure) / MAXSLIDES; + unsigned long bufferIndex = + positionInMeasure > difference ? positionInMeasure - difference : 0; + + if (bufferIndex < MAXSIZE) { + out1[i] = leftBuffer[bufferIndex] * gain; + out2[i] = rightBuffer[bufferIndex] * gain; } - else - { - if (!(sequencer[sliceIndex].stop || instantSlow) || displacement == 0) - { - unsigned long difference = (displacement * samplesInMeasure) / MAXSLIDES; - unsigned long bufferIndex = positionInMeasure > difference ? positionInMeasure - difference : 0; - - if (bufferIndex < MAXSIZE) - { - out1[i] = leftBuffer[bufferIndex] * gain; - out2[i] = rightBuffer[bufferIndex] * gain; - } - first = true; - } - else - { - if (first) - { - position = static_cast(positionInMeasure - (displacement * samplesInMeasure) / MAXSLIDES); - speed = 1.0f / speedDiff; - first = false; - } - - speed *= speedDiff; - position += speed; - - unsigned long bufferIndex = static_cast(std::floor(position)); - float alpha = position - bufferIndex; - - if (bufferIndex < MAXSIZE - 3) - { - out1[i] = hermiteInverse(leftBuffer.get(), bufferIndex, alpha) * gain; - out2[i] = hermiteInverse(rightBuffer.get(), bufferIndex, alpha) * gain; - } - } + first = true; + } else { + if (first) { + position = + static_cast(positionInMeasure - + (displacement * samplesInMeasure) / MAXSLIDES); + speed = 1.0f / speedDiff; + first = false; } - positionInMeasure++; + speed *= speedDiff; + position += speed; - if (positionInMeasure >= samplesInMeasure) - { - randomize(); - positionInMeasure = 0; - } + unsigned long bufferIndex = + static_cast(std::floor(position)); + float alpha = position - bufferIndex; - if (std::fabs(gain) < 1e-10f) - gain = 0.0f; + if (bufferIndex < MAXSIZE - 3) { + out1[i] = hermiteInverse(leftBuffer.get(), bufferIndex, alpha) * gain; + out2[i] = + hermiteInverse(rightBuffer.get(), bufferIndex, alpha) * gain; + } + } } -} -void SupaTriggaProcessor::randomize() -{ - float granularityRaw = granularityParam->load(); - float probRearrangeRaw = probRearrangeParam->load(); - float probRepeatRaw = probRepeatParam->load(); - float probReverseRaw = probReverseParam->load(); - float probSpeedRaw = probSpeedParam->load(); - float probSilenceRaw = probSilenceParam->load(); + positionInMeasure++; - unsigned long granularityTmp = static_cast(granularityRaw * (BITSLIDES + 0.5f)); - unsigned long granularityMaskTmp = 0xffffffff << (BITSLIDES - granularityTmp); + if (positionInMeasure >= samplesInMeasure) { + randomize(); + positionInMeasure = 0; + } - for (unsigned long i = 0; i < MAXSLIDES; i++) - { - if ((std::rand() % 100) < static_cast(probRearrangeRaw * 101.0f)) - { - if ((std::rand() % 100) < static_cast(probRepeatRaw * 101.0f)) - { - if (i != 0) - sequencer[i].offset = (sequencer[(i - 1) & granularityMaskTmp].offset + - (i - ((i - 1) & granularityMaskTmp))) & granularityMask; - else - sequencer[i].offset = 0; - } - else - { - sequencer[i].offset = (i * static_cast(std::rand() % MAXSLIDES)) / MAXSLIDES; - } - } - else - { - sequencer[i].offset = 0; - } + if (std::fabs(gain) < 1e-10f) + gain = 0.0f; + } +} - if ((std::rand() % 100) < static_cast(probReverseRaw * 100.0f) && sequencer[i].offset > 0) - sequencer[i].reverse = true; +void SupaTriggaProcessor::randomize() { + float granularityRaw = granularityParam->load(); + float probRearrangeRaw = probRearrangeParam->load(); + float probRepeatRaw = probRepeatParam->load(); + float probReverseRaw = probReverseParam->load(); + float probSpeedRaw = probSpeedParam->load(); + float probSilenceRaw = probSilenceParam->load(); + + unsigned long granularityTmp = + static_cast(granularityRaw * (BITSLIDES + 0.5f)); + unsigned long granularityMaskTmp = 0xffffffff << (BITSLIDES - granularityTmp); + + for (unsigned long i = 0; i < MAXSLIDES; i++) { + if ((std::rand() % 100) < static_cast(probRearrangeRaw * 101.0f)) { + if ((std::rand() % 100) < static_cast(probRepeatRaw * 101.0f)) { + if (i != 0) + sequencer[i].offset = + (sequencer[(i - 1) & granularityMaskTmp].offset + + (i - ((i - 1) & granularityMaskTmp))) & + granularityMask; else - sequencer[i].reverse = false; + sequencer[i].offset = 0; + } else { + sequencer[i].offset = + (i * static_cast(std::rand() % MAXSLIDES)) / + MAXSLIDES; + } + } else { + sequencer[i].offset = 0; + } - if ((std::rand() % 100) < static_cast(probSpeedRaw * 101.0f)) - sequencer[i].stop = true; - else - sequencer[i].stop = false; + if ((std::rand() % 100) < static_cast(probReverseRaw * 100.0f) && + sequencer[i].offset > 0) + sequencer[i].reverse = true; + else + sequencer[i].reverse = false; - if ((std::rand() % 100) < static_cast(probSilenceRaw * 101.0f)) - sequencer[i].silence = true; - else - sequencer[i].silence = false; - } + if ((std::rand() % 100) < static_cast(probSpeedRaw * 101.0f)) + sequencer[i].stop = true; + else + sequencer[i].stop = false; + + if ((std::rand() % 100) < static_cast(probSilenceRaw * 101.0f)) + sequencer[i].silence = true; + else + sequencer[i].silence = false; + } } -bool SupaTriggaProcessor::hasEditor() const { return false; } +bool SupaTriggaProcessor::hasEditor() const { return true; } -juce::AudioProcessorEditor* SupaTriggaProcessor::createEditor() -{ - return new juce::GenericAudioProcessorEditor(*this); +juce::AudioProcessorEditor *SupaTriggaProcessor::createEditor() { + return new SupaTriggaEditor(*this, apvts); } -void SupaTriggaProcessor::getStateInformation(juce::MemoryBlock& destData) -{ - auto state = apvts.copyState(); - std::unique_ptr xml(state.createXml()); - copyXmlToBinary(*xml, destData); +void SupaTriggaProcessor::getStateInformation(juce::MemoryBlock &destData) { + auto state = apvts.copyState(); + std::unique_ptr xml(state.createXml()); + copyXmlToBinary(*xml, destData); } -void SupaTriggaProcessor::setStateInformation(const void* data, int sizeInBytes) -{ - std::unique_ptr xmlState(getXmlFromBinary(data, sizeInBytes)); - if (xmlState != nullptr && xmlState->hasTagName(apvts.state.getType())) - { - apvts.replaceState(juce::ValueTree::fromXml(*xmlState)); - } +void SupaTriggaProcessor::setStateInformation(const void *data, + int sizeInBytes) { + std::unique_ptr xmlState( + getXmlFromBinary(data, sizeInBytes)); + if (xmlState != nullptr && xmlState->hasTagName(apvts.state.getType())) { + apvts.replaceState(juce::ValueTree::fromXml(*xmlState)); + } } -juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() -{ - return new SupaTriggaProcessor(); +juce::AudioProcessor *JUCE_CALLTYPE createPluginFilter() { + return new SupaTriggaProcessor(); } diff --git a/supatrigga/Source/PluginProcessor.h b/supatrigga/Source/PluginProcessor.h index 769d9f7..1fa9a5a 100644 --- a/supatrigga/Source/PluginProcessor.h +++ b/supatrigga/Source/PluginProcessor.h @@ -1,8 +1,10 @@ #pragma once -#include #include #include +#include + +class SupaTriggaEditor; // Forward declaration // Constants from original constexpr int NUMBERIO = 2; @@ -12,121 +14,119 @@ constexpr int MAXSLIDES = 1 << BITSLIDES; constexpr int FADETIME = 150; // Glitch parameters for each slice -struct GlitchParams -{ - unsigned long offset = 0; - bool reverse = false; - bool stop = false; - bool silence = false; +struct GlitchParams { + unsigned long offset = 0; + bool reverse = false; + bool stop = false; + bool silence = false; }; // Hermite interpolation (inverse direction) -inline float hermiteInverse(float* wavetable, unsigned long nearest_sample, float x) -{ - float y3 = (nearest_sample == 0) ? 0.f : wavetable[nearest_sample - 1]; - float y2 = wavetable[nearest_sample]; - float y1 = wavetable[nearest_sample + 1]; - float y0 = wavetable[nearest_sample + 2]; +inline float hermiteInverse(float *wavetable, unsigned long nearest_sample, + float x) { + float y3 = (nearest_sample == 0) ? 0.f : wavetable[nearest_sample - 1]; + float y2 = wavetable[nearest_sample]; + float y1 = wavetable[nearest_sample + 1]; + float y0 = wavetable[nearest_sample + 2]; - x = 1.f - x; + x = 1.f - x; - float c0 = y1; - float c1 = 0.5f * (y2 - y0); - float c2 = y0 - 2.5f * y1 + 2.f * y2 - 0.5f * y3; - float c3 = 1.5f * (y1 - y2) + 0.5f * (y3 - y0); + float c0 = y1; + float c1 = 0.5f * (y2 - y0); + float c2 = y0 - 2.5f * y1 + 2.f * y2 - 0.5f * y3; + float c3 = 1.5f * (y1 - y2) + 0.5f * (y3 - y0); - return ((c3 * x + c2) * x + c1) * x + c0; + return ((c3 * x + c2) * x + c1) * x + c0; } -class SupaTriggaProcessor : public juce::AudioProcessor -{ +class SupaTriggaProcessor : public juce::AudioProcessor { public: - SupaTriggaProcessor(); - ~SupaTriggaProcessor() override; + SupaTriggaProcessor(); + ~SupaTriggaProcessor() override; - void prepareToPlay(double sampleRate, int samplesPerBlock) override; - void releaseResources() override; + void prepareToPlay(double sampleRate, int samplesPerBlock) override; + void releaseResources() override; - bool isBusesLayoutSupported(const BusesLayout& layouts) const override; + bool isBusesLayoutSupported(const BusesLayout &layouts) const override; - void processBlock(juce::AudioBuffer&, juce::MidiBuffer&) override; + void processBlock(juce::AudioBuffer &, juce::MidiBuffer &) override; - juce::AudioProcessorEditor* createEditor() override; - bool hasEditor() const override; + juce::AudioProcessorEditor *createEditor() override; + bool hasEditor() const override; - const juce::String getName() const override; + const juce::String getName() const override; - bool acceptsMidi() const override; - bool producesMidi() const override; - bool isMidiEffect() const override; - double getTailLengthSeconds() const override; + bool acceptsMidi() const override; + bool producesMidi() const override; + bool isMidiEffect() const override; + double getTailLengthSeconds() const override; - int getNumPrograms() override; - int getCurrentProgram() override; - void setCurrentProgram(int index) override; - const juce::String getProgramName(int index) override; - void changeProgramName(int index, const juce::String& newName) override; + int getNumPrograms() override; + int getCurrentProgram() override; + void setCurrentProgram(int index) override; + const juce::String getProgramName(int index) override; + void changeProgramName(int index, const juce::String &newName) override; - void getStateInformation(juce::MemoryBlock& destData) override; - void setStateInformation(const void* data, int sizeInBytes) override; + void getStateInformation(juce::MemoryBlock &destData) override; + void setStateInformation(const void *data, int sizeInBytes) override; - // Parameter IDs - static constexpr const char* GRANULARITY_ID = "granularity"; - static constexpr const char* SPEED_ID = "speed"; - static constexpr const char* PROB_REVERSE_ID = "probReverse"; - static constexpr const char* PROB_SPEED_ID = "probSpeed"; - static constexpr const char* PROB_REARRANGE_ID = "probRearrange"; - static constexpr const char* PROB_SILENCE_ID = "probSilence"; - static constexpr const char* PROB_REPEAT_ID = "probRepeat"; - static constexpr const char* INSTANT_REVERSE_ID = "instantReverse"; - static constexpr const char* INSTANT_SPEED_ID = "instantSpeed"; - static constexpr const char* INSTANT_REPEAT_ID = "instantRepeat"; + // Parameter IDs + static constexpr const char *GRANULARITY_ID = "granularity"; + static constexpr const char *SPEED_ID = "speed"; + static constexpr const char *PROB_REVERSE_ID = "probReverse"; + static constexpr const char *PROB_SPEED_ID = "probSpeed"; + static constexpr const char *PROB_REARRANGE_ID = "probRearrange"; + static constexpr const char *PROB_SILENCE_ID = "probSilence"; + static constexpr const char *PROB_REPEAT_ID = "probRepeat"; + static constexpr const char *INSTANT_REVERSE_ID = "instantReverse"; + static constexpr const char *INSTANT_SPEED_ID = "instantSpeed"; + static constexpr const char *INSTANT_REPEAT_ID = "instantRepeat"; private: - juce::AudioProcessorValueTreeState apvts; - juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout(); - - void randomize(); - - // Buffers - std::unique_ptr leftBuffer; - std::unique_ptr rightBuffer; - - // Sequencer - GlitchParams sequencer[MAXSLIDES]; - - // State - unsigned long positionInMeasure = 0; - unsigned long previousSliceIndex = 0xffffffff; - unsigned long granularityMask = 0; - unsigned long granularity = 0; - - float gain = 0.0f; - float speed = 0.0f; - float position = 0.0f; - bool first = true; - - bool instantReverse = false; - bool instantSlow = false; - bool instantRepeat = false; - - float currentSampleRate = 44100.0f; - float fadeCoeff = 0.0f; - - // Last playback state for detecting transport changes - bool wasPlaying = false; - - // Parameter pointers - std::atomic* granularityParam = nullptr; - std::atomic* speedParam = nullptr; - std::atomic* probReverseParam = nullptr; - std::atomic* probSpeedParam = nullptr; - std::atomic* probRearrangeParam = nullptr; - std::atomic* probSilenceParam = nullptr; - std::atomic* probRepeatParam = nullptr; - std::atomic* instantReverseParam = nullptr; - std::atomic* instantSpeedParam = nullptr; - std::atomic* instantRepeatParam = nullptr; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SupaTriggaProcessor) + juce::AudioProcessorValueTreeState apvts; + juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout(); + + void randomize(); + + // Buffers + std::unique_ptr leftBuffer; + std::unique_ptr rightBuffer; + + // Sequencer + GlitchParams sequencer[MAXSLIDES]; + + // State + unsigned long positionInMeasure = 0; + unsigned long previousSliceIndex = 0xffffffff; + unsigned long granularityMask = 0; + unsigned long granularity = 0; + + float gain = 0.0f; + float speed = 0.0f; + float position = 0.0f; + bool first = true; + + bool instantReverse = false; + bool instantSlow = false; + bool instantRepeat = false; + + float currentSampleRate = 44100.0f; + float fadeCoeff = 0.0f; + + // Last playback state for detecting transport changes + bool wasPlaying = false; + + // Parameter pointers + std::atomic *granularityParam = nullptr; + std::atomic *speedParam = nullptr; + std::atomic *probReverseParam = nullptr; + std::atomic *probSpeedParam = nullptr; + std::atomic *probRearrangeParam = nullptr; + std::atomic *probSilenceParam = nullptr; + std::atomic *probRepeatParam = nullptr; + std::atomic *instantReverseParam = nullptr; + std::atomic *instantSpeedParam = nullptr; + std::atomic *instantRepeatParam = nullptr; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SupaTriggaProcessor) }; diff --git a/supatrigga/Source/SupaTriggaEditor.cpp b/supatrigga/Source/SupaTriggaEditor.cpp new file mode 100644 index 0000000..2eb22bf --- /dev/null +++ b/supatrigga/Source/SupaTriggaEditor.cpp @@ -0,0 +1,326 @@ +#include "SupaTriggaEditor.h" +#include "Theme.h" + +SupaTriggaLookAndFeel::SupaTriggaLookAndFeel() { + + // Configure default colors + setColour(juce::Slider::rotarySliderFillColourId, Theme::Colors::globalAccent); + setColour(juce::Slider::rotarySliderOutlineColourId, Theme::Colors::track); + setColour(juce::Slider::textBoxTextColourId, Theme::Colors::textValue); + +#if JUCE_MAC + interFont = juce::Typeface::createSystemTypefaceFor(juce::FontOptions{}.withName("Inter").withHeight(12.0f).withStyle("Bold")); +#endif +} + +void SupaTriggaLookAndFeel::drawRotarySlider(juce::Graphics &g, int x, int y, + int width, int height, + float sliderPos, + const float rotaryStartAngle, + const float rotaryEndAngle, + juce::Slider &slider) { + const float radius = 34.0f; + const float centreX = (float)x + (float)width * 0.5f; + const float centreY = (float)y + (float)height * 0.5f; + + // Track + const float trackThickness = 6.0f; + g.setColour(slider.findColour(juce::Slider::rotarySliderOutlineColourId)); + juce::Path trackPath; + trackPath.addCentredArc(centreX, centreY, radius, radius, 0.0f, rotaryStartAngle, rotaryEndAngle, true); + g.strokePath(trackPath, juce::PathStrokeType(trackThickness, juce::PathStrokeType::curved, juce::PathStrokeType::rounded)); + + // Fill + auto fillColour = slider.findColour(juce::Slider::rotarySliderFillColourId); + g.setColour(fillColour); + + // Only draw fill if greater than 0 + if (sliderPos > 0.0f) { + const float angle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle); + juce::Path fillPath; + fillPath.addCentredArc(centreX, centreY, radius, radius, 0.0f, rotaryStartAngle, angle, true); + g.strokePath(fillPath, juce::PathStrokeType(trackThickness, juce::PathStrokeType::curved, juce::PathStrokeType::rounded)); + } + + // Center Inner Circle + const float innerRadius = Theme::Metrics::innerRadius; + const float innerDiameter = innerRadius * 2.0f; + g.setColour(Theme::Colors::background); + g.fillEllipse(centreX - innerRadius, centreY - innerRadius, innerDiameter, innerDiameter); + g.setColour(Theme::Colors::innerCircle); + g.drawEllipse(centreX - innerRadius, centreY - innerRadius, innerDiameter, innerDiameter, 1.0f); + + // Text Value + g.setColour(slider.findColour(juce::Slider::textBoxTextColourId)); + + auto font = juce::Font(juce::FontOptions{}.withHeight(12.0f).withStyle("Bold")); + if (interFont != nullptr) { + font = juce::Font(juce::FontOptions(interFont).withHeight(12.0f)); + } + g.setFont(font); + + juce::String text = slider.getTextFromValue(slider.getValue()); + g.drawText(text, x, y, width, height, juce::Justification::centred, false); +} + +void SupaTriggaLookAndFeel::drawToggleButton( + juce::Graphics &g, juce::ToggleButton &button, + bool /*shouldDrawButtonAsHighlighted*/, + bool /*shouldDrawButtonAsDown*/) { + const float radius = 34.0f; + const float centreX = button.getLocalBounds().getCentreX(); + const float centreY = button.getLocalBounds().getCentreY(); + + // Toggle Track + g.setColour(Theme::Colors::track); + g.drawEllipse(centreX - radius, centreY - radius, radius * 2.0f, radius * 2.0f, 6.0f); + + // Toggle Fill (ON/OFF) + if (button.getToggleState()) { + g.setColour(button.findColour(juce::ToggleButton::tickColourId)); + g.drawEllipse(centreX - radius, centreY - radius, radius * 2.0f, radius * 2.0f, 6.0f); + } + + // Center Inner Circle + const float innerRadius = Theme::Metrics::innerRadius; + const float innerDiameter = innerRadius * 2.0f; + g.setColour(Theme::Colors::background); + g.fillEllipse(centreX - innerRadius, centreY - innerRadius, innerDiameter, innerDiameter); + g.setColour(Theme::Colors::innerCircle); + g.drawEllipse(centreX - innerRadius, centreY - innerRadius, innerDiameter, innerDiameter, 1.0f); + + // Text Label + g.setColour(button.getToggleState() + ? Theme::Colors::textValue + : Theme::Colors::textSecondary); + + auto font = juce::Font(juce::FontOptions{}.withHeight(12.0f).withStyle("Bold")); + if (interFont != nullptr) { + font = juce::Font(juce::FontOptions(interFont).withHeight(12.0f)); + } + g.setFont(font); + + g.drawText(button.getName(), button.getLocalBounds(), juce::Justification::centred, false); +} + +SupaTriggaEditor::SupaTriggaEditor(SupaTriggaProcessor &p, + juce::AudioProcessorValueTreeState &vts) + : AudioProcessorEditor(&p), audioProcessor(p), apvts(vts) { + setLookAndFeel(&customLookAndFeel); + + // Global section + setupKnob(rearrangeKnob, "global"); + rearrangeAttach = std::make_unique(apvts, SupaTriggaProcessor::PROB_REARRANGE_ID, rearrangeKnob); + + setupKnob(slicesKnob, "global"); + slicesAttach = std::make_unique(apvts, SupaTriggaProcessor::GRANULARITY_ID, slicesKnob); + + setupKnob(silenceKnob, "global"); + silenceAttach = std::make_unique(apvts, SupaTriggaProcessor::PROB_SILENCE_ID, silenceKnob); + + // Brake section + setupKnob(brakeProbKnob, "speed"); + brakeProbAttach = std::make_unique(apvts, SupaTriggaProcessor::PROB_SPEED_ID, brakeProbKnob); + + setupKnob(brakeTimeKnob, "speed"); + brakeTimeAttach = std::make_unique(apvts, SupaTriggaProcessor::SPEED_ID, brakeTimeKnob); + + setupToggle(brakeInstantToggle, "speed"); + brakeInstantAttach = std::make_unique(apvts, SupaTriggaProcessor::INSTANT_SPEED_ID, brakeInstantToggle); + + // Reverse section + setupKnob(reverseProbKnob, "reverse"); + reverseProbAttach = std::make_unique(apvts, SupaTriggaProcessor::PROB_REVERSE_ID, reverseProbKnob); + + setupToggle(reverseInstantToggle, "reverse"); + reverseInstantAttach = std::make_unique(apvts, SupaTriggaProcessor::INSTANT_REVERSE_ID, reverseInstantToggle); + + // Repeat section + setupKnob(repeatProbKnob, "repeat"); + repeatProbAttach = std::make_unique(apvts, SupaTriggaProcessor::PROB_REPEAT_ID, repeatProbKnob); + + setupToggle(repeatInstantToggle, "repeat"); + repeatInstantAttach = std::make_unique(apvts, SupaTriggaProcessor::INSTANT_REPEAT_ID, repeatInstantToggle); + + // Make window non-resizable + setResizable(false, false); + + // Set Window Size + setSize(Theme::Metrics::windowWidth, Theme::Metrics::windowHeight); +} + +SupaTriggaEditor::~SupaTriggaEditor() { setLookAndFeel(nullptr); } + +void SupaTriggaEditor::setupKnob(juce::Slider &slider, + const juce::String &styleClass) { + slider.setSliderStyle(juce::Slider::RotaryHorizontalVerticalDrag); + slider.setTextBoxStyle(juce::Slider::NoTextBox, false, 0, 0); // Text is drawn by LookAndFeel + slider.setRotaryParameters(juce::MathConstants::pi * 1.25f, juce::MathConstants::pi * 2.75f, true); + addAndMakeVisible(slider); + + // Set styling colors + if (styleClass == "global") { + slider.setColour(juce::Slider::rotarySliderFillColourId, Theme::Colors::globalAccent); + } else if (styleClass == "speed") { + slider.setColour(juce::Slider::rotarySliderFillColourId, Theme::Colors::speedAccent); + } else if (styleClass == "reverse") { + slider.setColour(juce::Slider::rotarySliderFillColourId, Theme::Colors::reverseAccent); + } else if (styleClass == "repeat") { + slider.setColour(juce::Slider::rotarySliderFillColourId, Theme::Colors::repeatAccent); + } +} + +void SupaTriggaEditor::setupToggle(juce::ToggleButton &toggle, + const juce::String &styleClass) { + // Configure text change on toggle state change using a listener + toggle.setName(toggle.getToggleState() ? "On" : "Off"); + toggle.onClick = [&toggle] { + toggle.setName(toggle.getToggleState() ? "On" : "Off"); + }; + addAndMakeVisible(toggle); + + if (styleClass == "global") { + toggle.setColour(juce::ToggleButton::tickColourId, Theme::Colors::globalAccent); + } else if (styleClass == "speed") { + toggle.setColour(juce::ToggleButton::tickColourId, Theme::Colors::speedAccent); + } else if (styleClass == "reverse") { + toggle.setColour(juce::ToggleButton::tickColourId, Theme::Colors::reverseAccent); + } else if (styleClass == "repeat") { + toggle.setColour(juce::ToggleButton::tickColourId, Theme::Colors::repeatAccent); + } +} + +void SupaTriggaEditor::paint(juce::Graphics &g) { + // Background + g.fillAll(Theme::Colors::background); + + // Header Panel is flush with the background + + // Draw thick bottom line of header + g.setColour(Theme::Colors::border); + g.drawLine(0.0f, 56.0f, (float)getWidth(), 56.0f, 1.0f); + + // Draw header text + g.setColour(Theme::Colors::textPrimary); + g.setFont(juce::FontOptions(20.0f).withStyle("Bold")); + g.drawText("SUPATRIGGA", 20, 0, 200, 56, juce::Justification::centredLeft, true); + + g.setColour(Theme::Colors::textSecondaryDim); + g.setFont(juce::FontOptions(11.0f).withStyle("Bold")); + g.drawText("BY SMARTELECTRONIX", getWidth() - 200, 0, 180, 56, juce::Justification::centredRight, true); + + // Section Dividers (the cross in the grid) + auto gridBounds = getLocalBounds().withTrimmedTop(56); + int midX = gridBounds.getCentreX(); + int midY = gridBounds.getCentreY(); + + g.setColour(Theme::Colors::border); + // Vertical line (indented top and bottom by 16px) + g.drawLine((float)midX, (float)(gridBounds.getY() + 16), (float)midX, (float)(gridBounds.getBottom() - 16), 1.0f); + // Horizontal line (indented left/right by 20px, gap in center) + g.drawLine(20.0f, (float)midY, (float)(midX - 16), (float)midY, 1.0f); + g.drawLine((float)(midX + 16), (float)midY, (float)(getWidth() - 20), (float)midY, 1.0f); + + // Draw Section Titles + auto drawSectionTitle = [&](const juce::String &title, juce::Colour c, int x, int y) { + g.setColour(c); + g.setFont(juce::FontOptions(14.0f).withStyle("Bold")); + g.drawText(title, x + 20, y + 16, 100, 20, juce::Justification::topLeft, true); + }; + + drawSectionTitle("GLOBAL", Theme::Colors::globalAccent, gridBounds.getX(), gridBounds.getY()); + drawSectionTitle("BRAKE", Theme::Colors::speedAccent, midX, gridBounds.getY()); + drawSectionTitle("REVERSE", Theme::Colors::reverseAccent, gridBounds.getX(), midY); + drawSectionTitle("REPEAT", Theme::Colors::repeatAccent, midX, midY); + + // Draw Sub-labels for controls + auto drawLabel = [&](const juce::String &text, juce::Component &comp) { + g.setColour(Theme::Colors::textSecondary); + g.setFont( juce::FontOptions(11.0f).withStyle("Bold")); + auto bounds = comp.getBounds(); + // Move the text up by drawing it in a box higher above the component + g.drawText(text, bounds.getX() - 20, bounds.getY() - 28, bounds.getWidth() + 40, 20, juce::Justification::centredBottom, false); + }; + + // Global Section Label centers + drawLabel("REARRANGE", rearrangeKnob); + drawLabel("SLICES", slicesKnob); + drawLabel("SILENCE", silenceKnob); + + // Brake Section Label centers + drawLabel("BRAKE PROB", brakeProbKnob); + drawLabel("BRAKE TIME", brakeTimeKnob); + drawLabel("INSTANT", brakeInstantToggle); + + // Reverse Section Label centers + drawLabel("REVERSE PROB", reverseProbKnob); + drawLabel("INSTANT", reverseInstantToggle); + + // Repeat Section Label centers + drawLabel("REPEAT PROB", repeatProbKnob); + drawLabel("INSTANT", repeatInstantToggle); +} + +void SupaTriggaEditor::resized() { + auto gridBounds = getLocalBounds().withTrimmedTop(56); + + int midX = gridBounds.getWidth() / 2; + int midY = gridBounds.getHeight() / 2; + + auto globalRect = gridBounds.withWidth(midX).withHeight(midY); + auto speedRect = globalRect.withX(midX); + auto reverseRect = globalRect.withY(gridBounds.getY() + midY); + auto repeatRect = reverseRect.withX(midX); + + // Controls Row bounds (leave room for title and labels) + auto globalRow = globalRect.withTrimmedTop(40).withTrimmedBottom(18); + auto speedRow = speedRect.withTrimmedTop(40).withTrimmedBottom(18); + auto reverseRow = reverseRect.withTrimmedTop(40).withTrimmedBottom(18); + auto repeatRow = repeatRect.withTrimmedTop(40).withTrimmedBottom(18); + + // juce::FlexBox is ideal here to distribute spacing + auto buildFlex = [](juce::FlexBox &fb) { + fb.justifyContent = juce::FlexBox::JustifyContent::spaceAround; + fb.alignItems = juce::FlexBox::AlignItems::flexEnd; // Bottom align to fix margin above divider + fb.flexDirection = juce::FlexBox::Direction::row; + }; + + auto mapItem = [](juce::Component &c) { + return juce::FlexItem(c).withWidth(80).withHeight(80).withMargin( + juce::FlexItem::Margin(0, 4, 0, 4)); + }; + + { + juce::FlexBox fb; + buildFlex(fb); + fb.items.add(mapItem(rearrangeKnob)); + fb.items.add(mapItem(slicesKnob)); + fb.items.add(mapItem(silenceKnob)); + fb.performLayout(globalRow); + } + + { + juce::FlexBox fb; + buildFlex(fb); + fb.items.add(mapItem(brakeProbKnob)); + fb.items.add(mapItem(brakeTimeKnob)); + fb.items.add(mapItem(brakeInstantToggle)); + fb.performLayout(speedRow); + } + + { + juce::FlexBox fb; + buildFlex(fb); + fb.items.add(mapItem(reverseProbKnob)); + fb.items.add(mapItem(reverseInstantToggle)); + fb.performLayout(reverseRow); + } + + { + juce::FlexBox fb; + buildFlex(fb); + fb.items.add(mapItem(repeatProbKnob)); + fb.items.add(mapItem(repeatInstantToggle)); + fb.performLayout(repeatRow); + } +} \ No newline at end of file diff --git a/supatrigga/Source/SupaTriggaEditor.h b/supatrigga/Source/SupaTriggaEditor.h new file mode 100644 index 0000000..37e5a37 --- /dev/null +++ b/supatrigga/Source/SupaTriggaEditor.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include "PluginProcessor.h" + +class SupaTriggaLookAndFeel : public juce::LookAndFeel_V4 +{ +public: + SupaTriggaLookAndFeel(); + + void drawRotarySlider(juce::Graphics& g, int x, int y, int width, int height, + float sliderPosProportional, float rotaryStartAngle, float rotaryEndAngle, + juce::Slider& slider) override; + + void drawToggleButton(juce::Graphics& g, juce::ToggleButton& button, + bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override; + +private: + juce::Typeface::Ptr interFont; +}; + +class SupaTriggaEditor : public juce::AudioProcessorEditor +{ +public: + SupaTriggaEditor(SupaTriggaProcessor&, juce::AudioProcessorValueTreeState&); + ~SupaTriggaEditor() override; + + void paint(juce::Graphics&) override; + void resized() override; + +private: + [[maybe_unused]] SupaTriggaProcessor& audioProcessor; + juce::AudioProcessorValueTreeState& apvts; + + SupaTriggaLookAndFeel customLookAndFeel; + + void setupKnob(juce::Slider& slider, const juce::String& styleClass); + void setupToggle(juce::ToggleButton& toggle, const juce::String& styleClass); + + // Header Components + // Not strictly needed as component, can be drawn in paint() instead to save overhead. + + // Global Section + juce::Slider rearrangeKnob; + juce::Slider slicesKnob; + juce::Slider silenceKnob; + + // Brake Section + juce::Slider brakeProbKnob; + juce::Slider brakeTimeKnob; + juce::ToggleButton brakeInstantToggle; + + // Reverse Section + juce::Slider reverseProbKnob; + juce::ToggleButton reverseInstantToggle; + + // Repeat Section + juce::Slider repeatProbKnob; + juce::ToggleButton repeatInstantToggle; + + // Attachments + using SliderAttachment = juce::AudioProcessorValueTreeState::SliderAttachment; + using ButtonAttachment = juce::AudioProcessorValueTreeState::ButtonAttachment; + + std::unique_ptr rearrangeAttach; + std::unique_ptr slicesAttach; + std::unique_ptr silenceAttach; + std::unique_ptr brakeProbAttach; + std::unique_ptr brakeTimeAttach; + std::unique_ptr brakeInstantAttach; + std::unique_ptr reverseProbAttach; + std::unique_ptr reverseInstantAttach; + std::unique_ptr repeatProbAttach; + std::unique_ptr repeatInstantAttach; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SupaTriggaEditor) +}; diff --git a/supatrigga/Source/Theme.h b/supatrigga/Source/Theme.h new file mode 100644 index 0000000..5a20f24 --- /dev/null +++ b/supatrigga/Source/Theme.h @@ -0,0 +1,34 @@ +#pragma once +#include + +namespace Theme { + namespace Colors { + inline const juce::Colour background { 0xff16161e }; + inline const juce::Colour track { 0xff2a2a3a }; + inline const juce::Colour innerCircle { 0xff252535 }; + inline const juce::Colour border { 0xff2e2e40 }; + + // Text colors + inline const juce::Colour textPrimary { 0xffe8e8f0 }; + inline const juce::Colour textSecondary { 0xff8888a0 }; + inline const juce::Colour textSecondaryDim{ 0xcc8888a0 }; + inline const juce::Colour textValue { 0xffffffff }; + + // Accent colors + inline const juce::Colour globalAccent { 0xfff0c040 }; + inline const juce::Colour speedAccent { 0xffe05050 }; + inline const juce::Colour reverseAccent { 0xff40c8c8 }; + inline const juce::Colour repeatAccent { 0xffa060e0 }; + } // namespace Colors + + namespace Metrics { + constexpr int windowWidth = 640; + constexpr int windowHeight = 400; + constexpr int headerHeight = 56; + constexpr float knobRadius = 34.0f; + constexpr float innerRadius = 24.0f; + constexpr float trackThickness = 6.0f; + constexpr float rotaryStartAngle = juce::MathConstants::pi * 1.25f; + constexpr float rotaryEndAngle = juce::MathConstants::pi * 2.75f; + } +} \ No newline at end of file