diff --git a/Source/LineArtComponent.cpp b/Source/LineArtComponent.cpp new file mode 100644 index 0000000..74aa067 --- /dev/null +++ b/Source/LineArtComponent.cpp @@ -0,0 +1,45 @@ +#include "LineArtComponent.h" +#include "PluginEditor.h" + +LineArtComponent::LineArtComponent(OscirenderAudioProcessor& p, OscirenderAudioProcessorEditor& editor) : audioProcessor(p), pluginEditor(editor) { + setText("Line Art Settings"); + + + addAndMakeVisible(animate); + addAndMakeVisible(sync); + addAndMakeVisible(rate); + rate.setText("10", false); + rate.setInputRestrictions(6, "0123456789."); + + update(); + + auto updateAnimation = [this]() { + audioProcessor.animateLineArt = animate.getToggleState(); + audioProcessor.syncMIDIAnimation = sync.getToggleState(); + try { + audioProcessor.animationRate = std::stof(rate.getText().toStdString()); + } + catch (std::exception e) { + audioProcessor.animationRate = 10.f; + } + audioProcessor.openFile(audioProcessor.currentFile); + }; + + animate.onClick = updateAnimation; + sync.onClick = updateAnimation; + rate.onFocusLost = updateAnimation; +} + +void LineArtComponent::resized() { + auto area = getLocalBounds().withTrimmedTop(20).reduced(20); + double rowHeight = 30; + animate.setBounds(area.removeFromTop(rowHeight)); + sync.setBounds(area.removeFromTop(rowHeight)); + rate.setBounds(area.removeFromTop(rowHeight)); +} + +void LineArtComponent::update() { + rate.setText(juce::String(audioProcessor.animationRate)); + animate.setToggleState(audioProcessor.animateLineArt, false); + sync.setToggleState(audioProcessor.syncMIDIAnimation, false); +} diff --git a/Source/LineArtComponent.h b/Source/LineArtComponent.h new file mode 100644 index 0000000..f03be5a --- /dev/null +++ b/Source/LineArtComponent.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include "PluginProcessor.h" + +class OscirenderAudioProcessorEditor; +class LineArtComponent : public juce::GroupComponent, public juce::MouseListener { +public: + LineArtComponent(OscirenderAudioProcessor&, OscirenderAudioProcessorEditor&); + + void resized() override; + void update(); +private: + OscirenderAudioProcessor& audioProcessor; + OscirenderAudioProcessorEditor& pluginEditor; + + juce::ToggleButton animate{"Animate"}; + juce::ToggleButton sync{"MIDI Sync"}; + juce::TextEditor rate{"Framerate"}; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LineArtComponent) +}; \ No newline at end of file diff --git a/Source/MainComponent.cpp b/Source/MainComponent.cpp index cb24ddf..415d342 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"); + chooser = std::make_unique("Open", juce::File::getSpecialLocation(juce::File::userHomeDirectory), "*.obj;*.svg;*.lua;*.txt;*.gpla"); auto flags = juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectMultipleItems | juce::FileBrowserComponent::canSelectFiles; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 280bebc..39d410a 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -545,8 +545,34 @@ void OscirenderAudioProcessor::setObjectServerRendering(bool enabled) { void OscirenderAudioProcessor::processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) { juce::ScopedNoDenormals noDenormals; - auto totalNumInputChannels = getTotalNumInputChannels(); - auto totalNumOutputChannels = getTotalNumOutputChannels(); + // Audio info variables + int totalNumInputChannels = getTotalNumInputChannels(); + int totalNumOutputChannels = getTotalNumOutputChannels(); + double sampleRate = getSampleRate(); + + // MIDI transport info variables (defaults to 120bpm, 4/4 time signature at zero seconds and not playing) + double bpm = 120; + double playTimeSeconds = 0; + bool isPlaying = false; + int timeSigNum = 4; + int timeSigDen = 4; + + // Get MIDI transport info + playHead = this->getPlayHead(); + if (playHead->getCurrentPosition(currentPositionInfo)) { + bpm = currentPositionInfo.bpm; + playTimeSeconds = currentPositionInfo.timeInSeconds; + isPlaying = currentPositionInfo.isPlaying; + timeSigNum = currentPositionInfo.timeSigNumerator; + timeSigDen = currentPositionInfo.timeSigDenominator; + } + + // Calculated number of beats + double playTimeBeats = bpm * playTimeSeconds / 60; + + // Calculated time per sample in seconds and beats + double sTimeSec = 1.f / sampleRate; + double sTimeBeats = bpm * sTimeSec / 60; // merge keyboard state and midi messages keyboardState.processNextMidiBuffer(midiMessages, 0, buffer.getNumSamples(), true); @@ -610,8 +636,20 @@ void OscirenderAudioProcessor::processBlock(juce::AudioBuffer& buffer, ju midiMessages.clear(); auto* channelData = buffer.getArrayOfWritePointers(); + + // Update line art animation + if (animateLineArt) { + if (syncMIDIAnimation) animationTime = playTimeBeats; + else animationTime = playTimeSeconds; + if ((currentFile >= 0) ? (sounds[currentFile]->parser->isAnimatable) : false) { + int animFrame = (int)(animationTime * animationRate); + sounds[currentFile]->parser->getLineArt()->setFrame(animFrame); + } + } + for (auto sample = 0; sample < buffer.getNumSamples(); ++sample) { + auto left = 0.0; auto right = 0.0; if (totalNumInputChannels >= 2) { @@ -679,6 +717,11 @@ void OscirenderAudioProcessor::processBlock(juce::AudioBuffer& buffer, ju consumer->notifyIfFull(); } } + + if (isPlaying) { + playTimeSeconds += sTimeSec; + playTimeBeats += sTimeBeats; + } } // used for any callback that must guarantee all audio is recieved (e.g. when recording to a file) diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index 2419f33..4adae9b 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -198,6 +198,11 @@ public: IntParameter* voices = new IntParameter("Voices", "voices", VERSION_HINT, 4, 1, 16); + bool animateLineArt = false; + bool syncMIDIAnimation = false; + float animationRate = 10.f; + double animationTime = 0.f; + private: juce::SpinLock consumerLock; std::vector> consumers; @@ -279,6 +284,9 @@ private: const double MIN_LENGTH_INCREMENT = 0.000001; + juce::AudioPlayHead* playHead; + juce::AudioPlayHead::CurrentPositionInfo currentPositionInfo; + //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OscirenderAudioProcessor) }; diff --git a/Source/SettingsComponent.cpp b/Source/SettingsComponent.cpp index dc51b70..cf42aab 100644 --- a/Source/SettingsComponent.cpp +++ b/Source/SettingsComponent.cpp @@ -9,6 +9,7 @@ SettingsComponent::SettingsComponent(OscirenderAudioProcessor& p, OscirenderAudi addAndMakeVisible(mainResizerBar); addAndMakeVisible(midi); addChildComponent(txt); + addChildComponent(gpla); midiLayout.setItemLayout(0, -0.1, -1.0, -1.0); midiLayout.setItemLayout(1, pluginEditor.RESIZER_BAR_SIZE, pluginEditor.RESIZER_BAR_SIZE, pluginEditor.RESIZER_BAR_SIZE); @@ -46,6 +47,8 @@ void SettingsComponent::resized() { if (txt.isVisible()) { effectSettings = &txt; + } else if (gpla.isVisible()) { + effectSettings = &gpla; } auto dummyBounds = dummy.getBounds(); @@ -63,17 +66,22 @@ void SettingsComponent::resized() { void SettingsComponent::fileUpdated(juce::String fileName) { juce::String extension = fileName.fromLastOccurrenceOf(".", true, false); txt.setVisible(false); + gpla.setVisible(false); if (fileName.isEmpty() || audioProcessor.objectServerRendering) { // do nothing - } if (extension == ".txt") { + } else if (extension == ".txt") { txt.setVisible(true); } + else if (extension == ".gpla") { + gpla.setVisible(true); + } main.updateFileLabel(); resized(); } void SettingsComponent::update() { txt.update(); + gpla.update(); } void SettingsComponent::mouseMove(const juce::MouseEvent& event) { diff --git a/Source/SettingsComponent.h b/Source/SettingsComponent.h index 08ef5dc..9e84a1d 100644 --- a/Source/SettingsComponent.h +++ b/Source/SettingsComponent.h @@ -3,6 +3,7 @@ #include #include "PluginProcessor.h" #include "MainComponent.h" +#include "LineArtComponent.h" #include "LuaComponent.h" #include "PerspectiveComponent.h" #include "TxtComponent.h" @@ -27,6 +28,7 @@ private: MainComponent main{audioProcessor, pluginEditor}; PerspectiveComponent perspective{audioProcessor, pluginEditor}; TxtComponent txt{audioProcessor, pluginEditor}; + LineArtComponent gpla{ audioProcessor, pluginEditor }; EffectsComponent effects{audioProcessor, pluginEditor}; MidiComponent midi{audioProcessor, pluginEditor}; diff --git a/Source/gpla/LineArtParser.cpp b/Source/gpla/LineArtParser.cpp new file mode 100644 index 0000000..d8aea5d --- /dev/null +++ b/Source/gpla/LineArtParser.cpp @@ -0,0 +1,166 @@ +#include "LineArtParser.h" + + +LineArtParser::LineArtParser(juce::String json) { + parseJsonFrames(json); +} + +LineArtParser::~LineArtParser() { + frames.clear(); +} + +void LineArtParser::parseJsonFrames(juce::String jsonStr) { + frames.clear(); + numFrames = 0; + + // format of json is: + // { + // "frames":[ + // "objects": [ + // { + // "name": "Line Art", + // "vertices": [ + // [ + // { + // "x": double value, + // "y": double value, + // "z": double value + // }, + // ... + // ], + // ... + // ], + // "matrix": [ + // 16 double values + // ] + // } + // ], + // "focalLength": double value + // }, + // ... + // ] + // } + + auto json = juce::JSON::parse(jsonStr); + + auto jsonFrames = *json.getProperty("frames", juce::Array()).getArray(); + numFrames = jsonFrames.size(); + + for (int f = 0; f < numFrames; f++) { + auto objects = *jsonFrames[f].getProperty("objects", juce::Array()).getArray(); + std::vector> allMatrices; + std::vector>> allVertices; + + double focalLength = jsonFrames[f].getProperty("focalLength", 1); + + for (int i = 0; i < objects.size(); i++) { + auto verticesArray = *objects[i].getProperty("vertices", juce::Array()).getArray(); + std::vector> vertices; + + for (auto& vertexArrayVar : verticesArray) { + vertices.push_back(std::vector()); + auto& vertexArray = *vertexArrayVar.getArray(); + for (auto& vertex : vertexArray) { + double x = vertex.getProperty("x", 0); + double y = vertex.getProperty("y", 0); + double z = vertex.getProperty("z", 0); + vertices[vertices.size() - 1].push_back(Point(x, y, z)); + } + } + auto matrix = *objects[i].getProperty("matrix", juce::Array()).getArray(); + + allMatrices.push_back(std::vector()); + for (auto& value : matrix) { + allMatrices[i].push_back(value); + } + + std::vector> reorderedVertices; + + if (vertices.size() > 0 && matrix.size() == 16) { + std::vector visited = std::vector(vertices.size(), false); + std::vector order = std::vector(vertices.size(), 0); + visited[0] = true; + + auto endPoint = vertices[0].back(); + + for (int i = 1; i < vertices.size(); i++) { + int minPath = 0; + double minDistance = 9999999; + for (int j = 0; j < vertices.size(); j++) { + if (!visited[j]) { + auto startPoint = vertices[j][0]; + + double diffX = endPoint.x - startPoint.x; + double diffY = endPoint.y - startPoint.y; + double diffZ = endPoint.z - startPoint.z; + + double distance = std::sqrt(diffX * diffX + diffY * diffY + diffZ * diffZ); + if (distance < minDistance) { + minPath = j; + minDistance = distance; + } + } + } + visited[minPath] = true; + order[i] = minPath; + endPoint = vertices[minPath].back(); + } + + for (int i = 0; i < vertices.size(); i++) { + std::vector reorderedVertex; + int index = order[i]; + for (int j = 0; j < vertices[index].size(); j++) { + reorderedVertex.push_back(vertices[index][j]); + } + reorderedVertices.push_back(reorderedVertex); + } + } + + allVertices.push_back(reorderedVertices); + } + + // generate a frame from the vertices and matrix + std::vector frame; + + for (int i = 0; i < objects.size(); i++) { + for (int j = 0; j < allVertices[i].size(); j++) { + for (int k = 0; k < allVertices[i][j].size() - 1; k++) { + auto start = allVertices[i][j][k]; + auto end = allVertices[i][j][k + 1]; + + // multiply the start and end points by the matrix + double rotatedX = start.x * allMatrices[i][0] + start.y * allMatrices[i][1] + start.z * allMatrices[i][2] + allMatrices[i][3]; + double rotatedY = start.x * allMatrices[i][4] + start.y * allMatrices[i][5] + start.z * allMatrices[i][6] + allMatrices[i][7]; + double rotatedZ = start.x * allMatrices[i][8] + start.y * allMatrices[i][9] + start.z * allMatrices[i][10] + allMatrices[i][11]; + + double rotatedX2 = end.x * allMatrices[i][0] + end.y * allMatrices[i][1] + end.z * allMatrices[i][2] + allMatrices[i][3]; + double rotatedY2 = end.x * allMatrices[i][4] + end.y * allMatrices[i][5] + end.z * allMatrices[i][6] + allMatrices[i][7]; + double rotatedZ2 = end.x * allMatrices[i][8] + end.y * allMatrices[i][9] + end.z * allMatrices[i][10] + allMatrices[i][11]; + + double x = rotatedX * focalLength / rotatedZ; + double y = rotatedY * focalLength / rotatedZ; + + double x2 = rotatedX2 * focalLength / rotatedZ2; + double y2 = rotatedY2 * focalLength / rotatedZ2; + + frame.push_back(Line(x, y, x2, y2)); + } + } + } + + frames.push_back(frame); + } +} + +void LineArtParser::setFrame(int fNum) { + frameNumber = fNum % numFrames; +} + +std::vector> LineArtParser::draw() { + std::vector> tempShapes; + + for (Line shape : frames[frameNumber]) { + tempShapes.push_back(shape.clone()); + } + return tempShapes; +} diff --git a/Source/gpla/LineArtParser.h b/Source/gpla/LineArtParser.h new file mode 100644 index 0000000..e47f426 --- /dev/null +++ b/Source/gpla/LineArtParser.h @@ -0,0 +1,21 @@ +#pragma once +#include "../shape/Point.h" +#include +#include "../shape/Shape.h" +#include "../svg/SvgParser.h" +#include "../shape/Line.h" + +class LineArtParser { +public: + LineArtParser(juce::String json); + ~LineArtParser(); + + void setFrame(int fNum); + std::vector> draw(); +private: + void parseJsonFrames(juce::String jsonStr); + int frameNumber = 0; + std::vector> frames; + int numFrames = 0; + +}; \ No newline at end of file diff --git a/Source/parser/FileParser.cpp b/Source/parser/FileParser.cpp index e9d1bcb..b259dab 100644 --- a/Source/parser/FileParser.cpp +++ b/Source/parser/FileParser.cpp @@ -15,7 +15,9 @@ void FileParser::parse(juce::String fileName, juce::String extension, std::uniqu object = nullptr; svg = nullptr; text = nullptr; + gpla = nullptr; lua = nullptr; + isAnimatable = false; if (extension == ".obj") { object = std::make_shared(stream->readEntireStreamAsString().toStdString()); @@ -25,6 +27,9 @@ void FileParser::parse(juce::String fileName, juce::String extension, std::uniqu text = std::make_shared(stream->readEntireStreamAsString(), font); } else if (extension == ".lua") { lua = std::make_shared(fileName, stream->readEntireStreamAsString(), errorCallback, fallbackLuaScript); + } else if (extension == ".gpla") { + isAnimatable = true; + gpla = std::make_shared(stream->readEntireStreamAsString()); } sampleSource = lua != nullptr; @@ -39,6 +44,8 @@ std::vector> FileParser::nextFrame() { return svg->draw(); } else if (text != nullptr) { return text->draw(); + } else if (gpla != nullptr) { + return gpla->draw(); } auto tempShapes = std::vector>(); // return a square @@ -98,6 +105,10 @@ std::shared_ptr FileParser::getText() { return text; } +std::shared_ptr FileParser::getLineArt() { + return gpla; +} + std::shared_ptr FileParser::getLua() { return lua; } diff --git a/Source/parser/FileParser.h b/Source/parser/FileParser.h index e88b28b..07ada8e 100644 --- a/Source/parser/FileParser.h +++ b/Source/parser/FileParser.h @@ -5,13 +5,14 @@ #include "../obj/WorldObject.h" #include "../svg/SvgParser.h" #include "../txt/TextParser.h" +#include "../gpla/LineArtParser.h" #include "../lua/LuaParser.h" class FileParser { public: FileParser(std::function errorCallback = nullptr); - void parse(juce::String fileName, juce::String extension, std::unique_ptr, juce::Font); + void parse(juce::String fileName, juce::String extension, std::unique_ptr stream, juce::Font font); std::vector> nextFrame(); Point nextSample(lua_State*& L, LuaVariables& vars); void closeLua(lua_State*& L); @@ -23,8 +24,11 @@ public: std::shared_ptr getObject(); std::shared_ptr getSvg(); std::shared_ptr getText(); + std::shared_ptr getLineArt(); std::shared_ptr getLua(); + bool isAnimatable = false; + private: bool active = true; bool sampleSource = false; @@ -34,6 +38,7 @@ private: std::shared_ptr object; std::shared_ptr svg; std::shared_ptr text; + std::shared_ptr gpla; std::shared_ptr lua; juce::String fallbackLuaScript = "return { 0.0, 0.0 }"; diff --git a/osci-render.jucer b/osci-render.jucer index 8d120b7..bb8d7ce 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -1,7 +1,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -254,10 +162,11 @@ - - + + + + @@ -356,6 +265,8 @@ + @@ -384,8 +295,6 @@ file="Source/ixwebsocket/IXWebSocketMessage.h"/> - - - - @@ -490,16 +395,98 @@ - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -515,10 +502,6 @@ - - @@ -528,17 +511,6 @@ - - - - - - @@ -565,9 +537,6 @@ - - @@ -582,6 +551,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + - +