diff --git a/.gitattributes b/.gitattributes index dc030e4..c5eae81 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ models/dx7_vae_model.pt filter=lfs diff=lfs merge=lfs -text +asssets/*.png filter=lfs diff=lfs merge=lfs -text \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index e738aad..ecc46c9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,35 @@ if(UNIX AND NOT APPLE) pkg_check_modules(HARFBUZZ REQUIRED harfbuzz) endif() +# Add binary data for UI assets +juce_add_binary_data(AssetsData + HEADER_NAME "AssetsData.h" + NAMESPACE AssetsData + SOURCES + asssets/global_header.png + asssets/background0.png + asssets/background1.png + asssets/background2.png + asssets/background3.png + asssets/customise_generate_pressed.png + asssets/customise_generate_unpressed.png + asssets/customise_randomise_pressed.png + asssets/customise_randomise_unpressed.png + asssets/customise_slider_knob.png + asssets/customise_slider_track.png + asssets/customise_7_seg_background.png + asssets/randomise_cart.png + asssets/tab_customise_selected_pressed.png + asssets/tab_customise_selected_unpressed.png + asssets/tab_customise_unselected_pressed.png + asssets/tab_customise_unselected_unpressed.png + asssets/tab_randomise_selected_pressed.png + asssets/tab_randomise_selected_unpressed.png + asssets/tab_randomise_unselected_pressed.png + asssets/tab_randomise_unselected_unpressed.png + asssets/DSEG7Classic-Bold.ttf +) + # Create the plugin target juce_add_plugin(NeuralDX7PatchGenerator COMPANY_NAME "NintoracAudio" @@ -62,7 +91,7 @@ juce_add_plugin(NeuralDX7PatchGenerator NEEDS_MIDI_OUTPUT TRUE IS_MIDI_PLUGIN TRUE IS_MIDI_EFFECT TRUE - EDITOR_WANTS_KEYBOARD_FOCUS FALSE + EDITOR_WANTS_KEYBOARD_FOCUS TRUE COPY_PLUGIN_AFTER_BUILD TRUE PLUGIN_MANUFACTURER_CODE NinA PLUGIN_CODE NeuD @@ -94,6 +123,9 @@ target_sources(NeuralDX7PatchGenerator PRIVATE Source/PluginProcessor.cpp Source/PluginEditor.cpp + Source/UI/DX7LatentSliderLookAndFeel.cpp + Source/UI/DX7LatentSlider.cpp + Source/UI/DX7TabComponents.cpp Source/DX7Voice.cpp Source/DX7VoicePacker.cpp Source/DX7BulkPacker.cpp @@ -105,6 +137,7 @@ target_sources(NeuralDX7PatchGenerator # Link libraries target_link_libraries(NeuralDX7PatchGenerator PRIVATE + AssetsData juce::juce_audio_utils juce::juce_audio_processors juce::juce_audio_devices diff --git a/README.md b/README.md index 328e655..0ea9e04 100644 --- a/README.md +++ b/README.md @@ -111,15 +111,26 @@ To use a real trained model instead of the dummy: ```python import torch from agoge import InferenceWorker - + # Load your trained model model = InferenceWorker('hasty-copper-dogfish', 'dx7-vae', with_data=False).model - + # Convert to TorchScript scripted_model = torch.jit.script(model) - + # Save the scripted model scripted_model.save('dx7_vae_model.pt') ``` -3. Replace the dummy model file with the real one. \ No newline at end of file +3. Replace the dummy model file with the real one. + +## Acknowledgements + +This project uses the **DSEG7 Classic** font by keshikan for the 7-segment LED display styling: +- Font: DSEG7 Classic Regular +- Author: keshikan +- Website: https://www.keshikan.net/fonts-e.html +- GitHub: https://github.com/keshikan/DSEG +- License: SIL Open Font License 1.1 + +The DSEG font family provides authentic 7-segment and 14-segment display typefaces for digital display aesthetics. \ No newline at end of file diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 28096a0..8a7749b 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -1,76 +1,117 @@ #include "PluginProcessor.h" #include "PluginEditor.h" +#include "AssetsData.h" + +//============================================================================== +// CustomiseTab Implementation +//============================================================================== CustomiseTab::CustomiseTab(NeuralDX7PatchGeneratorProcessor& processor) : audioProcessor(processor) { // Create latent dimension sliders latentSliders.resize(NeuralModelWrapper::LATENT_DIM); - latentLabels.resize(NeuralModelWrapper::LATENT_DIM); - + for (int i = 0; i < NeuralModelWrapper::LATENT_DIM; ++i) { - latentSliders[i] = std::make_unique(); - latentSliders[i]->setRange(-3.0, 3.0, 0.01); - latentSliders[i]->setValue(0.0); - latentSliders[i]->setSliderStyle(juce::Slider::LinearVertical); - latentSliders[i]->setTextBoxStyle(juce::Slider::TextBoxBelow, false, 60, 20); - latentSliders[i]->addListener(this); + // Unicode subscript 0 is U+2080, so subscript i is U+2080 + i + juce::String label = "Z" + juce::String::charToString(static_cast(0x2080 + i)); + + latentSliders[i] = std::make_unique(label, customLookAndFeel); + latentSliders[i]->getSlider().addListener(this); addAndMakeVisible(*latentSliders[i]); - - latentLabels[i] = std::make_unique("label" + juce::String(i), "Z" + juce::String(i + 1)); - latentLabels[i]->setJustificationType(juce::Justification::centred); - addAndMakeVisible(*latentLabels[i]); } - // Create buttons - generateButton = std::make_unique("Generate & Send"); + // Create Generate button + generateButton = std::make_unique("Generate"); + auto generateNormal = juce::ImageCache::getFromMemory( + AssetsData::customise_generate_unpressed_png, + AssetsData::customise_generate_unpressed_pngSize + ); + auto generatePressed = juce::ImageCache::getFromMemory( + AssetsData::customise_generate_pressed_png, + AssetsData::customise_generate_pressed_pngSize + ); + generateButton->setImages(true, true, true, + generateNormal, 1.0f, juce::Colours::transparentBlack, + generateNormal, 1.0f, juce::Colours::transparentBlack, + generatePressed, 1.0f, juce::Colours::transparentBlack); generateButton->addListener(this); addAndMakeVisible(*generateButton); - - randomizeButton = std::make_unique("Randomize"); + + // Create Randomize button + randomizeButton = std::make_unique("Randomize"); + auto randomizeNormal = juce::ImageCache::getFromMemory( + AssetsData::customise_randomise_unpressed_png, + AssetsData::customise_randomise_unpressed_pngSize + ); + auto randomizePressed = juce::ImageCache::getFromMemory( + AssetsData::customise_randomise_pressed_png, + AssetsData::customise_randomise_pressed_pngSize + ); + randomizeButton->setImages(true, true, true, + randomizeNormal, 1.0f, juce::Colours::transparentBlack, + randomizeNormal, 1.0f, juce::Colours::transparentBlack, + randomizePressed, 1.0f, juce::Colours::transparentBlack); randomizeButton->addListener(this); addAndMakeVisible(*randomizeButton); } CustomiseTab::~CustomiseTab() { + // Cleanup handled by DX7LatentSlider destructor } void CustomiseTab::paint(juce::Graphics& g) { - g.fillAll(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId)); - - g.setColour(juce::Colours::white); - g.setFont(12.0f); - - g.drawText("Control the neural model's latent space to generate DX7 patches", - 10, getHeight() - 30, getWidth() - 20, 20, juce::Justification::centred); + // Don't fill background - let the main background image show through } void CustomiseTab::resized() { auto bounds = getLocalBounds(); - - bounds.removeFromTop(10); // spacing - - // Sliders - auto sliderArea = bounds.removeFromTop(250); - int sliderWidth = sliderArea.getWidth() / NeuralModelWrapper::LATENT_DIM; - - for (int i = 0; i < NeuralModelWrapper::LATENT_DIM; ++i) { - auto sliderBounds = sliderArea.removeFromLeft(sliderWidth).reduced(5); - latentLabels[i]->setBounds(sliderBounds.removeFromTop(20)); - latentSliders[i]->setBounds(sliderBounds); + + // Main vertical flexbox containing sliders and buttons + juce::FlexBox mainVerticalBox; + mainVerticalBox.flexDirection = juce::FlexBox::Direction::column; + + // Horizontal flexbox for sliders + juce::FlexBox slidersBox; + slidersBox.flexDirection = juce::FlexBox::Direction::row; + slidersBox.justifyContent = juce::FlexBox::JustifyContent::spaceAround; + + for (auto& sliderComponent : latentSliders) { + slidersBox.items.add(juce::FlexItem(*sliderComponent) + .withFlex(1.0f) + .withMargin(5) + ); } - - bounds.removeFromTop(20); // spacing - - // Buttons - auto buttonArea = bounds.removeFromTop(40); - int buttonWidth = buttonArea.getWidth() / 2; - - generateButton->setBounds(buttonArea.removeFromLeft(buttonWidth).reduced(10)); - randomizeButton->setBounds(buttonArea.reduced(10)); + + // Horizontal flexbox for buttons + juce::FlexBox buttonsBox; + buttonsBox.flexDirection = juce::FlexBox::Direction::row; + buttonsBox.justifyContent = juce::FlexBox::JustifyContent::center; + + buttonsBox.items.add(juce::FlexItem(*generateButton) + .withFlex(1.0f) + .withMargin(5) + ); + + buttonsBox.items.add(juce::FlexItem(*randomizeButton) + .withFlex(1.0f) + .withMargin(5) + ); + + // Add sliders and buttons to main vertical box + mainVerticalBox.items.add(juce::FlexItem(slidersBox) + .withFlex(1.0f) + .withMargin(juce::FlexItem::Margin(10, 0, 0, 0)) + ); + + mainVerticalBox.items.add(juce::FlexItem(buttonsBox) + .withHeight(40.0f) + ); + + mainVerticalBox.performLayout(bounds.toFloat()); } void CustomiseTab::sliderValueChanged(juce::Slider* slider) @@ -87,8 +128,8 @@ void CustomiseTab::buttonClicked(juce::Button* button) else if (button == randomizeButton.get()) { std::cout << "Randomize button clicked!" << std::endl; juce::Random random; - for (auto& slider : latentSliders) { - slider->setValue(random.nextFloat() * 6.0f - 3.0f); // Range -3 to 3 + for (auto& sliderComponent : latentSliders) { + sliderComponent->getSlider().setValue(random.nextFloat() * 6.0f - 3.0f); // Range -3 to 3 } updateLatentValues(); } @@ -98,18 +139,27 @@ void CustomiseTab::updateLatentValues() { std::vector values; values.reserve(NeuralModelWrapper::LATENT_DIM); - - for (const auto& slider : latentSliders) { - values.push_back(static_cast(slider->getValue())); + + for (const auto& sliderComponent : latentSliders) { + values.push_back(static_cast(sliderComponent->getSlider().getValue())); } - + audioProcessor.setLatentValues(values); } RandomiseTab::RandomiseTab(NeuralDX7PatchGeneratorProcessor& processor) : audioProcessor(processor) { - randomiseButton = std::make_unique("Randomise"); + // Create cart image button + randomiseButton = std::make_unique("Randomise"); + auto cartImage = juce::ImageCache::getFromMemory( + AssetsData::randomise_cart_png, + AssetsData::randomise_cart_pngSize + ); + randomiseButton->setImages(true, true, true, + cartImage, 1.0f, juce::Colours::transparentBlack, + cartImage, 1.0f, juce::Colours::transparentBlack, + cartImage, 0.8f, juce::Colours::transparentBlack); randomiseButton->addListener(this); addAndMakeVisible(*randomiseButton); } @@ -120,13 +170,20 @@ RandomiseTab::~RandomiseTab() void RandomiseTab::paint(juce::Graphics& g) { - g.fillAll(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId)); + // Don't fill background - let the main background image show through } void RandomiseTab::resized() { auto bounds = getLocalBounds(); - auto buttonBounds = bounds.withSizeKeepingCentre(200, 50); + + // Cart image aspect ratio is 369:386 (roughly square, slightly taller) + // Size it to fit nicely in the tab, centered + auto maxSize = juce::jmin(bounds.getWidth(), bounds.getHeight()) * 0.6f; // 60% of available space + auto cartWidth = static_cast(maxSize); + auto cartHeight = static_cast(maxSize * (386.0f / 369.0f)); // Maintain aspect ratio + + auto buttonBounds = bounds.withSizeKeepingCentre(cartWidth, cartHeight); randomiseButton->setBounds(buttonBounds); } @@ -139,50 +196,119 @@ void RandomiseTab::buttonClicked(juce::Button* button) } NeuralDX7PatchGeneratorEditor::NeuralDX7PatchGeneratorEditor (NeuralDX7PatchGeneratorProcessor& p) - : AudioProcessorEditor (&p), audioProcessor (p) + : AudioProcessorEditor (&p), audioProcessor (p), currentBackgroundIndex(0) { - // Create title label - titleLabel = std::make_unique("title", "NeuralDX7 Patch Generator"); - titleLabel->setFont(juce::Font(20.0f, juce::Font::bold)); - titleLabel->setJustificationType(juce::Justification::centred); - addAndMakeVisible(*titleLabel); - + // Load header image + headerImage = juce::ImageCache::getFromMemory( + AssetsData::global_header_png, + AssetsData::global_header_pngSize + ); + + // Load all background images + backgroundImages.resize(4); + backgroundImages[0] = juce::ImageCache::getFromMemory( + AssetsData::background0_png, + AssetsData::background0_pngSize + ); + backgroundImages[1] = juce::ImageCache::getFromMemory( + AssetsData::background1_png, + AssetsData::background1_pngSize + ); + backgroundImages[2] = juce::ImageCache::getFromMemory( + AssetsData::background2_png, + AssetsData::background2_pngSize + ); + backgroundImages[3] = juce::ImageCache::getFromMemory( + AssetsData::background3_png, + AssetsData::background3_pngSize + ); + // Create tabs customiseTab = std::make_unique(audioProcessor); randomiseTab = std::make_unique(audioProcessor); - - // Create tabbed component - tabbedComponent = std::make_unique(juce::TabbedButtonBar::TabsAtTop); - tabbedComponent->setLookAndFeel(&customLookAndFeel); - tabbedComponent->addTab("Randomise", juce::Colours::lightgrey, randomiseTab.get(), false); - tabbedComponent->addTab("Customise", juce::Colours::lightgrey, customiseTab.get(), false); - tabbedComponent->setTabBarDepth(30); - + + // Create custom tabbed component + tabbedComponent = std::make_unique(); + tabbedComponent->addTab("Randomise", juce::Colours::transparentBlack, randomiseTab.get(), false); + tabbedComponent->addTab("Customise", juce::Colours::transparentBlack, customiseTab.get(), false); + addAndMakeVisible(*tabbedComponent); - + + // Enable keyboard focus to receive key events + setWantsKeyboardFocus(true); + // Call setSize() LAST after all components are initialized - setSize (600, 400); + setSize (960, 540); } NeuralDX7PatchGeneratorEditor::~NeuralDX7PatchGeneratorEditor() { - tabbedComponent->setLookAndFeel(nullptr); } void NeuralDX7PatchGeneratorEditor::paint (juce::Graphics& g) { - g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId)); + // Draw current background image + if (currentBackgroundIndex >= 0 && currentBackgroundIndex < backgroundImages.size() + && backgroundImages[currentBackgroundIndex].isValid()) + { + g.drawImage(backgroundImages[currentBackgroundIndex], + getLocalBounds().toFloat(), + juce::RectanglePlacement::stretchToFit); + } + else + { + // Fallback to solid color if image fails to load + g.fillAll(juce::Colour(0xff3a2f2f)); + } + + // Draw header image with 2.5% margin from top and left, scaled to 66% of editor width + if (headerImage.isValid()) + { + auto bounds = getLocalBounds(); + auto marginX = static_cast(bounds.getWidth() * 0.025f); + auto marginY = static_cast(bounds.getHeight() * 0.025f); + + // Calculate header size: 66% of editor width, maintain aspect ratio + auto headerWidth = static_cast(bounds.getWidth() * 0.66f); + auto aspectRatio = static_cast(headerImage.getHeight()) / static_cast(headerImage.getWidth()); + auto headerHeight = static_cast(headerWidth * aspectRatio); + + // Position header at 2.5% from top and left + auto headerBounds = juce::Rectangle(marginX, marginY, headerWidth, headerHeight); + + g.drawImage(headerImage, + headerBounds.toFloat(), + juce::RectanglePlacement::stretchToFit); + } } void NeuralDX7PatchGeneratorEditor::resized() { auto bounds = getLocalBounds(); - - // Title - titleLabel->setBounds(bounds.removeFromTop(40).reduced(10)); - - bounds.removeFromTop(5); // spacing - + + // Reserve top 18% for header area + auto headerHeight = static_cast(bounds.getHeight() * 0.20f); + bounds.removeFromTop(headerHeight); + + // Add 2.5% margin around the tabbed component + auto marginX = static_cast(bounds.getWidth() * 0.025f); + auto marginY = static_cast(bounds.getHeight() * 0.025f); + bounds.reduce(marginX, marginY); + // Tabbed component takes the rest of the space tabbedComponent->setBounds(bounds); +} + +bool NeuralDX7PatchGeneratorEditor::keyPressed (const juce::KeyPress& key) +{ + // Check if 'b' key is pressed + if (key.getKeyCode() == 'b' || key.getKeyCode() == 'B') + { + // Cycle to next background + currentBackgroundIndex = (currentBackgroundIndex + 1) % backgroundImages.size(); + repaint(); // Redraw with new background + return true; // Key was handled + } + + return false; // Key not handled } \ No newline at end of file diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index 976f7f9..05dc7b2 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -2,15 +2,9 @@ #include #include "PluginProcessor.h" - -class FullWidthLookAndFeel : public juce::LookAndFeel_V4 -{ -public: - int getTabButtonBestWidth(juce::TabBarButton& button, int tabDepth) override - { - return button.getTabbedButtonBar().getWidth() / button.getTabbedButtonBar().getNumTabs(); - } -}; +#include "UI/DX7LatentSliderLookAndFeel.h" +#include "UI/DX7LatentSlider.h" +#include "UI/DX7TabComponents.h" class CustomiseTab : public juce::Component, public juce::Slider::Listener, @@ -22,19 +16,20 @@ class CustomiseTab : public juce::Component, void paint (juce::Graphics&) override; void resized() override; - + void sliderValueChanged (juce::Slider* slider) override; void buttonClicked (juce::Button* button) override; private: NeuralDX7PatchGeneratorProcessor& audioProcessor; - - std::vector> latentSliders; - std::vector> latentLabels; - - std::unique_ptr generateButton; - std::unique_ptr randomizeButton; - + + DX7LatentSliderLookAndFeel customLookAndFeel; + + std::vector> latentSliders; + + std::unique_ptr generateButton; + std::unique_ptr randomizeButton; + void updateLatentValues(); JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomiseTab) @@ -49,12 +44,12 @@ class RandomiseTab : public juce::Component, void paint (juce::Graphics&) override; void resized() override; - + void buttonClicked (juce::Button* button) override; private: NeuralDX7PatchGeneratorProcessor& audioProcessor; - std::unique_ptr randomiseButton; + std::unique_ptr randomiseButton; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RandomiseTab) }; @@ -67,13 +62,16 @@ class NeuralDX7PatchGeneratorEditor : public juce::AudioProcessorEditor void paint (juce::Graphics&) override; void resized() override; + bool keyPressed (const juce::KeyPress& key) override; private: NeuralDX7PatchGeneratorProcessor& audioProcessor; - - FullWidthLookAndFeel customLookAndFeel; - std::unique_ptr titleLabel; - std::unique_ptr tabbedComponent; + + juce::Image headerImage; + std::vector backgroundImages; + int currentBackgroundIndex; + + std::unique_ptr tabbedComponent; std::unique_ptr customiseTab; std::unique_ptr randomiseTab; diff --git a/Source/UI/DX7LatentSlider.cpp b/Source/UI/DX7LatentSlider.cpp new file mode 100644 index 0000000..3269492 --- /dev/null +++ b/Source/UI/DX7LatentSlider.cpp @@ -0,0 +1,73 @@ +#include "DX7LatentSlider.h" +#include "AssetsData.h" + +DX7LatentSlider::DX7LatentSlider(const juce::String& labelText, DX7LatentSliderLookAndFeel& lookAndFeel) + : customLookAndFeel(lookAndFeel) +{ + // Load 7-segment background image + backgroundImage = juce::ImageCache::getFromMemory( + AssetsData::customise_7_seg_background_png, + AssetsData::customise_7_seg_background_pngSize + ); + + // Setup label + label.setText(labelText, juce::dontSendNotification); + label.setJustificationType(juce::Justification::centred); + label.setColour(juce::Label::textColourId, juce::Colours::white); + addAndMakeVisible(label); + + // Setup slider + slider.setRange(-3.0, 3.0, 0.01); + slider.setValue(0.0); + slider.setSliderStyle(juce::Slider::LinearVertical); + slider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 60, 20); + slider.setLookAndFeel(&customLookAndFeel); + addAndMakeVisible(slider); +} + +DX7LatentSlider::~DX7LatentSlider() +{ + slider.setLookAndFeel(nullptr); +} + +void DX7LatentSlider::paint(juce::Graphics& g) +{ + // Draw the 7-segment background image if available, matching slider width + if (backgroundImage.isValid()) + { + auto bounds = getLocalBounds(); + auto sliderWidth = slider.getWidth() + 10; // Match the width of the slider and its padding + auto imageX = (bounds.getWidth() - sliderWidth) / 2.0f; + + g.drawImage(backgroundImage, + juce::Rectangle(imageX, 0.0f, sliderWidth, static_cast(bounds.getHeight())), + juce::RectanglePlacement::stretchToFit); + } +} + +void DX7LatentSlider::resized() +{ + auto bounds = getLocalBounds(); + + // Create vertical flexbox: label at top, slider in middle (growing), textbox at bottom + juce::FlexBox flexBox; + flexBox.flexDirection = juce::FlexBox::Direction::column; + flexBox.justifyContent = juce::FlexBox::JustifyContent::flexStart; + flexBox.alignItems = juce::FlexBox::AlignItems::center; + + // Label at top (fixed height) + flexBox.items.add(juce::FlexItem(label) + .withHeight(20.0f) + .withWidth(60.0f) + ); + + // Slider in middle (flex grow to fill available space) + // The slider's text box is positioned by JUCE based on setTextBoxStyle + flexBox.items.add(juce::FlexItem(slider) + .withFlex(1.0f) + .withWidth(60.0f) + .withMargin(juce::FlexItem::Margin(0, 10, 0, 10)) + ); + + flexBox.performLayout(bounds.toFloat()); +} diff --git a/Source/UI/DX7LatentSlider.h b/Source/UI/DX7LatentSlider.h new file mode 100644 index 0000000..a5090df --- /dev/null +++ b/Source/UI/DX7LatentSlider.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include "DX7LatentSliderLookAndFeel.h" + +class DX7LatentSlider : public juce::Component +{ +public: + DX7LatentSlider(const juce::String& labelText, DX7LatentSliderLookAndFeel& lookAndFeel); + ~DX7LatentSlider() override; + + void paint(juce::Graphics& g) override; + void resized() override; + + juce::Slider& getSlider() { return slider; } + +private: + DX7LatentSliderLookAndFeel& customLookAndFeel; + juce::Image backgroundImage; + + juce::Label label; + juce::Slider slider; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DX7LatentSlider) +}; diff --git a/Source/UI/DX7LatentSliderLookAndFeel.cpp b/Source/UI/DX7LatentSliderLookAndFeel.cpp new file mode 100644 index 0000000..c0350bb --- /dev/null +++ b/Source/UI/DX7LatentSliderLookAndFeel.cpp @@ -0,0 +1,122 @@ +#include "DX7LatentSliderLookAndFeel.h" +#include "AssetsData.h" + +DX7LatentSliderLookAndFeel::DX7LatentSliderLookAndFeel() +{ + // Load slider images from embedded assets + sliderTrackImage = juce::ImageCache::getFromMemory( + AssetsData::customise_slider_track_png, + AssetsData::customise_slider_track_pngSize + ); + + sliderKnobImage = juce::ImageCache::getFromMemory( + AssetsData::customise_slider_knob_png, + AssetsData::customise_slider_knob_pngSize + ); + + sevenSegBackgroundImage = juce::ImageCache::getFromMemory( + AssetsData::customise_7_seg_background_png, + AssetsData::customise_7_seg_background_pngSize + ); + + // Set dark color scheme + setColour(juce::ResizableWindow::backgroundColourId, juce::Colour(0xff3a2f2f)); + setColour(juce::Slider::backgroundColourId, juce::Colours::transparentBlack); + setColour(juce::Slider::trackColourId, juce::Colours::transparentBlack); + setColour(juce::Slider::textBoxTextColourId, juce::Colours::white); + setColour(juce::Slider::textBoxBackgroundColourId, juce::Colour(0xff2a2020)); + setColour(juce::Slider::textBoxOutlineColourId, juce::Colours::transparentBlack); +} + +void DX7LatentSliderLookAndFeel::drawLinearSlider(juce::Graphics& g, + int x, int y, + int width, int height, + float sliderPos, + float minSliderPos, + float maxSliderPos, + juce::Slider::SliderStyle style, + juce::Slider& slider) +{ + if (style == juce::Slider::LinearVertical && sliderTrackImage.isValid()) + { + // Calculate track scale factor based on height to fill full vertical space + auto trackScaleFactor = static_cast(height) / sliderTrackImage.getHeight(); + + // Calculate track height and width maintaining aspect ratio + auto trackHeight = height; + auto trackWidth = static_cast(sliderTrackImage.getWidth() * trackScaleFactor); + + // Center the track horizontally if it's narrower than available width + auto trackX = x + (width - trackWidth) / 2; + + auto trackBounds = juce::Rectangle(trackX, y, trackWidth, trackHeight); + + // Draw the track image maintaining aspect ratio + g.drawImage(sliderTrackImage, + trackBounds.toFloat(), + juce::RectanglePlacement::stretchToFit); + + // Now draw the thumb on top + drawLinearSliderThumb(g, x, y, width, height, sliderPos, + minSliderPos, maxSliderPos, style, slider); + } + else + { + // Fallback to default rendering + LookAndFeel_V4::drawLinearSlider(g, x, y, width, height, sliderPos, + minSliderPos, maxSliderPos, style, slider); + } +} + +void DX7LatentSliderLookAndFeel::drawLinearSliderThumb(juce::Graphics& g, + int x, int y, + int width, int height, + float sliderPos, + float minSliderPos, + float maxSliderPos, + juce::Slider::SliderStyle style, + juce::Slider& slider) +{ + if (style == juce::Slider::LinearVertical && sliderKnobImage.isValid() && sliderTrackImage.isValid()) + { + // Calculate the same scale factor as used for the track + auto trackScaleFactor = static_cast(width) / sliderTrackImage.getWidth(); + + // Calculate knob dimensions using the same scale factor as the track + auto knobWidth = sliderKnobImage.getWidth() * trackScaleFactor; + auto knobHeight = sliderKnobImage.getHeight() * trackScaleFactor; + + // Center knob at 1/4 of the track width + auto knobX = static_cast(x) + (width / 4.0f) - (knobWidth / 2.0f); + auto knobY = sliderPos - knobHeight / 2.0f; + + // Draw the knob image with same scale factor as track + g.drawImage(sliderKnobImage, + juce::Rectangle(knobX, knobY, knobWidth, knobHeight), + juce::RectanglePlacement::stretchToFit); + } + else + { + // Fallback to default rendering + LookAndFeel_V4::drawLinearSliderThumb(g, x, y, width, height, sliderPos, + minSliderPos, maxSliderPos, style, slider); + } +} + +int DX7LatentSliderLookAndFeel::getSliderThumbRadius(juce::Slider& slider) +{ + // Return the height of the knob image + if (sliderKnobImage.isValid()) + return sliderKnobImage.getHeight() / 2; + + return LookAndFeel_V4::getSliderThumbRadius(slider); +} + +juce::Label* DX7LatentSliderLookAndFeel::createSliderTextBox(juce::Slider& slider) +{ + return new SevenSegmentLabel( + sevenSegBackgroundImage, + AssetsData::DSEG7ClassicBold_ttf, + AssetsData::DSEG7ClassicBold_ttfSize + ); +} diff --git a/Source/UI/DX7LatentSliderLookAndFeel.h b/Source/UI/DX7LatentSliderLookAndFeel.h new file mode 100644 index 0000000..08430a0 --- /dev/null +++ b/Source/UI/DX7LatentSliderLookAndFeel.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include "SevenSegmentLabel.h" + +class DX7LatentSliderLookAndFeel : public juce::LookAndFeel_V4 +{ +public: + DX7LatentSliderLookAndFeel(); + + void drawLinearSlider(juce::Graphics& g, + int x, int y, + int width, int height, + float sliderPos, + float minSliderPos, + float maxSliderPos, + juce::Slider::SliderStyle style, + juce::Slider& slider) override; + + void drawLinearSliderThumb(juce::Graphics& g, + int x, int y, + int width, int height, + float sliderPos, + float minSliderPos, + float maxSliderPos, + juce::Slider::SliderStyle style, + juce::Slider& slider) override; + + int getSliderThumbRadius(juce::Slider& slider) override; + + juce::Label* createSliderTextBox(juce::Slider& slider) override; + +private: + juce::Image sliderTrackImage; + juce::Image sliderKnobImage; + juce::Image sevenSegBackgroundImage; +}; diff --git a/Source/UI/DX7TabComponents.cpp b/Source/UI/DX7TabComponents.cpp new file mode 100644 index 0000000..5c7fd68 --- /dev/null +++ b/Source/UI/DX7TabComponents.cpp @@ -0,0 +1,186 @@ +#include "DX7TabComponents.h" +#include "AssetsData.h" + +//============================================================================== +// DX7TabButton Implementation +//============================================================================== + +DX7TabButton::DX7TabButton(const juce::String& name, juce::TabbedButtonBar& bar) + : TabBarButton(name, bar) +{ + // Load the appropriate images based on tab name + if (name == "Randomise") + { + selectedPressedImage = juce::ImageCache::getFromMemory( + AssetsData::tab_randomise_selected_pressed_png, + AssetsData::tab_randomise_selected_pressed_pngSize + ); + selectedUnpressedImage = juce::ImageCache::getFromMemory( + AssetsData::tab_randomise_selected_unpressed_png, + AssetsData::tab_randomise_selected_unpressed_pngSize + ); + unselectedPressedImage = juce::ImageCache::getFromMemory( + AssetsData::tab_randomise_unselected_pressed_png, + AssetsData::tab_randomise_unselected_pressed_pngSize + ); + unselectedUnpressedImage = juce::ImageCache::getFromMemory( + AssetsData::tab_randomise_unselected_unpressed_png, + AssetsData::tab_randomise_unselected_unpressed_pngSize + ); + } + else if (name == "Customise") + { + selectedPressedImage = juce::ImageCache::getFromMemory( + AssetsData::tab_customise_selected_pressed_png, + AssetsData::tab_customise_selected_pressed_pngSize + ); + selectedUnpressedImage = juce::ImageCache::getFromMemory( + AssetsData::tab_customise_selected_unpressed_png, + AssetsData::tab_customise_selected_unpressed_pngSize + ); + unselectedPressedImage = juce::ImageCache::getFromMemory( + AssetsData::tab_customise_unselected_pressed_png, + AssetsData::tab_customise_unselected_pressed_pngSize + ); + unselectedUnpressedImage = juce::ImageCache::getFromMemory( + AssetsData::tab_customise_unselected_unpressed_png, + AssetsData::tab_customise_unselected_unpressed_pngSize + ); + } +} + +void DX7TabButton::paintButton(juce::Graphics& g, bool isMouseOverButton, bool isButtonDown) +{ + // Determine which image to use based on state + juce::Image* imageToUse = nullptr; + + if (getToggleState()) // Selected tab + { + if (isButtonDown) + imageToUse = &selectedPressedImage; + else + imageToUse = &selectedUnpressedImage; + } + else // Unselected tab + { + if (isButtonDown) + imageToUse = &unselectedPressedImage; + else + imageToUse = &unselectedUnpressedImage; + } + + // Draw the tab background image + // The images already contain text, stretch to fit the tab bounds + if (imageToUse != nullptr && imageToUse->isValid()) + { + g.drawImage(*imageToUse, + getLocalBounds().toFloat(), + juce::RectanglePlacement::stretchToFit); + } + +} + +int DX7TabButton::getBestTabLength(int depth) +{ + // Calculate width to fill available space evenly between tabs + auto& bar = getTabbedButtonBar(); + auto numTabs = bar.getNumTabs(); + + if (numTabs == 0) + return 100; + + // Get total width + auto totalWidth = bar.getWidth(); + + // Each tab is 33% of total width + return static_cast(totalWidth * 0.33f); +} + +float DX7TabButton::getImageAspectRatio() const +{ + // Get aspect ratio from the loaded images (height / width) + // Use selectedUnpressedImage as reference since all images should have same dimensions + if (selectedUnpressedImage.isValid()) + { + auto width = selectedUnpressedImage.getWidth(); + auto height = selectedUnpressedImage.getHeight(); + + if (width > 0) + return static_cast(height) / static_cast(width); + } + + // Fallback to hardcoded aspect ratio if image not loaded + return 85.0f / 734.0f; +} + +//============================================================================== +// DX7TabbedComponent Implementation +//============================================================================== + +DX7TabbedComponent::DX7TabbedComponent() + : TabbedComponent(juce::TabbedButtonBar::TabsAtTop) +{ + // Remove the outline/border around the tabbed component + setOutline(0); + + // Remove the line under the tab buttons + getTabbedButtonBar().setMinimumTabScaleFactor(0.0f); + + // Make tab bar background transparent + getLookAndFeel().setColour(juce::TabbedButtonBar::tabOutlineColourId, juce::Colours::transparentBlack); + getLookAndFeel().setColour(juce::TabbedComponent::outlineColourId, juce::Colours::transparentBlack); +} + +void DX7TabbedComponent::resized() +{ + // Calculate tab bar depth based on aspect ratio from actual images + // Each tab is 33% of total width, so calculate height to maintain aspect ratio + auto tabWidth = getWidth() * 0.33f; + + // Get aspect ratio dynamically from first tab button if available + float aspectRatio = 85.0f / 734.0f; // Fallback default + + if (getNumTabs() > 0) + { + if (auto* firstButton = dynamic_cast(getTabbedButtonBar().getTabButton(0))) + { + aspectRatio = firstButton->getImageAspectRatio(); + } + } + + auto tabHeight = static_cast(tabWidth * aspectRatio); + + setTabBarDepth(tabHeight); + + // Let JUCE do the initial layout + TabbedComponent::resized(); + + // Use FlexBox to position tabs with equal spacing + auto& tabBar = getTabbedButtonBar(); + auto numTabs = getNumTabs(); + + if (numTabs > 0) + { + juce::FlexBox tabFlexBox; + tabFlexBox.flexDirection = juce::FlexBox::Direction::row; + tabFlexBox.justifyContent = juce::FlexBox::JustifyContent::spaceAround; + + // Add each tab button to the flexbox with 33% width + for (int i = 0; i < numTabs; ++i) + { + if (auto* button = tabBar.getTabButton(i)) + { + tabFlexBox.items.add(juce::FlexItem(*button) + .withFlex(0, 0, tabBar.getWidth() * 0.33f)); + } + } + + // Perform layout on the tab bar bounds + tabFlexBox.performLayout(tabBar.getLocalBounds().toFloat()); + } +} + +juce::TabBarButton* DX7TabbedComponent::createTabButton(const juce::String& tabName, int tabIndex) +{ + return new DX7TabButton(tabName, getTabbedButtonBar()); +} diff --git a/Source/UI/DX7TabComponents.h b/Source/UI/DX7TabComponents.h new file mode 100644 index 0000000..4987ba3 --- /dev/null +++ b/Source/UI/DX7TabComponents.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +class DX7TabButton : public juce::TabBarButton +{ +public: + DX7TabButton(const juce::String& name, juce::TabbedButtonBar& bar); + + void paintButton(juce::Graphics& g, bool isMouseOverButton, bool isButtonDown) override; + + int getBestTabLength(int depth) override; + + float getImageAspectRatio() const; + +private: + juce::Image selectedPressedImage; + juce::Image selectedUnpressedImage; + juce::Image unselectedPressedImage; + juce::Image unselectedUnpressedImage; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DX7TabButton) +}; + +class DX7TabbedComponent : public juce::TabbedComponent +{ +public: + DX7TabbedComponent(); + + juce::TabBarButton* createTabButton(const juce::String& tabName, int tabIndex) override; + + void resized() override; + +private: + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DX7TabbedComponent) +}; diff --git a/Source/UI/SevenSegmentLabel.h b/Source/UI/SevenSegmentLabel.h new file mode 100644 index 0000000..febaa14 --- /dev/null +++ b/Source/UI/SevenSegmentLabel.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +// Custom label for 7-segment display with background image +class SevenSegmentLabel : public juce::Label +{ +public: + SevenSegmentLabel(const juce::Image& backgroundImg, const void* fontData, size_t fontDataSize) + : backgroundImage(backgroundImg) + { + // Configure as editable slider textbox + setJustificationType(juce::Justification::centredRight); + setKeyboardType(juce::TextInputTarget::decimalKeyboard); + setEditable(false, false, false); + + // Load and apply the 7-segment display font with bold styling and increased weight + auto typeface = juce::Typeface::createSystemTypefaceFor(fontData, fontDataSize); + setFont(juce::Font(typeface).withHeight(14.0f).withHorizontalScale(1.1f)); + + // Style for LED display appearance + setColour(juce::Label::textColourId, juce::Colour(0xff940034)); // Red LED + setColour(juce::Label::backgroundColourId, juce::Colours::transparentBlack); // Transparent - image drawn in paint() + setColour(juce::Label::outlineColourId, juce::Colours::transparentBlack); + } + + void paint(juce::Graphics& g) override + { + // Draw the background image + if (backgroundImage.isValid()) + { + g.drawImage(backgroundImage, + getLocalBounds().toFloat(), + juce::RectanglePlacement::stretchToFit); + } + + // Let the parent class draw the text on top + juce::Label::paint(g); + } + +private: + juce::Image backgroundImage; +}; diff --git a/asssets/DSEG7Classic-Bold.ttf b/asssets/DSEG7Classic-Bold.ttf new file mode 100644 index 0000000..5f71db4 Binary files /dev/null and b/asssets/DSEG7Classic-Bold.ttf differ diff --git a/asssets/background0.png b/asssets/background0.png new file mode 100644 index 0000000..3c80b8f --- /dev/null +++ b/asssets/background0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5a1d2f7fad7e470933bb87b0b0ced2ec55636b0aa637d4d6c8323498a86bb3b +size 3375991 diff --git a/asssets/background1.png b/asssets/background1.png new file mode 100644 index 0000000..81441fe --- /dev/null +++ b/asssets/background1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45ff3b91a305698d9f050dd84081086b7f4530cc480afd0236bfaf6ff52e6551 +size 3375806 diff --git a/asssets/background2.png b/asssets/background2.png new file mode 100644 index 0000000..629c8c3 --- /dev/null +++ b/asssets/background2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:468a5cbb64556e6f57e03e3836aefd03e12b013911b5a6a4cbb3bd154d356a10 +size 3256005 diff --git a/asssets/background3.png b/asssets/background3.png new file mode 100644 index 0000000..88528c8 --- /dev/null +++ b/asssets/background3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80a8dcb33813a84dac6059d73634030cc8add4e1867cea441d5511e39c9fa937 +size 3932337 diff --git a/asssets/customise_7_seg_background.png b/asssets/customise_7_seg_background.png new file mode 100644 index 0000000..15b0aab --- /dev/null +++ b/asssets/customise_7_seg_background.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ca96d1d6e7ce3c83cc88efe5e18e573badf6fdf092120c28e14b1439c447cce +size 7344 diff --git a/asssets/customise_generate_pressed.png b/asssets/customise_generate_pressed.png new file mode 100644 index 0000000..dec127e --- /dev/null +++ b/asssets/customise_generate_pressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b9878da130a466fed230ceb89479c58887c319e16310739d895e8747cc7106c +size 23602 diff --git a/asssets/customise_generate_unpressed.png b/asssets/customise_generate_unpressed.png new file mode 100644 index 0000000..de87048 --- /dev/null +++ b/asssets/customise_generate_unpressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba8a9eab76c21e93269356d657d3427c0a04315058dfb5dfb2f6e6c92cb2767f +size 29338 diff --git a/asssets/customise_randomise_pressed.png b/asssets/customise_randomise_pressed.png new file mode 100644 index 0000000..48f9c47 --- /dev/null +++ b/asssets/customise_randomise_pressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f01c6073a537976cab2f6269baa2fe56664a7447ce7f6a6e4c3080abddb5e79 +size 22100 diff --git a/asssets/customise_randomise_unpressed.png b/asssets/customise_randomise_unpressed.png new file mode 100644 index 0000000..f4ba045 --- /dev/null +++ b/asssets/customise_randomise_unpressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91e42579dd844cdebd5f2977c3a5c021b7815047d7e8b05ca5f40ecf9aad0349 +size 22676 diff --git a/asssets/customise_slider_knob.png b/asssets/customise_slider_knob.png new file mode 100644 index 0000000..4810a21 --- /dev/null +++ b/asssets/customise_slider_knob.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca8b40c51a06137ca32af23b610d239f13700dff0f8b0ddb185264106191cc6b +size 4949 diff --git a/asssets/customise_slider_track.png b/asssets/customise_slider_track.png new file mode 100644 index 0000000..f45b995 --- /dev/null +++ b/asssets/customise_slider_track.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11fa22f998fd53ff337022e771dbd3b05c006a6d1ae3a35dd6159b17c4cd775e +size 8309 diff --git a/asssets/global_header.png b/asssets/global_header.png new file mode 100644 index 0000000..3b05c73 --- /dev/null +++ b/asssets/global_header.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d23b90d107a22cac77d3314db1727bddc01e77ad0dcfe7f2ca7306f966d6b6cf +size 47555 diff --git a/asssets/global_header_alt.png b/asssets/global_header_alt.png new file mode 100644 index 0000000..dfdea07 --- /dev/null +++ b/asssets/global_header_alt.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04e33a37558d61be90db6e0c4ba9c8905180fa0fb11ab594050a3bcb9f59b324 +size 30520 diff --git a/asssets/randomise_cart.png b/asssets/randomise_cart.png new file mode 100644 index 0000000..32cb56e --- /dev/null +++ b/asssets/randomise_cart.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0bbd62f6562cd822e803d5bea685bbc0b175b60da0aced4c98639f0dc9e59290 +size 147921 diff --git a/asssets/tab_customise_selected_pressed.png b/asssets/tab_customise_selected_pressed.png new file mode 100644 index 0000000..2ada0ee --- /dev/null +++ b/asssets/tab_customise_selected_pressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b687f32b3427b30fcccbcb6dea801256cb72871377b46593618093c0b9e0e37d +size 23528 diff --git a/asssets/tab_customise_selected_unpressed.png b/asssets/tab_customise_selected_unpressed.png new file mode 100644 index 0000000..6ca9d75 --- /dev/null +++ b/asssets/tab_customise_selected_unpressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65974575b28f461e4e27debe2555070ed7e163f7abdaf005ad72d404f931b3fa +size 22599 diff --git a/asssets/tab_customise_unselected_pressed.png b/asssets/tab_customise_unselected_pressed.png new file mode 100644 index 0000000..2954dfa --- /dev/null +++ b/asssets/tab_customise_unselected_pressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d01a9f8d8bfd11512f58d4418a2b6f091ebbc9281e2212ac4f7077d59dfc27c +size 39111 diff --git a/asssets/tab_customise_unselected_unpressed.png b/asssets/tab_customise_unselected_unpressed.png new file mode 100644 index 0000000..2954dfa --- /dev/null +++ b/asssets/tab_customise_unselected_unpressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d01a9f8d8bfd11512f58d4418a2b6f091ebbc9281e2212ac4f7077d59dfc27c +size 39111 diff --git a/asssets/tab_randomise_selected_pressed.png b/asssets/tab_randomise_selected_pressed.png new file mode 100644 index 0000000..d37fb03 --- /dev/null +++ b/asssets/tab_randomise_selected_pressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8e750cc532ed8b7d0e383ef5021a692dd125785568706012d025b341efd57c5 +size 23080 diff --git a/asssets/tab_randomise_selected_unpressed.png b/asssets/tab_randomise_selected_unpressed.png new file mode 100644 index 0000000..6b9ef3c --- /dev/null +++ b/asssets/tab_randomise_selected_unpressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:305b98ab3ff193880ef7295320a859013aa38f034041e9a4a685b70c8b0a40bd +size 39267 diff --git a/asssets/tab_randomise_unselected_pressed.png b/asssets/tab_randomise_unselected_pressed.png new file mode 100644 index 0000000..8a5ae79 --- /dev/null +++ b/asssets/tab_randomise_unselected_pressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83a11fcca464393f1f6c9019f0d2f81028a0ba8ff4a4cb90fc49b40ce70dea76 +size 22492 diff --git a/asssets/tab_randomise_unselected_unpressed.png b/asssets/tab_randomise_unselected_unpressed.png new file mode 100644 index 0000000..6b9ef3c --- /dev/null +++ b/asssets/tab_randomise_unselected_unpressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:305b98ab3ff193880ef7295320a859013aa38f034041e9a4a685b70c8b0a40bd +size 39267