From 9726317928349d0b898b0c98f8c16af890ed3514 Mon Sep 17 00:00:00 2001 From: James H Ball Date: Sat, 1 Jun 2024 21:50:56 +0100 Subject: [PATCH] Several improvements to ImageParser, support for all image types, show only relevant controls in FrameSettingsComponent --- Source/FrameSettingsComponent.cpp | 78 ++++++++++++++++++---------- Source/FrameSettingsComponent.h | 7 ++- Source/MainComponent.cpp | 2 +- Source/PluginProcessor.cpp | 5 +- Source/PluginProcessor.h | 8 +-- Source/SettingsComponent.cpp | 21 ++++---- Source/SettingsComponent.h | 4 +- Source/img/ImageParser.cpp | 86 +++++++++++++++++++++++-------- Source/img/ImageParser.h | 5 +- Source/parser/FileParser.cpp | 6 +-- 10 files changed, 150 insertions(+), 72 deletions(-) diff --git a/Source/FrameSettingsComponent.cpp b/Source/FrameSettingsComponent.cpp index 01e2585..957ddaa 100644 --- a/Source/FrameSettingsComponent.cpp +++ b/Source/FrameSettingsComponent.cpp @@ -58,40 +58,47 @@ FrameSettingsComponent::FrameSettingsComponent(OscirenderAudioProcessor& p, Osci audioProcessor.invertImage->addListener(this); } -FrameSettingsComponent::~FrameSettingsComponent() { - audioProcessor.invertImage->removeListener(this); - audioProcessor.animationOffset->removeListener(this); - audioProcessor.animationRate->removeListener(this); - audioProcessor.animationSyncBPM->removeListener(this); - audioProcessor.animateFrames->removeListener(this); +FrameSettingsComponent::~FrameSettingsComponent() { + audioProcessor.invertImage->removeListener(this); + audioProcessor.animationOffset->removeListener(this); + audioProcessor.animationRate->removeListener(this); + audioProcessor.animationSyncBPM->removeListener(this); + audioProcessor.animateFrames->removeListener(this); } void FrameSettingsComponent::resized() { auto area = getLocalBounds().withTrimmedTop(20).reduced(20); - auto firstColumn = area.removeFromLeft(220); - double rowHeight = 20; - double rowSpace = 10; - auto animateBounds = firstColumn.removeFromTop(rowHeight); - animate.setBounds(animateBounds.removeFromLeft(100)); - sync.setBounds(animateBounds.removeFromLeft(100)); - firstColumn.removeFromTop(rowSpace); + double rowHeight = 20; + + if (animated) { + double rowSpace = 10; + auto firstColumn = area.removeFromLeft(220); + + auto animateBounds = firstColumn.removeFromTop(rowHeight); + animate.setBounds(animateBounds.removeFromLeft(100)); + sync.setBounds(animateBounds.removeFromLeft(100)); + firstColumn.removeFromTop(rowSpace); - animateBounds = firstColumn.removeFromTop(rowHeight); - rateLabel.setBounds(animateBounds.removeFromLeft(140)); - rateBox.setBounds(animateBounds.removeFromLeft(60)); - firstColumn.removeFromTop(rowSpace); + animateBounds = firstColumn.removeFromTop(rowHeight); + rateLabel.setBounds(animateBounds.removeFromLeft(140)); + rateBox.setBounds(animateBounds.removeFromLeft(60)); + firstColumn.removeFromTop(rowSpace); - animateBounds = firstColumn.removeFromTop(rowHeight); - offsetLabel.setBounds(animateBounds.removeFromLeft(140)); - offsetBox.setBounds(animateBounds.removeFromLeft(60)); + animateBounds = firstColumn.removeFromTop(rowHeight); + offsetLabel.setBounds(animateBounds.removeFromLeft(140)); + offsetBox.setBounds(animateBounds.removeFromLeft(60)); + } - auto secondColumn = area; - auto invertBounds = secondColumn.removeFromTop(rowHeight); - invertImage.setBounds(invertBounds.removeFromLeft(100)); - secondColumn.removeFromTop(rowSpace); - - threshold.setBounds(secondColumn.removeFromTop(rowHeight)); - stride.setBounds(secondColumn.removeFromTop(rowHeight)); + if (image) { + auto secondColumn = area; + auto invertBounds = secondColumn.removeFromTop(rowHeight); + invertImage.setBounds(invertBounds.removeFromLeft(100)); + secondColumn.removeFromTop(5); + + rowHeight = 30; + threshold.setBounds(secondColumn.removeFromTop(rowHeight)); + stride.setBounds(secondColumn.removeFromTop(rowHeight)); + } } void FrameSettingsComponent::update() { @@ -118,3 +125,20 @@ void FrameSettingsComponent::parameterGestureChanged(int parameterIndex, bool ge void FrameSettingsComponent::handleAsyncUpdate() { update(); } + +void FrameSettingsComponent::setAnimated(bool animated) { + this->animated = animated; + animate.setVisible(animated); + sync.setVisible(animated); + rateBox.setVisible(animated); + offsetBox.setVisible(animated); + rateLabel.setVisible(animated); + offsetLabel.setVisible(animated); +} + +void FrameSettingsComponent::setImage(bool image) { + this->image = image; + invertImage.setVisible(image); + threshold.setVisible(image); + stride.setVisible(image); +} diff --git a/Source/FrameSettingsComponent.h b/Source/FrameSettingsComponent.h index 97a52f7..02b68bf 100644 --- a/Source/FrameSettingsComponent.h +++ b/Source/FrameSettingsComponent.h @@ -16,9 +16,14 @@ public: void parameterGestureChanged(int parameterIndex, bool gestureIsStarting) override; void handleAsyncUpdate() override; void update(); + void setAnimated(bool animated); + void setImage(bool image); private: OscirenderAudioProcessor& audioProcessor; OscirenderAudioProcessorEditor& pluginEditor; + + bool animated = true; + bool image = true; juce::ToggleButton animate{"Animate"}; juce::ToggleButton sync{"BPM Sync"}; @@ -32,4 +37,4 @@ private: EffectComponent stride{ audioProcessor, *audioProcessor.imageStride }; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FrameSettingsComponent) -}; \ No newline at end of file +}; diff --git a/Source/MainComponent.cpp b/Source/MainComponent.cpp index 54da4bf..cb0d31c 100644 --- a/Source/MainComponent.cpp +++ b/Source/MainComponent.cpp @@ -10,7 +10,7 @@ MainComponent::MainComponent(OscirenderAudioProcessor& p, OscirenderAudioProcess fileButton.setButtonText("Choose File(s)"); fileButton.onClick = [this] { - chooser = std::make_unique("Open", juce::File::getSpecialLocation(juce::File::userHomeDirectory), "*.obj;*.svg;*.lua;*.txt;*.gpla;*.gif"); + chooser = std::make_unique("Open", juce::File::getSpecialLocation(juce::File::userHomeDirectory), "*.obj;*.svg;*.lua;*.txt;*.gpla;*.gif;*.png;*.jpg;*.jpeg"); auto flags = juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectMultipleItems | juce::FileBrowserComponent::canSelectFiles; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index f9bfd22..0c3f607 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -805,7 +805,8 @@ void OscirenderAudioProcessor::getStateInformation(juce::MemoryBlock& destData) for (int i = 0; i < fileBlocks.size(); i++) { auto fileXml = filesXml->createNewChildElement("file"); fileXml->setAttribute("name", fileNames[i]); - fileXml->addTextElement(fileBlocks[i]->toBase64Encoding()); + auto base64 = fileBlocks[i]->toBase64Encoding(); + fileXml->addTextElement(base64); } xml->setAttribute("currentFile", currentFile); @@ -911,7 +912,7 @@ void OscirenderAudioProcessor::setStateInformation(const void* data, int sizeInB if (lessThanVersion(version, "2.2.0")) { // Older versions of osci-render opened files in a silly way auto stream = juce::MemoryOutputStream(); - juce::Base64::convertFromBase64(stream, fileXml->getAllSubText()); + juce::Base64::convertFromBase64(stream, text); fileBlock = std::make_shared(stream.getData(), stream.getDataSize()); } else { fileBlock = std::make_shared(); diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index ae1a148..9bdec3c 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -306,20 +306,20 @@ private: double valueFromLegacy(double value, const juce::String& id); void changeSound(ShapeSound::Ptr sound); - void parseVersion(int result[4], const juce::String& input) { + void parseVersion(int result[3], const juce::String& input) { std::istringstream parser(input.toStdString()); parser >> result[0]; - for (int idx = 1; idx < 4; idx++) { + for (int idx = 1; idx < 3; idx++) { parser.get(); //Skip period parser >> result[idx]; } } bool lessThanVersion(const juce::String& a, const juce::String& b) { - int parsedA[4], parsedB[4]; + int parsedA[3], parsedB[3]; parseVersion(parsedA, a); parseVersion(parsedB, b); - return std::lexicographical_compare(parsedA, parsedA + 4, parsedB, parsedB + 4); + return std::lexicographical_compare(parsedA, parsedA + 3, parsedB, parsedB + 3); } const double MIN_LENGTH_INCREMENT = 0.000001; diff --git a/Source/SettingsComponent.cpp b/Source/SettingsComponent.cpp index 76c6c4d..a087c6f 100644 --- a/Source/SettingsComponent.cpp +++ b/Source/SettingsComponent.cpp @@ -9,7 +9,7 @@ SettingsComponent::SettingsComponent(OscirenderAudioProcessor& p, OscirenderAudi addAndMakeVisible(mainResizerBar); addAndMakeVisible(midi); addChildComponent(txt); - addChildComponent(gpla); + addChildComponent(frame); midiLayout.setItemLayout(0, -0.1, -1.0, -1.0); midiLayout.setItemLayout(1, pluginEditor.RESIZER_BAR_SIZE, pluginEditor.RESIZER_BAR_SIZE, pluginEditor.RESIZER_BAR_SIZE); @@ -47,14 +47,14 @@ void SettingsComponent::resized() { if (txt.isVisible()) { effectSettings = &txt; - } else if (gpla.isVisible()) { - effectSettings = &gpla; + } else if (frame.isVisible()) { + effectSettings = &frame; } auto dummyBounds = dummy.getBounds(); if (effectSettings != nullptr) { - effectSettings->setBounds(dummyBounds.removeFromBottom(140)); + effectSettings->setBounds(dummyBounds.removeFromBottom(150)); dummyBounds.removeFromBottom(pluginEditor.RESIZER_BAR_SIZE); } @@ -66,14 +66,17 @@ void SettingsComponent::resized() { void SettingsComponent::fileUpdated(juce::String fileName) { juce::String extension = fileName.fromLastOccurrenceOf(".", true, false); txt.setVisible(false); - gpla.setVisible(false); + frame.setVisible(false); + bool isImage = extension == ".gif" || extension == ".png" || extension == ".jpg" || extension == ".jpeg"; if (fileName.isEmpty() || audioProcessor.objectServerRendering) { // do nothing } else if (extension == ".txt") { txt.setVisible(true); - } - else if (extension == ".gpla" || extension == ".gif") { - gpla.setVisible(true); + } else if (extension == ".gpla" || isImage) { + frame.setVisible(true); + frame.setAnimated(extension == ".gpla" || extension == ".gif"); + frame.setImage(isImage); + frame.resized(); } main.updateFileLabel(); resized(); @@ -81,7 +84,7 @@ void SettingsComponent::fileUpdated(juce::String fileName) { void SettingsComponent::update() { txt.update(); - gpla.update(); + frame.update(); } void SettingsComponent::mouseMove(const juce::MouseEvent& event) { diff --git a/Source/SettingsComponent.h b/Source/SettingsComponent.h index 99cbe3c..cac464e 100644 --- a/Source/SettingsComponent.h +++ b/Source/SettingsComponent.h @@ -28,7 +28,7 @@ private: MainComponent main{audioProcessor, pluginEditor}; PerspectiveComponent perspective{audioProcessor, pluginEditor}; TxtComponent txt{audioProcessor, pluginEditor}; - FrameSettingsComponent gpla{ audioProcessor, pluginEditor }; + FrameSettingsComponent frame{ audioProcessor, pluginEditor }; EffectsComponent effects{audioProcessor, pluginEditor}; MidiComponent midi{audioProcessor, pluginEditor}; @@ -42,4 +42,4 @@ private: double prefSizes[1] = { 300 }; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SettingsComponent) -}; \ No newline at end of file +}; diff --git a/Source/img/ImageParser.cpp b/Source/img/ImageParser.cpp index b5dc9a7..dc86fba 100644 --- a/Source/img/ImageParser.cpp +++ b/Source/img/ImageParser.cpp @@ -3,19 +3,19 @@ #include "../PluginProcessor.h" ImageParser::ImageParser(OscirenderAudioProcessor& p, juce::String extension, juce::MemoryBlock image) : audioProcessor(p) { - if (extension.equalsIgnoreCase(".gif")) { - juce::TemporaryFile temp{".gif"}; - juce::File file = temp.getFile(); + juce::TemporaryFile temp{".gif"}; + juce::File file = temp.getFile(); - { - juce::FileOutputStream output(file); + { + juce::FileOutputStream output(file); - if (output.openedOk()) { - output.write(image.getData(), image.getSize()); - output.flush(); - } + if (output.openedOk()) { + output.write(image.getData(), image.getSize()); + output.flush(); } - + } + + if (extension.equalsIgnoreCase(".gif")) { juce::String fileName = file.getFullPathName(); gd_GIF *gif = gd_open_gif(fileName.toRawUTF8()); @@ -43,6 +43,25 @@ ImageParser::ImageParser(OscirenderAudioProcessor& p, juce::String extension, ju gd_close_gif(gif); } + } else { + juce::Image image = juce::ImageFileFormat::loadFrom(file); + image.desaturate(); + + width = image.getWidth(); + height = image.getHeight(); + int frameSize = width * height; + + visited = std::vector(frameSize, false); + frames.emplace_back(std::vector(frameSize)); + + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + juce::Colour pixel = image.getPixelAt(x, y); + int index = y * width + x; + // RGB should be equal since we have desaturated + frames[0][index] = pixel.getRed(); + } + } } setFrame(0); @@ -58,9 +77,29 @@ void ImageParser::setFrame(int index) { std::fill(visited.begin(), visited.end(), false); } +bool ImageParser::isOverThreshold(double pixel, double thresholdPow) { + float threshold = std::pow(pixel, thresholdPow); + return pixel > 0.2 && rng.nextFloat() < threshold; +} + void ImageParser::resetPosition() { - currentX = rng.nextInt(width); - currentY = rng.nextInt(height); + currentX = width > 0 ? rng.nextInt(width) : 0; + currentY = height > 0 ? rng.nextInt(height) : 0; +} + +float ImageParser::getPixelValue(int x, int y) { + int index = (height - y - 1) * width + x; + float pixel = frames[frameIndex][index] / (float) std::numeric_limits::max(); + return pixel; +} + +void ImageParser::findWhite(double thresholdPow) { + for (int i = 0; i < 100; i++) { + resetPosition(); + if (isOverThreshold(getPixelValue(currentX, currentY), thresholdPow)) { + break; + } + } } int ImageParser::jumpFrequency() { @@ -74,8 +113,6 @@ void ImageParser::findNearestNeighbour(int searchRadius, float thresholdPow, int int y = currentY; int dir = rng.nextInt(4); - float maxValue = std::numeric_limits::max(); - for (int len = 1; len <= maxSteps; len++) { // Length of spiral arm for (int i = 0 ; i < 2; i++) { // Repeat twice for each arm length for (int step = 0 ; step < len; step++) { // Steps in the current direction @@ -83,15 +120,14 @@ void ImageParser::findNearestNeighbour(int searchRadius, float thresholdPow, int y += stride * spiralSteps[dir][1]; if (x < 0 || x >= width || y < 0 || y >= height) break; - - int index = (height - y - 1) * width + x; - float pixel = frames[frameIndex][index] / maxValue; + + float pixel = getPixelValue(x, y); if (invert) { pixel = 1 - pixel; } - float threshold = std::pow(pixel, thresholdPow); - if (pixel > 0.2 && rng.nextFloat() < threshold && !visited[index]) { + int index = (height - y - 1) * width + x; + if (isOverThreshold(pixel, thresholdPow) && !visited[index]) { visited[index] = true; currentX = x; currentY = y; @@ -103,18 +139,24 @@ void ImageParser::findNearestNeighbour(int searchRadius, float thresholdPow, int } } - resetPosition(); + findWhite(thresholdPow); } Point ImageParser::getSample() { if (count % jumpFrequency() == 0) { resetPosition(); } + + if (count % 10 * jumpFrequency() == 0) { + std::fill(visited.begin(), visited.end(), false); + } float thresholdPow = audioProcessor.imageThreshold->getValue() * 10 + 1; - findNearestNeighbour(50, thresholdPow, audioProcessor.imageStride->getValue(), audioProcessor.invertImage->getValue()); + findNearestNeighbour(10, thresholdPow, audioProcessor.imageStride->getValue(), audioProcessor.invertImage->getValue()); float maxDim = juce::jmax(width, height); count++; - return Point(2 * currentX / maxDim - 1, 2 * currentY / maxDim - 1); + float widthDiff = (maxDim - width) / 2; + float heightDiff = (maxDim - height) / 2; + return Point(2 * (currentX + widthDiff) / maxDim - 1, 2 * (currentY + heightDiff) / maxDim - 1); } diff --git a/Source/img/ImageParser.h b/Source/img/ImageParser.h index cf5d7f1..c1a1824 100644 --- a/Source/img/ImageParser.h +++ b/Source/img/ImageParser.h @@ -17,6 +17,9 @@ public: private: void findNearestNeighbour(int searchRadius, float thresholdPow, int stride, bool invert); void resetPosition(); + float getPixelValue(int x, int y); + void findWhite(double thresholdPow); + bool isOverThreshold(double pixel, double thresholdValue); int jumpFrequency(); OscirenderAudioProcessor& audioProcessor; @@ -27,4 +30,4 @@ private: int currentX, currentY; int width, height; int count = 0; -}; \ No newline at end of file +}; diff --git a/Source/parser/FileParser.cpp b/Source/parser/FileParser.cpp index 3600bcc..6d8e7e8 100644 --- a/Source/parser/FileParser.cpp +++ b/Source/parser/FileParser.cpp @@ -29,13 +29,13 @@ void FileParser::parse(juce::String fileId, juce::String extension, std::unique_ lua = std::make_shared(fileId, stream->readEntireStreamAsString(), errorCallback, fallbackLuaScript); } else if (extension == ".gpla") { gpla = std::make_shared(stream->readEntireStreamAsString()); - } else if (extension == ".gif") { + } else if (extension == ".gif" || extension == ".png" || extension == ".jpg" || extension == ".jpeg") { juce::MemoryBlock buffer{}; int bytesRead = stream->readIntoMemoryBlock(buffer); img = std::make_shared(audioProcessor, extension, buffer); } - isAnimatable = gpla != nullptr || img != nullptr; + isAnimatable = gpla != nullptr || (img != nullptr && extension == ".gif"); sampleSource = lua != nullptr || img != nullptr; } @@ -121,4 +121,4 @@ std::shared_ptr FileParser::getLua() { std::shared_ptr FileParser::getImg() { return img; -} \ No newline at end of file +}