Add further support and add a timeline for animatable files

pull/300/head
James H Ball 2025-04-22 19:13:41 +01:00
rodzic 7e43db6c66
commit 1b4b826763
13 zmienionych plików z 316 dodań i 73 usunięć

Wyświetl plik

@ -4,23 +4,28 @@
FrameSettingsComponent::FrameSettingsComponent(OscirenderAudioProcessor& p, OscirenderAudioProcessorEditor& editor) : audioProcessor(p), pluginEditor(editor) {
setText("Frame Settings");
addAndMakeVisible(animate);
addAndMakeVisible(sync);
if (!juce::JUCEApplicationBase::isStandaloneApp()) {
addAndMakeVisible(animate);
addAndMakeVisible(sync);
addAndMakeVisible(offsetLabel);
addAndMakeVisible(offsetBox);
offsetLabel.setText("Start Frame", juce::dontSendNotification);
offsetBox.setJustification(juce::Justification::left);
offsetLabel.setTooltip("Offsets the animation's start point by a specified number of frames.");
} else {
audioProcessor.animationSyncBPM->setValueNotifyingHost(false);
addAndMakeVisible(timeline);
}
addAndMakeVisible(rateLabel);
addAndMakeVisible(rateBox);
addAndMakeVisible(offsetLabel);
addAndMakeVisible(offsetBox);
addAndMakeVisible(invertImage);
addAndMakeVisible(threshold);
addAndMakeVisible(stride);
offsetLabel.setTooltip("Offsets the animation's start point by a specified number of frames.");
rateLabel.setText("Frames per Second", juce::dontSendNotification);
rateBox.setJustification(juce::Justification::left);
offsetLabel.setText("Start Frame", juce::dontSendNotification);
offsetBox.setJustification(juce::Justification::left);
update();
@ -39,12 +44,14 @@ FrameSettingsComponent::FrameSettingsComponent(OscirenderAudioProcessor& p, Osci
stride.slider.onValueChange = [this]() {
audioProcessor.imageStride->setValue(stride.slider.getValue());
};
audioProcessor.animationRate->addListener(this);
audioProcessor.animationOffset->addListener(this);
audioProcessor.animationSyncBPM->addListener(this);
}
FrameSettingsComponent::~FrameSettingsComponent() {
audioProcessor.animationSyncBPM->removeListener(this);
audioProcessor.animationOffset->removeListener(this);
audioProcessor.animationRate->removeListener(this);
}
@ -52,16 +59,23 @@ FrameSettingsComponent::~FrameSettingsComponent() {
void FrameSettingsComponent::resized() {
auto area = getLocalBounds().withTrimmedTop(20).reduced(20);
double rowHeight = 20;
auto timelineArea = juce::JUCEApplicationBase::isStandaloneApp() ? area.removeFromBottom(30) : juce::Rectangle<int>();
auto toggleBounds = area.removeFromTop(rowHeight);
auto toggleBounds = juce::JUCEApplicationBase::isStandaloneApp() ? juce::Rectangle<int>() : area.removeFromTop(rowHeight);
auto toggleWidth = juce::jmin(area.getWidth() / 3, 150);
auto firstColumn = area.removeFromLeft(220);
if (animated) {
animate.setBounds(toggleBounds.removeFromLeft(toggleWidth));
sync.setBounds(toggleBounds.removeFromLeft(toggleWidth));
if (juce::JUCEApplicationBase::isStandaloneApp()) {
timeline.setBounds(timelineArea);
} else {
animate.setBounds(toggleBounds.removeFromLeft(toggleWidth));
sync.setBounds(toggleBounds.removeFromLeft(toggleWidth));
}
double rowSpace = 10;
auto firstColumn = area.removeFromLeft(220);
firstColumn.removeFromTop(rowSpace);
@ -70,13 +84,19 @@ void FrameSettingsComponent::resized() {
rateBox.setBounds(animateBounds.removeFromLeft(60));
firstColumn.removeFromTop(rowSpace);
animateBounds = firstColumn.removeFromTop(rowHeight);
offsetLabel.setBounds(animateBounds.removeFromLeft(140));
offsetBox.setBounds(animateBounds.removeFromLeft(60));
if (!juce::JUCEApplicationBase::isStandaloneApp()) {
animateBounds = firstColumn.removeFromTop(rowHeight);
offsetLabel.setBounds(animateBounds.removeFromLeft(140));
offsetBox.setBounds(animateBounds.removeFromLeft(60));
}
}
if (image) {
invertImage.setBounds(toggleBounds.removeFromLeft(toggleWidth));
if (juce::JUCEApplicationBase::isStandaloneApp()) {
invertImage.setBounds(firstColumn.removeFromTop(rowHeight));
} else {
invertImage.setBounds(toggleBounds.removeFromLeft(toggleWidth));
}
auto secondColumn = area;
secondColumn.removeFromTop(5);

Wyświetl plik

@ -5,6 +5,7 @@
#include "components/DoubleTextBox.h"
#include "components/EffectComponent.h"
#include "components/SwitchButton.h"
#include "components/AnimationTimelineComponent.h"
class OscirenderAudioProcessorEditor;
class FrameSettingsComponent : public juce::GroupComponent, public juce::AudioProcessorParameter::Listener, juce::AsyncUpdater {
@ -32,6 +33,7 @@ private:
juce::Label offsetLabel{ "Offset","Offset" };
DoubleTextBox rateBox{ audioProcessor.animationRate->min, audioProcessor.animationRate->max };
DoubleTextBox offsetBox{ audioProcessor.animationOffset->min, audioProcessor.animationRate->max };
AnimationTimelineComponent timeline{audioProcessor};
jux::SwitchButton invertImage{audioProcessor.invertImage};
EffectComponent threshold{*audioProcessor.imageThreshold};

Wyświetl plik

@ -155,6 +155,7 @@ OscirenderAudioProcessor::OscirenderAudioProcessor() : CommonAudioProcessor(Buse
booleanParameters.push_back(midiEnabled);
booleanParameters.push_back(inputEnabled);
booleanParameters.push_back(animateFrames);
booleanParameters.push_back(loopAnimation);
booleanParameters.push_back(animationSyncBPM);
booleanParameters.push_back(invertImage);
@ -537,26 +538,26 @@ void OscirenderAudioProcessor::processBlock(juce::AudioBuffer<float>& buffer, ju
for (int sample = 0; sample < buffer.getNumSamples(); ++sample) {
// Update frame animation
if (animateFrames->getValue()) {
if (animationSyncBPM->getValue()) {
animationTime = playTimeBeats;
if (animateFrames->getBoolValue()) {
if (juce::JUCEApplicationBase::isStandaloneApp()) {
animationFrame = animationFrame + sTimeSec * animationRate->getValueUnnormalised();
} else if (animationSyncBPM->getValue()) {
animationFrame = playTimeBeats * animationRate->getValueUnnormalised() + animationOffset->getValueUnnormalised();
} else {
animationTime = playTimeSeconds;
animationFrame = playTimeSeconds * animationRate->getValueUnnormalised() + animationOffset->getValueUnnormalised();
}
juce::SpinLock::ScopedLockType lock1(parsersLock);
juce::SpinLock::ScopedLockType lock2(effectsLock);
if (currentFile >= 0 && sounds[currentFile]->parser->isAnimatable) {
int animFrame = (int)(animationTime * animationRate->getValueUnnormalised() + animationOffset->getValueUnnormalised());
auto lineArt = sounds[currentFile]->parser->getLineArt();
auto img = sounds[currentFile]->parser->getImg();
if (lineArt != nullptr) {
lineArt->setFrame(animFrame);
} else if (img != nullptr) {
img->setFrame(animFrame);
int totalFrames = sounds[currentFile]->parser->getNumFrames();
if (loopAnimation->getBoolValue()) {
animationFrame = std::fmod(animationFrame, totalFrames);
} else {
animationFrame = juce::jlimit(0.0, (double) totalFrames - 1, animationFrame.load());
}
sounds[currentFile]->parser->setFrame(animationFrame);
}
}

Wyświetl plik

@ -148,6 +148,7 @@ public:
IntParameter* voices = new IntParameter("Voices", "voices", VERSION_HINT, 4, 1, 16);
BooleanParameter* animateFrames = new BooleanParameter("Animate", "animateFrames", VERSION_HINT, true, "Enables animation for files that have multiple frames, such as GIFs or Line Art.");
BooleanParameter* loopAnimation = new BooleanParameter("Loop Animation", "loopAnimation", VERSION_HINT, true, "Loops the animation. If disabled, the animation will stop at the last frame.");
BooleanParameter* animationSyncBPM = new BooleanParameter("Sync To BPM", "animationSyncBPM", VERSION_HINT, false, "Synchronises the animation's framerate with the BPM of your DAW.");
FloatParameter* animationRate = new FloatParameter("Animation Rate", "animationRate", VERSION_HINT, 30, -1000, 1000);
FloatParameter* animationOffset = new FloatParameter("Animation Offset", "animationOffset", VERSION_HINT, 0, -10000, 10000);
@ -174,7 +175,7 @@ public:
)
);
double animationTime = 0.f;
std::atomic<double> animationFrame = 0.f;
std::shared_ptr<WobbleEffect> wobbleEffect = std::make_shared<WobbleEffect>(*this);

Wyświetl plik

@ -30,6 +30,10 @@ void SettingsComponent::resized() {
area.removeFromRight(5);
area.removeFromTop(5);
area.removeFromBottom(5);
if (area.getWidth() <= 0 || area.getHeight() <= 0) {
return;
}
juce::Component dummy;
juce::Component dummy2;
@ -57,7 +61,7 @@ void SettingsComponent::resized() {
auto dummyBounds = dummy.getBounds();
if (effectSettings != nullptr) {
effectSettings->setBounds(dummyBounds.removeFromBottom(150));
effectSettings->setBounds(dummyBounds.removeFromBottom(160));
dummyBounds.removeFromBottom(pluginEditor.RESIZER_BAR_SIZE);
}

Wyświetl plik

@ -0,0 +1,133 @@
#include "AnimationTimelineComponent.h"
#include "../PluginProcessor.h"
AnimationTimelineComponent::AnimationTimelineComponent(OscirenderAudioProcessor& processor)
: audioProcessor(processor)
{
setOpaque(false);
addAndMakeVisible(slider);
slider.setSliderStyle(juce::Slider::SliderStyle::LinearHorizontal);
slider.setTextBoxStyle(juce::Slider::NoTextBox, true, 0, 0);
slider.setOpaque(false);
slider.setRange(0, 1, 0.001);
slider.setColour(juce::Slider::ColourIds::thumbColourId, juce::Colours::black);
slider.onValueChange = [this]() {
juce::SpinLock::ScopedLockType sl(audioProcessor.parsersLock);
int currentFileIndex = audioProcessor.getCurrentFileIndex();
if (currentFileIndex < 0) return;
auto parser = audioProcessor.parsers[currentFileIndex];
if (parser != nullptr) {
audioProcessor.animationFrame = slider.getValue() * (parser->getNumFrames() - 1);
parser->setFrame((int) audioProcessor.animationFrame);
}
};
addChildComponent(playButton);
addChildComponent(pauseButton);
addAndMakeVisible(stopButton);
addAndMakeVisible(repeatButton);
// Set up button behavior
playButton.onClick = [this]() {
audioProcessor.animateFrames->setValueNotifyingHost(true);
playButton.setVisible(false);
pauseButton.setVisible(true);
};
pauseButton.onClick = [this]() {
audioProcessor.animateFrames->setValueNotifyingHost(false);
playButton.setVisible(true);
pauseButton.setVisible(false);
};
repeatButton.onClick = [this]() {
audioProcessor.loopAnimation->setValueNotifyingHost(repeatButton.getToggleState());
};
stopButton.onClick = [this]() {
audioProcessor.animateFrames->setValueNotifyingHost(false);
playButton.setVisible(true);
pauseButton.setVisible(false);
slider.setValue(0, juce::sendNotification);
};
setup();
startTimer(20);
}
AnimationTimelineComponent::~AnimationTimelineComponent()
{
stopTimer();
}
void AnimationTimelineComponent::timerCallback()
{
juce::SpinLock::ScopedLockType sl(audioProcessor.parsersLock);
int currentFileIndex = audioProcessor.getCurrentFileIndex();
if (currentFileIndex < 0) return;
auto parser = audioProcessor.parsers[currentFileIndex];
if (parser == nullptr) return;
int totalFrames = parser->getNumFrames();
double frame = std::fmod(audioProcessor.animationFrame, totalFrames);
slider.setValue(frame / (totalFrames - 1), juce::dontSendNotification);
}
void AnimationTimelineComponent::setup()
{
// Get the current file parser
juce::SpinLock::ScopedLockType sl(audioProcessor.parsersLock);
int currentFileIndex = audioProcessor.getCurrentFileIndex();
bool hasAnimatableContent = false;
if (currentFileIndex >= 0) {
auto parser = audioProcessor.parsers[currentFileIndex];
if (parser->isAnimatable) {
hasAnimatableContent = true;
int totalFrames = parser->getNumFrames();
int currentFrame = parser->getCurrentFrame();
// Update the slider position without triggering the callback
if (totalFrames > 1) {
slider.setValue(static_cast<double>(currentFrame) / (totalFrames - 1), juce::dontSendNotification);
} else {
slider.setValue(0, juce::dontSendNotification);
}
}
}
// Update visibility of components
slider.setVisible(hasAnimatableContent);
repeatButton.setVisible(hasAnimatableContent);
stopButton.setVisible(hasAnimatableContent);
if (hasAnimatableContent) {
playButton.setVisible(!audioProcessor.animateFrames->getBoolValue());
pauseButton.setVisible(audioProcessor.animateFrames->getBoolValue());
} else {
playButton.setVisible(false);
pauseButton.setVisible(false);
}
}
void AnimationTimelineComponent::update()
{
setup();
}
void AnimationTimelineComponent::resized()
{
auto r = getLocalBounds();
auto playPauseBounds = r.removeFromLeft(25);
playButton.setBounds(playPauseBounds);
pauseButton.setBounds(playPauseBounds);
stopButton.setBounds(r.removeFromLeft(25));
repeatButton.setBounds(r.removeFromRight(25));
slider.setBounds(r);
}

Wyświetl plik

@ -0,0 +1,31 @@
#pragma once
#include <JuceHeader.h>
#include "../PluginProcessor.h"
#include "../LookAndFeel.h"
#include "../parser/FileParser.h"
#include "SvgButton.h"
class AnimationTimelineComponent : public juce::Component, public juce::Timer
{
public:
AnimationTimelineComponent(OscirenderAudioProcessor& processor);
~AnimationTimelineComponent() override;
void resized() override;
void update();
void timerCallback() override;
private:
OscirenderAudioProcessor& audioProcessor;
juce::Slider slider;
SvgButton playButton{"Play", BinaryData::play_svg, juce::Colours::white, juce::Colours::white};
SvgButton pauseButton{"Pause", BinaryData::pause_svg, juce::Colours::white, juce::Colours::white};
SvgButton stopButton{"Stop", BinaryData::stop_svg, juce::Colours::white, juce::Colours::white};
SvgButton repeatButton{"Repeat", BinaryData::repeat_svg, juce::Colours::white, Colours::accentColor, audioProcessor.loopAnimation};
void setup();
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AnimationTimelineComponent)
};

Wyświetl plik

@ -18,13 +18,15 @@ public:
static std::vector<std::vector<Line>> parseBinaryFrames(char* data, int dataLength);
static std::vector<Line> generateFrame(juce::Array < juce::var> objects, double focalLength);
int numFrames = 0;
int frameNumber = 0;
private:
static std::vector<std::vector<Line>> epicFail();
static double makeDouble(int64_t data);
static void makeChars(int64_t data, char* chars);
static std::vector<std::vector<OsciPoint>> reorderVertices(std::vector<std::vector<OsciPoint>> vertices);
static std::vector<Line> assembleFrame(std::vector<std::vector<std::vector<OsciPoint>>> allVertices, std::vector<std::vector<double>> allMatrices, double focalLength);
int frameNumber = 0;
std::vector<std::vector<Line>> frames;
int numFrames = 0;
};

Wyświetl plik

@ -4,9 +4,7 @@
#include "../CommonPluginEditor.h"
ImageParser::ImageParser(OscirenderAudioProcessor& p, juce::String extension, juce::MemoryBlock image) : audioProcessor(p) {
// Set up the temporary file
temp = std::make_unique<juce::TemporaryFile>();
juce::File file = temp->getFile();
juce::File file = temp.getFile();
{
juce::FileOutputStream output(file);
@ -142,18 +140,17 @@ void ImageParser::processVideoFile(juce::File& file) {
}
bool ImageParser::loadAllVideoFrames(const juce::File& file, const juce::File& ffmpegFile) {
juce::String altCmd = "\"" + ffmpegFile.getFullPathName() + "\" -i \"" + file.getFullPathName() +
"\" -hide_banner 2>&1";
juce::String cmd = "\"" + ffmpegFile.getFullPathName() + "\" -i \"" + file.getFullPathName() + "\" -hide_banner 2>&1";
ffmpegProcess.start(altCmd);
ffmpegProcess.start(cmd);
char altBuf[2048];
memset(altBuf, 0, sizeof(altBuf));
size_t altSize = ffmpegProcess.read(altBuf, sizeof(altBuf) - 1);
char buf[2048];
memset(buf, 0, sizeof(buf));
size_t size = ffmpegProcess.read(buf, sizeof(buf) - 1);
ffmpegProcess.close();
if (altSize > 0) {
juce::String output(altBuf, altSize);
if (size > 0) {
juce::String output(buf, size);
// Look for resolution in format "1920x1080"
std::regex resolutionRegex(R"((\d{2,5})x(\d{2,5}))");
@ -167,10 +164,23 @@ bool ImageParser::loadAllVideoFrames(const juce::File& file, const juce::File& f
}
}
// If still no dimensions, use defaults
// If still no dimensions or dimensions are too large, use reasonable defaults
if (width <= 0 || height <= 0) {
width = 640;
height = 360;
width = 320;
height = 240;
} else {
// Downscale large videos to improve performance
const int MAX_DIMENSION = 512;
if (width > MAX_DIMENSION || height > MAX_DIMENSION) {
float aspectRatio = static_cast<float>(width) / height;
if (width > height) {
width = MAX_DIMENSION;
height = static_cast<int>(width / aspectRatio);
} else {
height = MAX_DIMENSION;
width = static_cast<int>(height * aspectRatio);
}
}
}
// Now prepare for frame reading
@ -185,28 +195,36 @@ bool ImageParser::loadAllVideoFrames(const juce::File& file, const juce::File& f
// Cap the number of frames to prevent excessive memory usage
const int MAX_FRAMES = 10000;
// Start ffmpeg process to read frames
juce::String cmd = "\"" + ffmpegFile.getFullPathName() + "\" -i \"" + file.getFullPathName() +
"\" -f rawvideo -pix_fmt gray -v error -stats pipe:1";
// Determine available hardware acceleration options
#if JUCE_MAC
// Try to use videotoolbox on macOS
juce::String hwAccel = " -hwaccel videotoolbox";
#elif JUCE_WINDOWS
// Try to use DXVA2 on Windows
juce::String hwAccel = " -hwaccel dxva2";
#else
juce::String hwAccel = "";
#endif
// Start ffmpeg process to read frames with optimizations:
// - Use hardware acceleration if available
// - Lower resolution with scale filter
// - Use multiple threads for faster processing
// - Use gray colorspace directly to avoid extra conversion
cmd = "\"" + ffmpegFile.getFullPathName() + "\"" +
hwAccel +
" -i \"" + file.getFullPathName() + "\"" +
" -threads 8" + // Use 8 threads for processing
" -vf \"scale=" + juce::String(width) + ":" + juce::String(height) + "\"" + // Scale to target size
" -f rawvideo -pix_fmt gray" + // Output format
" -v error" + // Only show errors
" pipe:1"; // Output to stdout
ffmpegProcess.start(cmd);
if (!ffmpegProcess.isRunning()) {
return false;
}
// Read all frames into memory
int framesRead = 0;
// Flag to indicate which frames to save (first, middle, last)
bool shouldSaveFrame = false;
// Create debug directory in user documents
juce::File debugDir = juce::File::getSpecialLocation(juce::File::userDocumentsDirectory).getChildFile("osci-render-debug");
if (!debugDir.exists()) {
debugDir.createDirectory();
}
while (framesRead < MAX_FRAMES) {
size_t bytesRead = ffmpegProcess.read(frameBuffer.data(), frameBuffer.size());

Wyświetl plik

@ -16,6 +16,8 @@ public:
void setFrame(int index);
OsciPoint getSample();
int getNumFrames() { return frames.size(); }
int getCurrentFrame() const { return frameIndex; }
private:
void findNearestNeighbour(int searchRadius, float thresholdPow, int stride, bool invert);
@ -47,7 +49,7 @@ private:
// Video processing fields
ReadProcess ffmpegProcess;
bool isVideo = false;
std::unique_ptr<juce::TemporaryFile> temp;
juce::TemporaryFile temp;
std::vector<uint8_t> frameBuffer;
int videoFrameSize = 0;

Wyświetl plik

@ -152,12 +152,6 @@ OsciPoint FileParser::nextSample(lua_State*& L, LuaVariables& vars) {
return OsciPoint();
}
void FileParser::closeLua(lua_State*& L) {
if (lua != nullptr) {
lua->close(L);
}
}
bool FileParser::isSample() {
return sampleSource;
}
@ -201,3 +195,29 @@ std::shared_ptr<ImageParser> FileParser::getImg() {
std::shared_ptr<WavParser> FileParser::getWav() {
return wav;
}
int FileParser::getNumFrames() {
if (gpla != nullptr) {
return gpla->numFrames;
} else if (img != nullptr) {
return img->getNumFrames();
}
return 1; // Default to 1 frame for non-animatable content
}
int FileParser::getCurrentFrame() {
if (gpla != nullptr) {
return gpla->frameNumber;
} else if (img != nullptr) {
return img->getCurrentFrame();
}
return 0; // Default to frame 0 for non-animatable content
}
void FileParser::setFrame(int frame) {
if (gpla != nullptr) {
gpla->setFrame(frame);
} else if (img != nullptr) {
img->setFrame(frame);
}
}

Wyświetl plik

@ -18,12 +18,16 @@ public:
void parse(juce::String fileId, juce::String fileName, juce::String extension, std::unique_ptr<juce::InputStream> stream, juce::Font font);
std::vector<std::unique_ptr<Shape>> nextFrame();
OsciPoint nextSample(lua_State*& L, LuaVariables& vars);
void closeLua(lua_State*& L);
bool isSample();
bool isActive();
void disable();
void enable();
int getNumFrames();
int getCurrentFrame();
void setFrame(int frame);
std::shared_ptr<WorldObject> getObject();
std::shared_ptr<SvgParser> getSvg();
std::shared_ptr<TextParser> getText();

Wyświetl plik

@ -152,6 +152,10 @@
file="Source/components/AboutComponent.cpp"/>
<FILE id="vDlOTn" name="AboutComponent.h" compile="0" resource="0"
file="Source/components/AboutComponent.h"/>
<FILE id="AAXWW6" name="AnimationTimelineComponent.cpp" compile="1"
resource="0" file="Source/components/AnimationTimelineComponent.cpp"/>
<FILE id="ppzO8J" name="AnimationTimelineComponent.h" compile="0" resource="0"
file="Source/components/AnimationTimelineComponent.h"/>
<FILE id="xxiMAy" name="AudioPlayerComponent.cpp" compile="1" resource="0"
file="Source/components/AudioPlayerComponent.cpp"/>
<FILE id="DSvDMv" name="AudioPlayerComponent.h" compile="0" resource="0"
@ -218,6 +222,7 @@
file="Source/concurrency/BufferConsumer.h"/>
<FILE id="L9aCHY" name="readerwritercircularbuffer.h" compile="0" resource="0"
file="Source/concurrency/readerwritercircularbuffer.h"/>
<FILE id="noScYw" name="ReadProcess.h" compile="0" resource="0" file="Source/concurrency/ReadProcess.h"/>
<FILE id="aat2Je" name="WriteProcess.h" compile="0" resource="0" file="Source/concurrency/WriteProcess.h"/>
</GROUP>
<GROUP id="{A3E24187-62A5-AB8D-8837-14043B89A640}" name="gpla">