kopia lustrzana https://github.com/jameshball/osci-render
Add support for opening mp4 and mov files
rodzic
27cbd30a45
commit
7e43db6c66
|
@ -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());
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
};
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
Ładowanie…
Reference in New Issue