Add support for opening mp4 and mov files

pull/300/head
James H Ball 2025-04-21 18:05:04 +01:00
rodzic 27cbd30a45
commit 7e43db6c66
14 zmienionych plików z 521 dodań i 154 usunięć

Wyświetl plik

@ -5,10 +5,6 @@
CommonPluginEditor::CommonPluginEditor(CommonAudioProcessor& p, juce::String appName, juce::String projectFileType, int defaultWidth, int defaultHeight)
: AudioProcessorEditor(&p), audioProcessor(p), appName(appName), projectFileType(projectFileType)
{
if (!applicationFolder.exists()) {
applicationFolder.createDirectory();
}
#if JUCE_LINUX
// use OpenGL on Linux for much better performance. The default on Mac is CoreGraphics, and on Window is Direct2D which is much faster.
openGlContext.attachTo(*getTopLevelComponent());

Wyświetl plik

@ -8,6 +8,7 @@
#include "components/SosciMainMenuBarModel.h"
#include "components/SvgButton.h"
#include "components/VolumeComponent.h"
#include "components/DownloaderComponent.h"
class CommonPluginEditor : public juce::AudioProcessorEditor {
public:
@ -30,19 +31,6 @@ public:
private:
CommonAudioProcessor& audioProcessor;
bool fullScreen = false;
juce::File applicationFolder = juce::File::getSpecialLocation(juce::File::SpecialLocationType::userApplicationDataDirectory)
#if JUCE_MAC
.getChildFile("Application Support")
#endif
.getChildFile("osci-render");
juce::String ffmpegFileName =
#if JUCE_WINDOWS
"ffmpeg.exe";
#else
"ffmpeg";
#endif
public:
OscirenderLookAndFeel lookAndFeel;
@ -51,6 +39,7 @@ public:
juce::String currentFileName;
#if SOSCI_FEATURES
DownloaderComponent ffmpegDownloader;
SharedTextureManager sharedTextureManager;
#endif
@ -65,10 +54,11 @@ public:
SettingsWindow recordingSettingsWindow = SettingsWindow("Recording Settings", recordingSettings, 330, 350, 330, 350);
VisualiserComponent visualiser{
audioProcessor,
*this,
#if SOSCI_FEATURES
sharedTextureManager,
#endif
applicationFolder.getChildFile(ffmpegFileName),
audioProcessor.applicationFolder.getChildFile(audioProcessor.ffmpegFileName),
visualiserSettings,
recordingSettings,
nullptr,

Wyświetl plik

@ -17,6 +17,10 @@ CommonAudioProcessor::CommonAudioProcessor(const BusesProperties& busesPropertie
: AudioProcessor(busesProperties)
#endif
{
if (!applicationFolder.exists()) {
applicationFolder.createDirectory();
}
// Initialize the global settings with the plugin name
juce::PropertiesFile::Options options;
options.applicationName = JucePlugin_Name + juce::String("_globals");
@ -423,3 +427,85 @@ bool CommonAudioProcessor::programCrashedAndUserWantsToReset() {
return userWantsToReset;
}
juce::String CommonAudioProcessor::getFFmpegURL() {
juce::String ffmpegURL = juce::String("https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/") +
#if JUCE_WINDOWS
#if JUCE_64BIT
"ffmpeg-win32-x64"
#elif JUCE_32BIT
"ffmpeg-win32-ia32"
#endif
#elif JUCE_MAC
#if JUCE_ARM
"ffmpeg-darwin-arm64"
#elif JUCE_INTEL
"ffmpeg-darwin-x64"
#endif
#elif JUCE_LINUX
#if JUCE_ARM
#if JUCE_64BIT
"ffmpeg-linux-arm64"
#elif JUCE_32BIT
"ffmpeg-linux-arm"
#endif
#elif JUCE_INTEL
#if JUCE_64BIT
"ffmpeg-linux-x64"
#elif JUCE_32BIT
"ffmpeg-linux-ia32"
#endif
#endif
#endif
+ ".gz";
return ffmpegURL;
}
bool CommonAudioProcessor::ensureFFmpegExists(std::function<void()> onStart, std::function<void()> onSuccess) {
juce::File ffmpegFile = getFFmpegFile();
if (ffmpegFile.exists()) {
// FFmpeg already exists
if (onSuccess != nullptr) {
onSuccess();
}
return true;
}
auto editor = dynamic_cast<CommonPluginEditor*>(getActiveEditor());
if (editor == nullptr) {
return false; // Editor not found
}
juce::String url = getFFmpegURL();
editor->ffmpegDownloader.setup(url, ffmpegFile);
editor->ffmpegDownloader.onSuccessfulDownload = [this, onSuccess]() {
if (onSuccess != nullptr) {
juce::MessageManager::callAsync(onSuccess);
}
};
// Ask the user if they want to download ffmpeg
juce::MessageBoxOptions options = juce::MessageBoxOptions()
.withTitle("FFmpeg Required")
.withMessage("FFmpeg is required to process video files.\n\nWould you like to download it now?")
.withButton("Yes")
.withButton("No")
.withIconType(juce::AlertWindow::QuestionIcon)
.withAssociatedComponent(editor);
juce::AlertWindow::showAsync(options, [this, onStart, editor](int result) {
if (result == 1) { // Yes
editor->ffmpegDownloader.setVisible(true);
editor->ffmpegDownloader.download();
if (onStart != nullptr) {
onStart();
}
editor->resized();
}
});
return false;
}

Wyświetl plik

@ -65,6 +65,15 @@ public:
std::any getProperty(const std::string& key, std::any defaultValue);
void setProperty(const std::string& key, std::any value);
// Get the ffmpeg binary file
juce::File getFFmpegFile() const { return applicationFolder.getChildFile(ffmpegFileName); }
// Check if ffmpeg exists, if not download it
bool ensureFFmpegExists(std::function<void()> onStart = nullptr, std::function<void()> onSuccess = nullptr);
// A static method to get the appropriate ffmpeg URL based on platform
static juce::String getFFmpegURL();
// Global settings methods
bool getGlobalBoolValue(const juce::String& keyName, bool defaultValue = false) const;
int getGlobalIntValue(const juce::String& keyName, int defaultValue = 0) const;
@ -129,6 +138,19 @@ public:
juce::File getLastOpenedDirectory();
void setLastOpenedDirectory(const juce::File& directory);
juce::File applicationFolder = juce::File::getSpecialLocation(juce::File::SpecialLocationType::userApplicationDataDirectory)
#if JUCE_MAC
.getChildFile("Application Support")
#endif
.getChildFile("osci-render");
juce::String ffmpegFileName =
#if JUCE_WINDOWS
"ffmpeg.exe";
#else
"ffmpeg";
#endif
protected:
bool brightnessEnabled = false;

Wyświetl plik

@ -10,7 +10,7 @@ MainComponent::MainComponent(OscirenderAudioProcessor& p, OscirenderAudioProcess
fileButton.setButtonText("Choose File(s)");
fileButton.onClick = [this] {
chooser = std::make_unique<juce::FileChooser>("Open", audioProcessor.getLastOpenedDirectory(), "*.obj;*.svg;*.lua;*.txt;*.gpla;*.gif;*.png;*.jpg;*.jpeg;*.wav;*.aiff;*.ogg;*.flac;*.mp3");
chooser = std::make_unique<juce::FileChooser>("Open", audioProcessor.getLastOpenedDirectory(), "*.obj;*.svg;*.lua;*.txt;*.gpla;*.gif;*.png;*.jpg;*.jpeg;*.wav;*.aiff;*.ogg;*.flac;*.mp3;*.mp4;*.mov");
auto flags = juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectMultipleItems |
juce::FileBrowserComponent::canSelectFiles;

Wyświetl plik

@ -75,14 +75,14 @@ void SettingsComponent::fileUpdated(juce::String fileName) {
juce::String extension = fileName.fromLastOccurrenceOf(".", true, false).toLowerCase();
txt.setVisible(false);
frame.setVisible(false);
bool isImage = extension == ".gif" || extension == ".png" || extension == ".jpg" || extension == ".jpeg";
bool isImage = extension == ".gif" || extension == ".png" || extension == ".jpg" || extension == ".jpeg" || extension == ".mov" || extension == ".mp4";
if (fileName.isEmpty() || audioProcessor.objectServerRendering) {
// do nothing
} else if (extension == ".txt") {
txt.setVisible(true);
} else if (extension == ".gpla" || isImage) {
frame.setVisible(true);
frame.setAnimated(extension == ".gpla" || extension == ".gif");
frame.setAnimated(extension == ".gpla" || extension == ".gif" || extension == ".mov" || extension == ".mp4");
frame.setImage(isImage);
frame.resized();
}

Wyświetl plik

@ -1,8 +1,13 @@
#include "DownloaderComponent.h"
DownloaderComponent::DownloaderComponent(juce::URL url, juce::File file) : juce::Thread("DownloaderComponent"), url(url), file(file) {
DownloaderComponent::DownloaderComponent() : juce::Thread("DownloaderComponent") {
addChildComponent(progressBar);
addChildComponent(successLabel);
}
void DownloaderComponent::setup(juce::URL url, juce::File file) {
this->url = url;
this->file = file;
successLabel.setText(file.getFileName() + " downloaded!", juce::dontSendNotification);

Wyświetl plik

@ -21,14 +21,16 @@ private:
class DownloaderComponent : public juce::Component, public juce::Thread, public juce::URL::DownloadTaskListener {
public:
DownloaderComponent(juce::URL url, juce::File file);
DownloaderComponent();
void setup(juce::URL url, juce::File file);
void download();
void run() override;
void threadComplete();
void resized() override;
void finished(juce::URL::DownloadTask* task, bool success) override;
void progress(juce::URL::DownloadTask* task, juce::int64 bytesDownloaded, juce::int64 totalLength) override;
bool isDownloading() const { return task != nullptr && !task->isFinished(); }
std::function<void()> onSuccessfulDownload;

Wyświetl plik

@ -0,0 +1,136 @@
#pragma once
#include <JuceHeader.h>
#if JUCE_WINDOWS
#include <windows.h>
#endif
class ReadProcess {
public:
ReadProcess() {}
void start(juce::String cmd) {
if (isRunning()) {
close();
}
#if JUCE_WINDOWS
cmd = "cmd /c \"" + cmd + "\"";
SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES), NULL, TRUE };
if (!CreatePipe(&hReadPipe, &hWritePipe, &sa, 0)) {
errorExit(TEXT("CreatePipe"));
return;
}
// Mark the read handle as inheritable
if (!SetHandleInformation(hReadPipe, HANDLE_FLAG_INHERIT, 0)) {
CloseHandle(hReadPipe);
CloseHandle(hWritePipe);
errorExit(TEXT("SetHandleInformation"));
return;
}
STARTUPINFO si = { 0 };
si.cb = sizeof(STARTUPINFO);
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdOutput = hWritePipe; // Child process writes here
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
// Create the process
if (!CreateProcess(NULL, const_cast<LPSTR>(cmd.toStdString().c_str()), NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
CloseHandle(hReadPipe);
CloseHandle(hWritePipe);
errorExit(TEXT("CreateProcess"));
return;
}
// Close the write end of the pipe as we don't need it
CloseHandle(hWritePipe);
hWritePipe = INVALID_HANDLE_VALUE;
#else
process = popen(cmd.toStdString().c_str(), "r");
if (process == nullptr) {
DBG("popen failed: " + juce::String(std::strerror(errno)));
jassertfalse;
}
#endif
}
size_t read(void* buffer, size_t size) {
#if JUCE_WINDOWS
DWORD bytesRead = 0;
if (!ReadFile(hReadPipe, buffer, size, &bytesRead, NULL) && GetLastError() != ERROR_BROKEN_PIPE) {
errorExit(TEXT("ReadFile"));
}
return bytesRead;
#else
return fread(buffer, 1, size, process);
#endif
}
void close() {
if (isRunning()) {
#if JUCE_WINDOWS
// Clean up
CloseHandle(hReadPipe);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
if (hWritePipe != INVALID_HANDLE_VALUE)
CloseHandle(hWritePipe);
hReadPipe = INVALID_HANDLE_VALUE;
hWritePipe = INVALID_HANDLE_VALUE;
#else
pclose(process);
process = nullptr;
#endif
}
}
bool isRunning() {
#if JUCE_WINDOWS
return hReadPipe != INVALID_HANDLE_VALUE;
#else
return process != nullptr;
#endif
}
#if JUCE_WINDOWS
void errorExit(PCTSTR lpszFunction) {
LPVOID lpMsgBuf;
DWORD dw = GetLastError();
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
dw,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR) &lpMsgBuf,
0, NULL );
DBG("Error: " + juce::String((LPCTSTR)lpMsgBuf));
LocalFree(lpMsgBuf);
jassertfalse;
}
#endif
~ReadProcess() {
close();
}
private:
#if JUCE_WINDOWS
HANDLE hReadPipe = INVALID_HANDLE_VALUE;
HANDLE hWritePipe = INVALID_HANDLE_VALUE;
PROCESS_INFORMATION pi;
#else
FILE* process = nullptr;
#endif
};

Wyświetl plik

@ -1,10 +1,12 @@
#include "ImageParser.h"
#include "gifdec.h"
#include "../PluginProcessor.h"
#include "../CommonPluginEditor.h"
ImageParser::ImageParser(OscirenderAudioProcessor& p, juce::String extension, juce::MemoryBlock image) : audioProcessor(p) {
juce::TemporaryFile temp{".gif"};
juce::File file = temp.getFile();
// Set up the temporary file
temp = std::make_unique<juce::TemporaryFile>();
juce::File file = temp->getFile();
{
juce::FileOutputStream output(file);
@ -13,74 +15,24 @@ ImageParser::ImageParser(OscirenderAudioProcessor& p, juce::String extension, ju
output.write(image.getData(), image.getSize());
output.flush();
} else {
handleError("The image could not be loaded.");
handleError("The file could not be loaded.");
return;
}
}
if (extension.equalsIgnoreCase(".gif")) {
juce::String fileName = file.getFullPathName();
gd_GIF *gif = gd_open_gif(fileName.toRawUTF8());
if (gif != nullptr) {
width = gif->width;
height = gif->height;
int frameSize = width * height;
std::vector<uint8_t> tempBuffer = std::vector<uint8_t>(frameSize * 3);
visited = std::vector<bool>(frameSize, false);
int i = 0;
while (gd_get_frame(gif) > 0) {
gd_render_frame(gif, tempBuffer.data());
frames.emplace_back(std::vector<uint8_t>(frameSize));
uint8_t *pixels = tempBuffer.data();
for (int j = 0; j < tempBuffer.size(); j += 3) {
uint8_t avg = (pixels[j] + pixels[j + 1] + pixels[j + 2]) / 3;
// value of 0 is reserved for transparent pixels
frames[i][j / 3] = juce::jmax(1, (int) avg);
}
i++;
}
gd_close_gif(gif);
} else {
handleError("The image could not be loaded. Please try optimising the GIF with https://ezgif.com/optimize.");
return;
}
processGifFile(file);
} else if (isVideoFile(extension)) {
processVideoFile(file);
} else {
juce::Image image = juce::ImageFileFormat::loadFrom(file);
if (image.isValid()) {
image.desaturate();
width = image.getWidth();
height = image.getHeight();
int frameSize = width * height;
visited = std::vector<bool>(frameSize, false);
frames.emplace_back(std::vector<uint8_t>(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
int value = pixel.getRed();
// value of 0 is reserved for transparent pixels
frames[0][index] = pixel.isTransparent() ? 0 : juce::jmax(1, value);
}
}
} else {
handleError("The image could not be loaded.");
return;
}
processImageFile(file);
}
if (frames.size() == 0) {
if (extension.equalsIgnoreCase(".gif")) {
handleError("The image could not be loaded. Please try optimising the GIF with https://ezgif.com/optimize.");
} else if (isVideoFile(extension)) {
handleError("The video could not be loaded. Please check that ffmpeg is installed.");
} else {
handleError("The image could not be loaded.");
}
@ -90,7 +42,202 @@ ImageParser::ImageParser(OscirenderAudioProcessor& p, juce::String extension, ju
setFrame(0);
}
ImageParser::~ImageParser() {}
bool ImageParser::isVideoFile(const juce::String& extension) const {
return extension.equalsIgnoreCase(".mp4") || extension.equalsIgnoreCase(".mov");
}
void ImageParser::processGifFile(juce::File& file) {
juce::String fileName = file.getFullPathName();
gd_GIF *gif = gd_open_gif(fileName.toRawUTF8());
if (gif != nullptr) {
width = gif->width;
height = gif->height;
int frameSize = width * height;
std::vector<uint8_t> tempBuffer = std::vector<uint8_t>(frameSize * 3);
visited = std::vector<bool>(frameSize, false);
int i = 0;
while (gd_get_frame(gif) > 0) {
gd_render_frame(gif, tempBuffer.data());
frames.emplace_back(std::vector<uint8_t>(frameSize));
uint8_t *pixels = tempBuffer.data();
for (int j = 0; j < tempBuffer.size(); j += 3) {
uint8_t avg = (pixels[j] + pixels[j + 1] + pixels[j + 2]) / 3;
// value of 0 is reserved for transparent pixels
frames[i][j / 3] = juce::jmax(1, (int) avg);
}
i++;
}
gd_close_gif(gif);
} else {
handleError("The GIF could not be loaded. Please try optimising the GIF with https://ezgif.com/optimize.");
}
}
void ImageParser::processImageFile(juce::File& file) {
juce::Image image = juce::ImageFileFormat::loadFrom(file);
if (image.isValid()) {
image.desaturate();
width = image.getWidth();
height = image.getHeight();
int frameSize = width * height;
visited = std::vector<bool>(frameSize, false);
frames.emplace_back(std::vector<uint8_t>(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
int value = pixel.getRed();
// value of 0 is reserved for transparent pixels
frames[0][index] = pixel.isTransparent() ? 0 : juce::jmax(1, value);
}
}
} else {
handleError("The image could not be loaded.");
}
}
void ImageParser::processVideoFile(juce::File& file) {
// Set video processing flag
isVideo = true;
// assert on the message thread
if (!juce::MessageManager::getInstance()->isThisTheMessageThread()) {
handleError("Could not process video file - not on the message thread.");
return;
}
// Try to get ffmpeg
juce::File ffmpegFile = audioProcessor.getFFmpegFile();
if (ffmpegFile.exists()) {
// FFmpeg exists, continue with video processing
if (!loadAllVideoFrames(file, ffmpegFile)) {
handleError("Could not read video frames. Please ensure the video file is valid.");
}
} else {
// Ask user to download ffmpeg
audioProcessor.ensureFFmpegExists(nullptr, [this, file]() {
// This will be called once ffmpeg is successfully downloaded
juce::File ffmpegFile = audioProcessor.getFFmpegFile();
if (!loadAllVideoFrames(file, ffmpegFile)) {
handleError("Could not read video frames after downloading ffmpeg. Please ensure the video file is valid.");
} else {
// Successfully loaded frames after downloading ffmpeg
if (frames.size() > 0) {
setFrame(0);
}
}
});
}
}
bool ImageParser::loadAllVideoFrames(const juce::File& file, const juce::File& ffmpegFile) {
juce::String altCmd = "\"" + ffmpegFile.getFullPathName() + "\" -i \"" + file.getFullPathName() +
"\" -hide_banner 2>&1";
ffmpegProcess.start(altCmd);
char altBuf[2048];
memset(altBuf, 0, sizeof(altBuf));
size_t altSize = ffmpegProcess.read(altBuf, sizeof(altBuf) - 1);
ffmpegProcess.close();
if (altSize > 0) {
juce::String output(altBuf, altSize);
// Look for resolution in format "1920x1080"
std::regex resolutionRegex(R"((\d{2,5})x(\d{2,5}))");
std::smatch match;
std::string stdOut = output.toStdString();
if (std::regex_search(stdOut, match, resolutionRegex) && match.size() == 3)
{
width = std::stoi(match[1].str());
height = std::stoi(match[2].str());
}
}
// If still no dimensions, use defaults
if (width <= 0 || height <= 0) {
width = 640;
height = 360;
}
// Now prepare for frame reading
int frameSize = width * height;
videoFrameSize = frameSize;
visited = std::vector<bool>(frameSize, false);
frameBuffer.resize(frameSize);
// Clear any existing frames
frames.clear();
// 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";
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());
if (bytesRead != frameBuffer.size()) {
break; // End of video or error
}
// Create a new frame
frames.emplace_back(std::vector<uint8_t>(videoFrameSize));
// Copy data to the current frame
for (int i = 0; i < videoFrameSize; i++) {
// value of 0 is reserved for transparent pixels
frames.back()[i] = juce::jmax(1, (int)frameBuffer[i]);
}
framesRead++;
}
// Close the ffmpeg process
ffmpegProcess.close();
// Return true if we successfully loaded at least one frame
return frames.size() > 0;
}
ImageParser::~ImageParser() {
if (ffmpegProcess.isRunning()) {
ffmpegProcess.close();
}
}
void ImageParser::handleError(juce::String message) {
juce::MessageManager::callAsync([this, message] {
@ -106,7 +253,9 @@ void ImageParser::handleError(juce::String message) {
void ImageParser::setFrame(int index) {
// Ensure that the frame number is within the bounds of the number of frames
// This weird modulo trick is to handle negative numbers
frameIndex = (frames.size() + (index % frames.size())) % frames.size();
index = (frames.size() + (index % frames.size())) % frames.size();
frameIndex = index;
resetPosition();
std::fill(visited.begin(), visited.end(), false);
}
@ -123,7 +272,7 @@ void ImageParser::resetPosition() {
float ImageParser::getPixelValue(int x, int y, bool invert) {
int index = (height - y - 1) * width + x;
if (index < 0 || index >= frames[frameIndex].size()) {
if (index < 0 || frames.size() <= 0 || index >= frames[frameIndex].size()) {
return 0;
}
float pixel = frames[frameIndex][index] / (float) std::numeric_limits<uint8_t>::max();

Wyświetl plik

@ -4,8 +4,11 @@
#include "../shape/Shape.h"
#include "../svg/SvgParser.h"
#include "../shape/Line.h"
#include "../concurrency/ReadProcess.h"
class OscirenderAudioProcessor;
class CommonPluginEditor;
class ImageParser {
public:
ImageParser(OscirenderAudioProcessor& p, juce::String fileName, juce::MemoryBlock image);
@ -18,10 +21,16 @@ private:
void findNearestNeighbour(int searchRadius, float thresholdPow, int stride, bool invert);
void resetPosition();
float getPixelValue(int x, int y, bool invert);
int getPixelIndex(int x, int y);
void findWhite(double thresholdPow, bool invert);
bool isOverThreshold(double pixel, double thresholdValue);
int jumpFrequency();
void handleError(juce::String message);
void processGifFile(juce::File& file);
void processImageFile(juce::File& file);
void processVideoFile(juce::File& file);
bool loadAllVideoFrames(const juce::File& file, const juce::File& ffmpegFile);
bool isVideoFile(const juce::String& extension) const;
const juce::String ALGORITHM = "HILLIGOSS";
@ -31,9 +40,17 @@ private:
std::vector<std::vector<uint8_t>> frames;
std::vector<bool> visited;
int currentX, currentY;
int width, height;
int width = -1;
int height = -1;
int count = 0;
// Video processing fields
ReadProcess ffmpegProcess;
bool isVideo = false;
std::unique_ptr<juce::TemporaryFile> temp;
std::vector<uint8_t> frameBuffer;
int videoFrameSize = 0;
// experiments
double scanX = -1;
double scanY = 1;

Wyświetl plik

@ -93,7 +93,7 @@ void FileParser::parse(juce::String fileId, juce::String fileName, juce::String
stream->setPosition(0);
gpla = std::make_shared<LineArtParser>(stream->readEntireStreamAsString());
}
} else if (extension == ".gif" || extension == ".png" || extension == ".jpg" || extension == ".jpeg") {
} else if (extension == ".gif" || extension == ".png" || extension == ".jpg" || extension == ".jpeg" || extension == ".mp4" || extension == ".mov") {
juce::MemoryBlock buffer{};
int bytesRead = stream->readIntoMemoryBlock(buffer);
img = std::make_shared<ImageParser>(audioProcessor, extension, buffer);
@ -108,7 +108,7 @@ void FileParser::parse(juce::String fileId, juce::String fileName, juce::String
}
}
isAnimatable = gpla != nullptr || (img != nullptr && extension == ".gif");
isAnimatable = gpla != nullptr || (img != nullptr && (extension == ".gif" || extension == ".mp4" || extension == ".mov"));
sampleSource = lua != nullptr || img != nullptr || wav != nullptr;
}

Wyświetl plik

@ -1,6 +1,7 @@
#include "../LookAndFeel.h"
#include "VisualiserComponent.h"
#include "../CommonPluginProcessor.h"
#include "../CommonPluginEditor.h"
#include "AfterglowFragmentShader.glsl"
#include "AfterglowVertexShader.glsl"
@ -21,6 +22,7 @@
VisualiserComponent::VisualiserComponent(
CommonAudioProcessor& processor,
CommonPluginEditor& pluginEditor,
#if SOSCI_FEATURES
SharedTextureManager& sharedTextureManager,
#endif
@ -38,22 +40,10 @@ VisualiserComponent::VisualiserComponent(
recordingSettings(recordingSettings),
visualiserOnly(visualiserOnly),
AudioBackgroundThread("VisualiserComponent" + juce::String(parent != nullptr ? " Child" : ""), processor.threadManager),
parent(parent) {
parent(parent),
editor(pluginEditor) {
#if SOSCI_FEATURES
addAndMakeVisible(ffmpegDownloader);
ffmpegDownloader.onSuccessfulDownload = [this] {
juce::MessageManager::callAsync([this] {
record.setEnabled(true);
juce::Timer::callAfterDelay(3000, [this] {
juce::MessageManager::callAsync([this] {
ffmpegDownloader.setVisible(false);
downloading = false;
resized();
});
});
});
};
addAndMakeVisible(editor.ffmpegDownloader);
#endif
audioProcessor.haltRecording = [this] {
@ -407,25 +397,26 @@ void VisualiserComponent::setRecording(bool recording) {
}
if (recordingVideo) {
if (!ffmpegFile.exists()) {
// ask the user if they want to download ffmpeg
juce::MessageBoxOptions options = juce::MessageBoxOptions()
.withTitle("Recording requires FFmpeg")
.withMessage("FFmpeg is required to record video to .mp4.\n\nWould you like to download it now?")
.withButton("Yes")
.withButton("No")
.withIconType(juce::AlertWindow::QuestionIcon)
.withAssociatedComponent(this);
juce::AlertWindow::showAsync(options, [this](int result) {
if (result == 1) {
record.setEnabled(false);
ffmpegDownloader.download();
ffmpegDownloader.setVisible(true);
downloading = true;
resized();
}
auto onDownloadSuccess = [this] {
juce::MessageManager::callAsync([this] {
record.setEnabled(true);
juce::Timer::callAfterDelay(3000, [this] {
juce::MessageManager::callAsync([this] {
editor.ffmpegDownloader.setVisible(false);
downloading = false;
resized();
});
});
});
};
auto onDownloadStart = [this] {
juce::MessageManager::callAsync([this] {
record.setEnabled(false);
downloading = true;
resized();
});
};
if (!audioProcessor.ensureFFmpegExists(onDownloadStart, onDownloadSuccess)) {
record.setToggleState(false, juce::NotificationType::dontSendNotification);
return;
}
@ -588,7 +579,7 @@ void VisualiserComponent::resized() {
#if SOSCI_FEATURES
if (child == nullptr && downloading) {
auto bounds = buttons.removeFromRight(160);
ffmpegDownloader.setBounds(bounds.withSizeKeepingCentre(bounds.getWidth() - 10, bounds.getHeight() - 10));
editor.ffmpegDownloader.setBounds(bounds.withSizeKeepingCentre(bounds.getWidth() - 10, bounds.getHeight() - 10));
}
#endif
@ -611,6 +602,7 @@ void VisualiserComponent::popoutWindow() {
setRecording(false);
auto visualiser = new VisualiserComponent(
audioProcessor,
editor,
#if SOSCI_FEATURES
sharedTextureManager,
#endif
@ -640,7 +632,7 @@ void VisualiserComponent::popoutWindow() {
void VisualiserComponent::childUpdated() {
popOutButton.setVisible(child == nullptr);
#if SOSCI_FEATURES
ffmpegDownloader.setVisible(child == nullptr);
editor.ffmpegDownloader.setVisible(child == nullptr);
#endif
record.setVisible(child == nullptr);
audioPlayer.setVisible(child == nullptr);

Wyświetl plik

@ -32,11 +32,13 @@ struct Texture {
};
class CommonAudioProcessor;
class CommonPluginEditor;
class VisualiserWindow;
class VisualiserComponent : public juce::Component, public AudioBackgroundThread, public juce::MouseListener, public juce::OpenGLRenderer, public juce::AsyncUpdater {
public:
VisualiserComponent(
CommonAudioProcessor& processor,
CommonPluginEditor& editor,
#if SOSCI_FEATURES
SharedTextureManager& sharedTextureManager,
#endif
@ -80,6 +82,7 @@ public:
private:
CommonAudioProcessor& audioProcessor;
CommonPluginEditor& editor;
float intensity;
@ -117,37 +120,6 @@ private:
std::vector<unsigned char> framePixels;
WriteProcess ffmpegProcess;
std::unique_ptr<juce::TemporaryFile> tempVideoFile;
juce::String ffmpegURL = juce::String("https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/") +
#if JUCE_WINDOWS
#if JUCE_64BIT
"ffmpeg-win32-x64"
#elif JUCE_32BIT
"ffmpeg-win32-ia32"
#endif
#elif JUCE_MAC
#if JUCE_ARM
"ffmpeg-darwin-arm64"
#elif JUCE_INTEL
"ffmpeg-darwin-x64"
#endif
#elif JUCE_LINUX
#if JUCE_ARM
#if JUCE_64BIT
"ffmpeg-linux-arm64"
#elif JUCE_32BIT
"ffmpeg-linux-arm"
#endif
#elif JUCE_INTEL
#if JUCE_64BIT
"ffmpeg-linux-x64"
#elif JUCE_32BIT
"ffmpeg-linux-ia32"
#endif
#endif
#endif
+ ".gz";
DownloaderComponent ffmpegDownloader{ffmpegURL, ffmpegFile};
#endif
StopwatchComponent stopwatch;