Implement recording to PNG or QOI

pull/261/head
DJLevel3 2024-11-18 14:21:32 -05:00 zatwierdzone przez James H Ball
rodzic 513ec136d6
commit c1b5f784d1
4 zmienionych plików z 1611 dodań i 52 usunięć

1400
Source/img/qoixx.hpp 100644

Plik diff jest za duży Load Diff

Wyświetl plik

@ -24,13 +24,14 @@ float noise(in vec2 uv, in float time) {
}
void main() {
float glow = uGlow / (2.0 * max(0.0001,sqrt(uExposure)));
vec4 line = texture2D(uTexture0, vTexCoordCanvas);
// r components have grid; g components do not.
vec4 screen = texture2D(uTexture3, vTexCoord);
vec4 tightGlow = texture2D(uTexture1, vTexCoord);
vec4 scatter = texture2D(uTexture2, vTexCoord)+0.35;
float light = line.r + uGlow * 1.5 * screen.g * screen.g * tightGlow.r;
light += uGlow * 0.4 * scatter.g * (2.0 + 1.0 * screen.g + 0.5 * screen.r);
vec4 scatter = texture2D(uTexture2, vTexCoord)+glow;
float light = line.r * 1.2 * screen.r + 1.5 * screen.r * screen.g * tightGlow.r;
light += glow * 0.4 * scatter.g * (2.0 + 1.0 * screen.r + 0.5 * screen.r);
float tlight = 1.0-pow(2.0, -uExposure*light);
float tlight2 = tlight * tlight * tlight;
gl_FragColor.rgb = mix(uColour, vec3(1.0), 0.3+tlight2*tlight2*0.5)*tlight;

Wyświetl plik

@ -14,14 +14,17 @@
VisualiserComponent::VisualiserComponent(AudioBackgroundThreadManager& threadManager, VisualiserSettings& settings, VisualiserComponent* parent, bool visualiserOnly) : settings(settings), threadManager(threadManager), visualiserOnly(visualiserOnly), AudioBackgroundThread("VisualiserComponent", threadManager), parent(parent) {
addAndMakeVisible(record);
record.setPulseAnimation(true);
//record.setPulseAnimation(true);
record.onClick = [this] {
toggleRecording();
stopwatch.stop();
stopwatch.reset();
//stopwatch.stop();
//stopwatch.reset();
/*
if (record.getToggleState()) {
stopwatch.start();
}
*/
record.setToggleState(false, juce::NotificationType::dontSendNotification);
resized();
};
@ -134,7 +137,11 @@ bool VisualiserComponent::keyPressed(const juce::KeyPress& key) {
void VisualiserComponent::setFullScreen(bool fullScreen) {}
void VisualiserComponent::toggleRecording() {
chooser = std::make_unique<juce::FileChooser>("Choose a .wav file to render...", juce::File(), "*.wav;*.flac");
auto chooserFlags = juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::canSelectFiles;
chooser->launchAsync(chooserFlags, [this](const juce::FileChooser& fc) {
audioFile = fc.getResult();
});
}
void VisualiserComponent::haltRecording() {
@ -244,6 +251,7 @@ void VisualiserComponent::openGLContextClosing() {
glDeleteTextures(1, &blur2Texture.id);
glDeleteTextures(1, &blur3Texture.id);
glDeleteTextures(1, &blur4Texture.id);
glDeleteTextures(1, &renderTexture.id);
screenOpenGLTexture.release();
simpleShader.reset();
@ -279,26 +287,20 @@ void VisualiserComponent::renderOpenGL() {
setupArrays(RESAMPLE_RATIO * sampleRate / FRAME_RATE);
}
time += 0.01f;
juce::OpenGLHelpers::clear(juce::Colours::black);
intensity = settings.getIntensity() * (41000.0f / sampleRate);
if (active) {
juce::CriticalSection::ScopedLockType lock(samplesLock);
if (graticuleEnabled != settings.getGraticuleEnabled() || smudgesEnabled != settings.getSmudgesEnabled()) {
graticuleEnabled = settings.getGraticuleEnabled();
smudgesEnabled = settings.getSmudgesEnabled();
screenTexture = createScreenTexture();
if (audioFile != juce::File{}) {
renderAudioFile(audioFile, FILE_RENDER_QOI);
audioFile = juce::File{};
}
renderScale = (float) openGLContext.getRenderingScale();
if (settings.parameters.upsamplingEnabled->getBoolValue()) {
drawLineTexture(smoothedXSamples, smoothedYSamples, smoothedZSamples);
renderScope(smoothedXSamples, smoothedYSamples, smoothedZSamples);
} else {
drawLineTexture(xSamples, ySamples, zSamples);
renderScope(xSamples, ySamples, zSamples);
}
checkGLErrors("drawLineTexture");
drawCRT();
checkGLErrors("drawCRT");
}
}
}
@ -373,10 +375,12 @@ void VisualiserComponent::setupTextures() {
blur2Texture = makeTexture(256, 256);
blur3Texture = makeTexture(32, 32);
blur4Texture = makeTexture(32, 32);
renderTexture = makeTexture(1024, 1024);
screenTexture = createScreenTexture();
glBindFramebuffer(GL_FRAMEBUFFER, 0); // Unbind
}
Texture VisualiserComponent::makeTexture(int width, int height) {
@ -398,45 +402,48 @@ Texture VisualiserComponent::makeTexture(int width, int height) {
return { textureID, width, height };
}
void VisualiserComponent::drawLineTexture(const std::vector<float>& xPoints, const std::vector<float>& yPoints, const std::vector<float>& zPoints) {
void VisualiserComponent::drawLineTexture(const std::vector<float>& xP, const std::vector<float>& yP, const std::vector<float>& zP) {
using namespace juce::gl;
fadeAmount = juce::jmin(1.0, std::pow(0.5, settings.getPersistence()) * 0.4);
activateTargetTexture(lineTexture);
fade();
drawLine(xPoints, yPoints, zPoints);
drawLine(xP, yP, zP);
glBindTexture(GL_TEXTURE_2D, targetTexture.value().id);
}
void VisualiserComponent::saveTextureToFile(GLuint textureID, int width, int height, const juce::File& file) {
void VisualiserComponent::saveTextureToPNG(Texture texture, const juce::File& file) {
using namespace juce::gl;
GLuint textureID = texture.id;
int width = texture.width;
int height = texture.height;
// Bind the texture to read its data
glBindTexture(GL_TEXTURE_2D, textureID);
// Create a vector to store the pixel data (RGBA)
std::vector<unsigned char> pixels(width * height * 4);
std::vector<unsigned char> pixels(width * height * 8);
// Read the pixels from the texture
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
// Convert raw pixel data to JUCE Image
juce::Image image(juce::Image::PixelFormat::ARGB, width, height, true); // Create a JUCE image
juce::Image* image = new juce::Image (juce::Image::PixelFormat::ARGB, width, height, true); // Create a JUCE image
// Lock the image to get access to its pixel data
juce::Image::BitmapData bitmapData(image, juce::Image::BitmapData::writeOnly);
juce::Image::BitmapData bitmapData(*image, juce::Image::BitmapData::writeOnly);
// Copy the pixel data to the JUCE image (and swap R and B channels)
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
int srcIndex = (y * width + x) * 4; // RGBA format
juce::uint8 r = pixels[srcIndex]; // Red
juce::uint8 g = pixels[srcIndex + 1]; // Green
juce::uint8 b = pixels[srcIndex + 2]; // Blue
juce::uint8 a = pixels[srcIndex + 3]; // Alpha
juce::uint8 r = (pixels)[srcIndex]; // Red
juce::uint8 g = (pixels)[srcIndex + 1]; // Green
juce::uint8 b = (pixels)[srcIndex + 2]; // Blue
juce::uint8 a = (pixels)[srcIndex + 3]; // Alpha
// JUCE stores colors in ARGB, so we need to adjust the channel order
bitmapData.setPixelColour(x, y, juce::Colour(a, r, g, b));
// This method uses colors in RGBA
bitmapData.setPixelColour(x, height-y-1, juce::Colour(r, g, b, a));
}
}
@ -448,11 +455,29 @@ void VisualiserComponent::saveTextureToFile(GLuint textureID, int width, int hei
std::unique_ptr<juce::FileOutputStream> outputStream(file.createOutputStream());
if (outputStream != nullptr) {
outputStream->setPosition(0);
pngFormat.writeImageToStream(image, *outputStream);
pngFormat.writeImageToStream(*image, *outputStream);
outputStream->flush();
}
delete image;
}
void VisualiserComponent::saveTextureToQOI(Texture texture, const juce::File& file) {
using namespace juce::gl;
GLuint textureID = texture.id;
int width = texture.width;
int height = texture.height;
// Bind the texture to read its data
glBindTexture(GL_TEXTURE_2D, textureID);
if (pixels.size() < 1024 * 1024 * 4) pixels.resize(1024 * 1024 * 4);
// Read the pixels from the texture
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
std::vector<unsigned char> binaryData = qoixx::qoi::encode<std::vector<unsigned char>>(pixels, imageFormat);
file.replaceWithData(binaryData.data(), binaryData.size());
}
void VisualiserComponent::activateTargetTexture(std::optional<Texture> texture) {
using namespace juce::gl;
@ -504,7 +529,7 @@ void VisualiserComponent::drawTexture(std::optional<Texture> texture0, std::opti
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * fullScreenQuad.size(), fullScreenQuad.data(), GL_STATIC_DRAW);
glVertexAttribPointer(glGetAttribLocation(currentShader->getProgramID(), "aPos"), 2, GL_FLOAT, GL_FALSE, 0, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glDrawArrays(GL_TRIANGLES, 0, 6);
glDisableVertexAttribArray(glGetAttribLocation(currentShader->getProgramID(), "aPos"));
@ -525,18 +550,21 @@ void VisualiserComponent::setNormalBlending() {
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}
void VisualiserComponent::drawLine(const std::vector<float>& xPoints, const std::vector<float>& yPoints, const std::vector<float>& zPoints) {
void VisualiserComponent::drawLine(const std::vector<float>& xP, const std::vector<float>& yP, const std::vector<float>& zP) {
using namespace juce::gl;
setAdditiveBlending();
int nPoints = xPoints.size();
int nPoints = xP.size();
// Without this, there's an access violation that seems to occur only on some systems
if (scratchVertices.size() != nPoints * 12) scratchVertices.resize(nPoints * 12);
for (int i = 0; i < nPoints; ++i) {
int p = i * 12;
scratchVertices[p] = scratchVertices[p + 3] = scratchVertices[p + 6] = scratchVertices[p + 9] = xPoints[i];
scratchVertices[p + 1] = scratchVertices[p + 4] = scratchVertices[p + 7] = scratchVertices[p + 10] = yPoints[i];
scratchVertices[p + 2] = scratchVertices[p + 5] = scratchVertices[p + 8] = scratchVertices[p + 11] = zPoints[i];
scratchVertices[p] = scratchVertices[p + 3] = scratchVertices[p + 6] = scratchVertices[p + 9] = xP[i];
scratchVertices[p + 1] = scratchVertices[p + 4] = scratchVertices[p + 7] = scratchVertices[p + 10] = yP[i];
scratchVertices[p + 2] = scratchVertices[p + 5] = scratchVertices[p + 8] = scratchVertices[p + 11] = zP[i];
}
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
@ -561,7 +589,6 @@ void VisualiserComponent::drawLine(const std::vector<float>& xPoints, const std:
lineShader->setUniform("uGain", 450.0f / 512.0f);
lineShader->setUniform("uInvert", 1.0f);
float intensity = settings.getIntensity() * (41000.0f / sampleRate);
if (settings.getUpsamplingEnabled()) {
lineShader->setUniform("uIntensity", intensity);
} else {
@ -572,7 +599,7 @@ void VisualiserComponent::drawLine(const std::vector<float>& xPoints, const std:
lineShader->setUniform("uNEdges", (GLfloat) nEdges);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vertexIndexBuffer);
int nEdgesThisTime = xPoints.size() - 1;
int nEdgesThisTime = xP.size() - 1;
glDrawElements(GL_TRIANGLES, nEdgesThisTime * 6, GL_UNSIGNED_INT, 0);
glDisableVertexAttribArray(glGetAttribLocation(lineShader->getProgramID(), "aStart"));
@ -598,30 +625,31 @@ void VisualiserComponent::fade() {
}
void VisualiserComponent::drawCRT() {
using namespace juce::gl;
setNormalBlending();
activateTargetTexture(blur1Texture);
setShader(texturedShader.get());
texturedShader->setUniform("uResizeForCanvas", lineTexture.width / 1024.0f);
drawTexture(lineTexture);
//horizontal blur 256x256
activateTargetTexture(blur2Texture);
setShader(blurShader.get());
blurShader->setUniform("uOffset", 1.0f / 256.0f, 0.0f);
drawTexture(blur1Texture);
//vertical blur 256x256
activateTargetTexture(blur1Texture);
blurShader->setUniform("uOffset", 0.0f, 1.0f / 256.0f);
drawTexture(blur2Texture);
//preserve blur1 for later
activateTargetTexture(blur3Texture);
setShader(texturedShader.get());
texturedShader->setUniform("uResizeForCanvas", 1.0f);
drawTexture(blur1Texture);
//horizontal blur 64x64
activateTargetTexture(blur4Texture);
setShader(blurShader.get());
@ -632,19 +660,24 @@ void VisualiserComponent::drawCRT() {
activateTargetTexture(blur3Texture);
blurShader->setUniform("uOffset", -1.0f / 60.0f, 1.0f / 32.0f);
drawTexture(blur4Texture);
activateTargetTexture(std::nullopt);
activateTargetTexture(renderTexture);
setShader(outputShader.get());
float brightness = std::pow(2, settings.getBrightness() - 2);
outputShader->setUniform("uExposure", brightness);
outputShader->setUniform("uSaturation", (float) settings.getSaturation());
outputShader->setUniform("uNoise", (float) settings.getNoise());
outputShader->setUniform("uSaturation", (float)settings.getSaturation());
outputShader->setUniform("uNoise", (float)settings.getNoise());
outputShader->setUniform("uTime", time);
outputShader->setUniform("uGlow", (float) settings.getGlow());
outputShader->setUniform("uGlow", (float)settings.getGlow());
outputShader->setUniform("uResizeForCanvas", lineTexture.width / 1024.0f);
juce::Colour colour = juce::Colour::fromHSV(settings.getHue() / 360.0f, 1.0, 1.0, 1.0);
outputShader->setUniform("uColour", colour.getFloatRed(), colour.getFloatGreen(), colour.getFloatBlue());
activateTargetTexture(renderTexture);
drawTexture(lineTexture, blur1Texture, blur3Texture, screenTexture);
activateTargetTexture(std::nullopt);
setShader(texturedShader.get());
drawTexture(renderTexture);
}
Texture VisualiserComponent::createScreenTexture() {
@ -747,3 +780,111 @@ void VisualiserComponent::paint(juce::Graphics& g) {
g.drawText(text, viewportArea, juce::Justification::centred);
}
}
void VisualiserComponent::renderScope(const std::vector<float>& xp, const std::vector<float>& yp, const std::vector<float>& zp) {
if (graticuleEnabled != settings.getGraticuleEnabled() || smudgesEnabled != settings.getSmudgesEnabled()) {
graticuleEnabled = settings.getGraticuleEnabled();
smudgesEnabled = settings.getSmudgesEnabled();
screenTexture = createScreenTexture();
}
renderScale = (float)openGLContext.getRenderingScale();
drawLineTexture(xp, yp, zp);
checkGLErrors("drawLineTexture");
drawCRT();
checkGLErrors("drawCRT");
}
// sourceAudio must be a .wav file
int VisualiserComponent::renderAudioFile(juce::File& sourceAudio, int method, int width, int height) {
if (!sourceAudio.existsAsFile()) return 0;
if (sourceAudio.getFileExtension() != ".wav" && sourceAudio.getFileExtension() != ".flacI ") return -1;
using namespace juce::gl;
juce::AudioFormatManager manager;
manager.registerBasicFormats();
juce::AudioFormat *audioFormat = manager.getDefaultFormat();
juce::AudioFormatReader* reader = manager.createReaderFor(sourceAudio);
juce::AudioSampleBuffer buffer;
buffer.setSize(2, reader->lengthInSamples, false, false, false);
bool readSucceeded = reader->read(&buffer, 0, reader->lengthInSamples, 0, true, true);
if (!readSucceeded) return -2;
int fileChannels = buffer.getNumChannels();
int fileSamples = buffer.getNumSamples();
double fileSampleRate = reader->sampleRate;
sampleRate = fileSampleRate;
intensity = settings.getIntensity() * 41000.f / sampleRate;
oldSampleRate = fileSampleRate;
int frameNSamples = sampleRate / FRAME_RATE;
int frameNSamplesResampled = frameNSamples * RESAMPLE_RATIO;
bool resample = settings.parameters.upsamplingEnabled->getBoolValue();
if (resample) setupArrays(frameNSamplesResampled);
else setupArrays(frameNSamples);
xResampler.prepare(sampleRate, RESAMPLE_RATIO);
yResampler.prepare(sampleRate, RESAMPLE_RATIO);
zResampler.prepare(sampleRate, RESAMPLE_RATIO);
int nFrames = std::ceil(((float)fileSamples) / frameNSamples);
std::vector<float> fileXSamples(frameNSamples);
std::vector<float> fileYSamples(frameNSamples);
std::vector<float> fileZSamples(frameNSamples);
std::vector<float> frameXSamples(resample ? frameNSamplesResampled : frameNSamples);
std::vector<float> frameYSamples(resample ? frameNSamplesResampled : frameNSamples);
std::vector<float> frameZSamples(resample ? frameNSamplesResampled : frameNSamples);
std::string fileName;
juce::File destDir = sourceAudio.getParentDirectory().getChildFile("sosci export/");
int f;
for (f = 0; f < nFrames; f++) {
for (int s = 0; s < frameNSamples; s++) {
if (fileChannels > 0) (fileXSamples)[s] = (buffer.getSample(0, std::min(f * frameNSamples + s, fileSamples - 1)));
if (fileChannels > 1) (fileYSamples)[s] = -(buffer.getSample(1, std::min(f * frameNSamples + s, fileSamples - 1)));
else fileYSamples[s] = fileXSamples[s];
if (fileChannels > 2) (fileZSamples)[s] = (buffer.getSample(2, std::min(f * frameNSamples + s, fileSamples - 1)));
else fileZSamples[s] = 1;
}
if (resample) {
xResampler.process((fileXSamples).data(), (frameXSamples).data(), frameNSamples);
yResampler.process((fileYSamples).data(), (frameYSamples).data(), frameNSamples);
zResampler.process((fileZSamples).data(), (frameZSamples).data(), frameNSamples);
}
else {
for (int s = 0; s < frameNSamples; s++) {
(frameXSamples)[s] = (fileXSamples)[s];
(frameYSamples)[s] = (fileYSamples)[s];
(frameZSamples)[s] = (fileZSamples)[s];
}
}
renderScope(frameXSamples, frameYSamples, frameZSamples);
fileName = std::to_string(f);
fileName = std::string(std::max(0, (int)(6 - fileName.length())), '0') + fileName;
switch (method) {
case FILE_RENDER_DUMMY:
break;
case FILE_RENDER_PNG:
saveTextureToPNG(renderTexture, destDir.getChildFile(fileName + ".png"));
break;
case FILE_RENDER_QOI:
saveTextureToQOI(renderTexture, destDir.getChildFile(fileName + ".qoi"));
break;
};
time += 0.01f;
}
// cleanup
delete reader;
return f;
}

Wyświetl plik

@ -7,6 +7,11 @@
#include "../components/SvgButton.h"
#include "VisualiserSettings.h"
#include "../components/StopwatchComponent.h"
#include "../img/qoixx.hpp"
#define FILE_RENDER_DUMMY 0
#define FILE_RENDER_PNG 1
#define FILE_RENDER_QOI 2
enum class FullScreenMode {
TOGGLE,
@ -58,6 +63,7 @@ public:
std::function<void()> recordingHalted;
private:
float intensity;
const double FRAME_RATE = 60.0;
bool visualiserOnly;
@ -114,6 +120,7 @@ private:
Texture blur2Texture;
Texture blur3Texture;
Texture blur4Texture;
Texture renderTexture;
juce::OpenGLTexture screenOpenGLTexture;
juce::Image screenTextureImage = juce::ImageFileFormat::loadFrom(BinaryData::noise_jpg, BinaryData::noise_jpgSize);
juce::Image emptyScreenImage = juce::ImageFileFormat::loadFrom(BinaryData::empty_jpg, BinaryData::empty_jpgSize);
@ -142,7 +149,8 @@ private:
void setupArrays(int num_points);
void setupTextures();
void drawLineTexture(const std::vector<float>& xPoints, const std::vector<float>& yPoints, const std::vector<float>& zPoints);
void saveTextureToFile(GLuint textureID, int width, int height, const juce::File& file);
void saveTextureToPNG(Texture texture, const juce::File& file);
void saveTextureToQOI(Texture texture, const juce::File& file);
void activateTargetTexture(std::optional<Texture> texture);
void setShader(juce::OpenGLShaderProgram* program);
void drawTexture(std::optional<Texture> texture0, std::optional<Texture> texture1 = std::nullopt, std::optional<Texture> texture2 = std::nullopt, std::optional<Texture> texture3 = std::nullopt);
@ -153,8 +161,17 @@ private:
void drawCRT();
void checkGLErrors(const juce::String& location);
void viewportChanged(juce::Rectangle<int> area);
void renderScope(const std::vector<float>& xPoints, const std::vector<float>& yPoints, const std::vector<float>& zPoints);
int renderAudioFile(juce::File& sourceAudio, int method = 1, int width = 1024, int height = 1024);
Texture createScreenTexture();
juce::File audioFile;
std::vector<unsigned char> pixels;
const qoixx::qoi::desc imageFormat{ .width = 1024, .height = 1024, .channels = 4, .colorspace = qoixx::qoi::colorspace::srgb };
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(VisualiserComponent)
};