#include "../LookAndFeel.h" #include "VisualiserComponent.h" #include "../CommonPluginProcessor.h" #include "BlurFragmentShader.glsl" #include "BlurVertexShader.glsl" #include "WideBlurFragmentShader.glsl" #include "WideBlurVertexShader.glsl" #include "GlowFragmentShader.glsl" #include "GlowVertexShader.glsl" #include "LineFragmentShader.glsl" #include "LineVertexShader.glsl" #include "OutputFragmentShader.glsl" #include "OutputVertexShader.glsl" #include "SimpleFragmentShader.glsl" #include "SimpleVertexShader.glsl" #include "TexturedFragmentShader.glsl" #include "TexturedVertexShader.glsl" VisualiserComponent::VisualiserComponent( CommonAudioProcessor& processor, #if SOSCI_FEATURES SharedTextureManager& sharedTextureManager, #endif juce::File ffmpegFile, VisualiserSettings& settings, RecordingSettings& recordingSettings, VisualiserComponent* parent, bool visualiserOnly ) : audioProcessor(processor), ffmpegFile(ffmpegFile), #if SOSCI_FEATURES sharedTextureManager(sharedTextureManager), #endif settings(settings), recordingSettings(recordingSettings), visualiserOnly(visualiserOnly), AudioBackgroundThread("VisualiserComponent" + juce::String(parent != nullptr ? " Child" : ""), processor.threadManager), parent(parent) { #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(); }); }); }); }; #endif audioProcessor.haltRecording = [this] { setRecording(false); }; addAndMakeVisible(record); #if SOSCI_FEATURES record.setTooltip("Toggles recording of the oscilloscope's visuals and audio."); #else record.setTooltip("Toggles recording of the audio."); #endif record.setPulseAnimation(true); record.onClick = [this] { setRecording(record.getToggleState()); }; addAndMakeVisible(stopwatch); setMouseCursor(juce::MouseCursor::PointingHandCursor); setWantsKeyboardFocus(true); if (parent == nullptr) { addAndMakeVisible(fullScreenButton); fullScreenButton.setTooltip("Toggles fullscreen mode."); } if (child == nullptr && parent == nullptr && !visualiserOnly) { addAndMakeVisible(popOutButton); popOutButton.setTooltip("Opens the oscilloscope in a new window."); } addAndMakeVisible(settingsButton); settingsButton.setTooltip("Opens the visualiser settings window."); #if SOSCI_FEATURES addAndMakeVisible(sharedTextureButton); sharedTextureButton.setTooltip("Toggles sending the oscilloscope's visuals to a Syphon/Spout receiver."); sharedTextureButton.onClick = [this] { if (sharedTextureSender != nullptr) { openGLContext.executeOnGLThread([this](juce::OpenGLContext& context) { closeSharedTexture(); }, false); } else { openGLContext.executeOnGLThread([this](juce::OpenGLContext& context) { initialiseSharedTexture(); }, false); } }; #endif fullScreenButton.onClick = [this]() { enableFullScreen(); }; settingsButton.onClick = [this]() { if (openSettings != nullptr) { openSettings(); } }; popOutButton.onClick = [this]() { popoutWindow(); }; if (visualiserOnly && juce::JUCEApplication::isStandaloneApp()) { addAndMakeVisible(audioInputButton); audioInputButton.setTooltip("Appears red when audio input is being used. Click to enable audio input and close any open audio files."); audioInputButton.setClickingTogglesState(false); audioInputButton.setToggleState(!audioPlayer.isInitialised(), juce::NotificationType::dontSendNotification); audioPlayer.onParserChanged = [this] { juce::MessageManager::callAsync([this] { audioInputButton.setToggleState(!audioPlayer.isInitialised(), juce::NotificationType::dontSendNotification); }); }; audioInputButton.onClick = [this] { audioProcessor.stopAudioFile(); }; } addAndMakeVisible(audioPlayer); audioPlayer.addMouseListener(static_cast(this), true); openGLContext.setRenderer(this); openGLContext.attachTo(*this); setShouldBeRunning(true); } VisualiserComponent::~VisualiserComponent() { setRecording(false); if (parent == nullptr) { audioProcessor.haltRecording = nullptr; } openGLContext.detach(); setShouldBeRunning(false, [this] { renderingSemaphore.release(); }); } void VisualiserComponent::setFullScreen(bool fullScreen) { this->fullScreen = fullScreen; hideButtonRow = false; setMouseCursor(juce::MouseCursor::PointingHandCursor); resized(); } void VisualiserComponent::setFullScreenCallback(std::function callback) { fullScreenCallback = callback; } void VisualiserComponent::enableFullScreen() { if (fullScreenCallback) { fullScreenCallback(FullScreenMode::TOGGLE); } grabKeyboardFocus(); } void VisualiserComponent::mouseDoubleClick(const juce::MouseEvent& event) { if (event.originalComponent == this) { enableFullScreen(); } } void VisualiserComponent::runTask(const std::vector& points) { { juce::CriticalSection::ScopedLockType lock(samplesLock); // copy the points before applying effects audioOutputBuffer.setSize(2, points.size(), false, true, true); for (int i = 0; i < points.size(); ++i) { audioOutputBuffer.setSample(0, i, points[i].x); audioOutputBuffer.setSample(1, i, points[i].y); } xSamples.clear(); ySamples.clear(); zSamples.clear(); if (settings.isSweepEnabled()) { double sweepIncrement = getSweepIncrement(); long samplesPerSweep = sampleRate * settings.getSweepSeconds(); double triggerValue = settings.getTriggerValue(); bool belowTrigger = false; for (auto& point : points) { OsciPoint smoothPoint = settings.parameters.smoothEffect->apply(0, point); long samplePosition = sampleCount - lastTriggerPosition; double startPoint = 1.135; double sweep = samplePosition * sweepIncrement * 2 * startPoint - startPoint; double value = smoothPoint.x; if (sweep > startPoint && belowTrigger && value >= triggerValue) { lastTriggerPosition = sampleCount; } belowTrigger = value < triggerValue; xSamples.push_back(sweep); ySamples.push_back(value); zSamples.push_back(1); sampleCount++; } } else { for (OsciPoint point : points) { for (auto& effect : settings.parameters.audioEffects) { point = effect->apply(0, point); } #if SOSCI_FEATURES if (settings.isFlippedHorizontal()) { point.x = -point.x; } if (settings.isFlippedVertical()) { point.y = -point.y; } #endif xSamples.push_back(point.x); ySamples.push_back(point.y); zSamples.push_back(point.z); } } sampleBufferCount++; if (settings.parameters.upsamplingEnabled->getBoolValue()) { int newResampledSize = xSamples.size() * RESAMPLE_RATIO; smoothedXSamples.resize(newResampledSize); smoothedYSamples.resize(newResampledSize); smoothedZSamples.resize(newResampledSize); smoothedZSamples.resize(newResampledSize); if (settings.isSweepEnabled()) { // interpolate between sweep values to avoid any artifacts from quickly going from one sweep to the next for (int i = 0; i < newResampledSize; ++i) { int index = i / RESAMPLE_RATIO; if (index < xSamples.size() - 1) { double thisSample = xSamples[index]; double nextSample = xSamples[index + 1]; if (nextSample > thisSample) { smoothedXSamples[i] = xSamples[index] + (i % (int) RESAMPLE_RATIO) * (nextSample - thisSample) / RESAMPLE_RATIO; } else { smoothedXSamples[i] = xSamples[index]; } } else { smoothedXSamples[i] = xSamples[index]; } } } else { xResampler.process(xSamples.data(), smoothedXSamples.data(), xSamples.size()); } yResampler.process(ySamples.data(), smoothedYSamples.data(), ySamples.size()); zResampler.process(zSamples.data(), smoothedZSamples.data(), zSamples.size()); } } // this just triggers a repaint triggerAsyncUpdate(); // wait for rendering on the OpenGLRenderer thread to complete renderingSemaphore.acquire(); } int VisualiserComponent::prepareTask(double sampleRate, int bufferSize) { this->sampleRate = sampleRate; xResampler.prepare(sampleRate, RESAMPLE_RATIO); yResampler.prepare(sampleRate, RESAMPLE_RATIO); zResampler.prepare(sampleRate, RESAMPLE_RATIO); audioRecorder.setSampleRate(sampleRate); int desiredBufferSize = sampleRate / recordingSettings.getFrameRate(); return desiredBufferSize; } void VisualiserComponent::stopTask() { setRecording(false); renderingSemaphore.release(); } double VisualiserComponent::getSweepIncrement() { return 1.0 / (sampleRate * settings.getSweepSeconds()); } void VisualiserComponent::setPaused(bool paused) { active = !paused; setShouldBeRunning(active); renderingSemaphore.release(); audioPlayer.setPaused(paused); repaint(); } void VisualiserComponent::mouseDrag(const juce::MouseEvent& event) { timerId = -1; } void VisualiserComponent::mouseMove(const juce::MouseEvent& event) { if (event.getScreenX() == lastMouseX && event.getScreenY() == lastMouseY) { return; } hideButtonRow = false; setMouseCursor(juce::MouseCursor::PointingHandCursor); if (fullScreen) { if (!getScreenBounds().removeFromBottom(25).contains(event.getScreenX(), event.getScreenY()) && !event.mods.isLeftButtonDown()) { lastMouseX = event.getScreenX(); lastMouseY = event.getScreenY(); int newTimerId = juce::Random::getSystemRandom().nextInt(); timerId = newTimerId; auto pos = event.getScreenPosition(); auto parent = this->parent; juce::WeakReference weakRef = this; juce::Timer::callAfterDelay(1000, [this, weakRef, newTimerId, pos, parent]() { if (weakRef) { if (parent == nullptr || parent->child == this) { if (timerId == newTimerId && fullScreen) { hideButtonRow = true; setMouseCursor(juce::MouseCursor::NoCursor); resized(); } } } }); } resized(); } } void VisualiserComponent::mouseDown(const juce::MouseEvent& event) { if (event.originalComponent == this) { if (event.mods.isLeftButtonDown() && child == nullptr && !record.getToggleState()) { setPaused(active); } } } bool VisualiserComponent::keyPressed(const juce::KeyPress& key) { if (key.isKeyCode(juce::KeyPress::escapeKey)) { if (fullScreenCallback) { fullScreenCallback(FullScreenMode::MAIN_COMPONENT); } return true; } else if (key.isKeyCode(juce::KeyPress::spaceKey)) { setPaused(active); return true; } return false; } void VisualiserComponent::setRecording(bool recording) { stopwatch.stop(); stopwatch.reset(); #if SOSCI_FEATURES bool stillRecording = ffmpegProcess.isRunning() || audioRecorder.isRecording(); #else bool stillRecording = audioRecorder.isRecording(); #endif if (recording) { #if SOSCI_FEATURES recordingVideo = recordingSettings.recordingVideo(); recordingAudio = recordingSettings.recordingAudio(); if (!recordingVideo && !recordingAudio) { record.setToggleState(false, juce::NotificationType::dontSendNotification); return; } 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(); } }); record.setToggleState(false, juce::NotificationType::dontSendNotification); return; } tempVideoFile = std::make_unique(".mp4"); juce::String resolution = std::to_string(renderTexture.width) + "x" + std::to_string(renderTexture.height); juce::String cmd = "\"" + ffmpegFile.getFullPathName() + "\"" + " -r " + juce::String(recordingSettings.getFrameRate()) + " -f rawvideo" + " -pix_fmt rgba" + " -s " + resolution + " -i -" + " -threads 4" + " -preset " + recordingSettings.getCompressionPreset() + " -y" + " -pix_fmt yuv420p" + " -crf " + juce::String(recordingSettings.getCRF()) + #if JUCE_MAC #if JUCE_ARM // use software encoding on Apple Silicon " -c:v hevc_videotoolbox" + " -q:v " + juce::String(recordingSettings.getVideoToolboxQuality()) + " -tag:v hvc1" + #endif #endif " -vf vflip" + " \"" + tempVideoFile->getFile().getFullPathName() + "\""; ffmpegProcess.start(cmd); framePixels.resize(renderTexture.width * renderTexture.height * 4); } if (recordingAudio) { tempAudioFile = std::make_unique(".wav"); audioRecorder.startRecording(tempAudioFile->getFile()); } #else // audio only recording tempAudioFile = std::make_unique(".wav"); audioRecorder.startRecording(tempAudioFile->getFile()); #endif setPaused(false); stopwatch.start(); } else if (stillRecording) { #if SOSCI_FEATURES bool wasRecordingAudio = recordingAudio; bool wasRecordingVideo = recordingVideo; recordingAudio = false; recordingVideo = false; juce::String extension = wasRecordingVideo ? "mp4" : "wav"; if (wasRecordingAudio) { audioRecorder.stop(); } if (wasRecordingVideo) { ffmpegProcess.close(); } #else audioRecorder.stop(); juce::String extension = "wav"; #endif chooser = std::make_unique("Save recording", audioProcessor.lastOpenedDirectory, "*." + extension); auto flags = juce::FileBrowserComponent::saveMode | juce::FileBrowserComponent::canSelectFiles | juce::FileBrowserComponent::warnAboutOverwriting; #if SOSCI_FEATURES chooser->launchAsync(flags, [this, wasRecordingAudio, wasRecordingVideo](const juce::FileChooser& chooser) { auto file = chooser.getResult(); if (file != juce::File()) { if (wasRecordingAudio && wasRecordingVideo) { ffmpegProcess.start("\"" + ffmpegFile.getFullPathName() + "\" -i \"" + tempVideoFile->getFile().getFullPathName() + "\" -i \"" + tempAudioFile->getFile().getFullPathName() + "\" -c:v copy -c:a aac -b:a 384k -y \"" + file.getFullPathName() + "\""); ffmpegProcess.close(); } else if (wasRecordingAudio) { tempAudioFile->getFile().copyFileTo(file); } else if (wasRecordingVideo) { tempVideoFile->getFile().copyFileTo(file); } audioProcessor.lastOpenedDirectory = file.getParentDirectory(); } }); #else chooser->launchAsync(flags, [this](const juce::FileChooser& chooser) { auto file = chooser.getResult(); if (file != juce::File()) { tempAudioFile->getFile().copyFileTo(file); audioProcessor.lastOpenedDirectory = file.getParentDirectory(); } }); #endif } setBlockOnAudioThread(recording); #if SOSCI_FEATURES numFrames = 0; #endif record.setToggleState(recording, juce::NotificationType::dontSendNotification); resized(); } void VisualiserComponent::resized() { auto area = getLocalBounds(); if (fullScreen && hideButtonRow) { buttonRow = area.removeFromBottom(0); } else { buttonRow = area.removeFromBottom(25); } auto buttons = buttonRow; if (parent == nullptr) { fullScreenButton.setBounds(buttons.removeFromRight(30)); } if (child == nullptr && parent == nullptr && !visualiserOnly) { popOutButton.setBounds(buttons.removeFromRight(30)); } if (openSettings != nullptr) { settingsButton.setVisible(true); settingsButton.setBounds(buttons.removeFromRight(30)); } else { settingsButton.setVisible(false); } if (visualiserOnly && juce::JUCEApplication::isStandaloneApp()) { audioInputButton.setBounds(buttons.removeFromRight(30)); } #if SOSCI_FEATURES sharedTextureButton.setBounds(buttons.removeFromRight(30)); #endif record.setBounds(buttons.removeFromRight(25)); if (record.getToggleState()) { stopwatch.setVisible(true); stopwatch.setBounds(buttons.removeFromRight(100)); } else { stopwatch.setVisible(false); } #if SOSCI_FEATURES if (child == nullptr && downloading) { auto bounds = buttons.removeFromRight(160); ffmpegDownloader.setBounds(bounds.withSizeKeepingCentre(bounds.getWidth() - 10, bounds.getHeight() - 10)); } #endif buttons.removeFromRight(10); // padding audioPlayer.setBounds(buttons); viewportArea = area; viewportChanged(viewportArea); } void VisualiserComponent::popoutWindow() { #if SOSCI_FEATURES if (sharedTextureButton.getToggleState()) { sharedTextureButton.triggerClick(); } #endif setRecording(false); auto visualiser = new VisualiserComponent( audioProcessor, #if SOSCI_FEATURES sharedTextureManager, #endif ffmpegFile, settings, recordingSettings, this ); visualiser->settings.setLookAndFeel(&getLookAndFeel()); visualiser->openSettings = openSettings; visualiser->closeSettings = closeSettings; child = visualiser; childUpdated(); visualiser->setSize(300, 325); popout = std::make_unique("Software Oscilloscope", this); popout->setContentOwned(visualiser, true); popout->setUsingNativeTitleBar(true); popout->setResizable(true, false); popout->setVisible(true); popout->centreWithSize(300, 325); setPaused(true); resized(); } void VisualiserComponent::childUpdated() { popOutButton.setVisible(child == nullptr); #if SOSCI_FEATURES ffmpegDownloader.setVisible(child == nullptr); #endif record.setVisible(child == nullptr); if (child != nullptr) { audioProcessor.haltRecording = [this] { setRecording(false); child->setRecording(false); }; } else { audioProcessor.haltRecording = [this] { setRecording(false); }; } } #if SOSCI_FEATURES void VisualiserComponent::initialiseSharedTexture() { sharedTextureSender = sharedTextureManager.addSender(recordingSettings.getCustomSharedTextureServerName(), renderTexture.width, renderTexture.height); sharedTextureSender->initGL(); sharedTextureSender->setSharedTextureId(renderTexture.id); sharedTextureSender->setDrawFunction([this] { setShader(texturedShader.get()); drawTexture({renderTexture}); }); } void VisualiserComponent::closeSharedTexture() { if (sharedTextureSender != nullptr) { sharedTextureManager.removeSender(sharedTextureSender); sharedTextureSender = nullptr; } } #endif void VisualiserComponent::newOpenGLContextCreated() { using namespace juce::gl; juce::CriticalSection::ScopedLockType lock(samplesLock); glColorMask(true, true, true, true); viewportChanged(viewportArea); glEnable(GL_BLEND); glBlendEquation(GL_FUNC_ADD); fullScreenQuad = { -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, -1.0f }; simpleShader = std::make_unique(openGLContext); simpleShader->addVertexShader(juce::OpenGLHelpers::translateVertexShaderToV3(simpleVertexShader)); simpleShader->addFragmentShader(simpleFragmentShader); simpleShader->link(); lineShader = std::make_unique(openGLContext); lineShader->addVertexShader(juce::OpenGLHelpers::translateVertexShaderToV3(lineVertexShader)); lineShader->addFragmentShader(lineFragmentShader); lineShader->link(); outputShader = std::make_unique(openGLContext); outputShader->addVertexShader(juce::OpenGLHelpers::translateVertexShaderToV3(outputVertexShader)); outputShader->addFragmentShader(outputFragmentShader); outputShader->link(); texturedShader = std::make_unique(openGLContext); texturedShader->addVertexShader(juce::OpenGLHelpers::translateVertexShaderToV3(texturedVertexShader)); texturedShader->addFragmentShader(texturedFragmentShader); texturedShader->link(); blurShader = std::make_unique(openGLContext); blurShader->addVertexShader(juce::OpenGLHelpers::translateVertexShaderToV3(blurVertexShader)); blurShader->addFragmentShader(blurFragmentShader); blurShader->link(); wideBlurShader = std::make_unique(openGLContext); wideBlurShader->addVertexShader(juce::OpenGLHelpers::translateVertexShaderToV3(wideBlurVertexShader)); wideBlurShader->addFragmentShader(wideBlurFragmentShader); wideBlurShader->link(); #if SOSCI_FEATURES glowShader = std::make_unique(openGLContext); glowShader->addVertexShader(juce::OpenGLHelpers::translateVertexShaderToV3(glowVertexShader)); glowShader->addFragmentShader(glowFragmentShader); glowShader->link(); #endif glGenBuffers(1, &vertexBuffer); glGenBuffers(1, &quadIndexBuffer); glGenBuffers(1, &vertexIndexBuffer); setupTextures(); } void VisualiserComponent::openGLContextClosing() { using namespace juce::gl; #if SOSCI_FEATURES closeSharedTexture(); #endif glDeleteBuffers(1, &quadIndexBuffer); glDeleteBuffers(1, &vertexIndexBuffer); glDeleteBuffers(1, &vertexBuffer); glDeleteFramebuffers(1, &frameBuffer); glDeleteTextures(1, &lineTexture.id); glDeleteTextures(1, &blur1Texture.id); glDeleteTextures(1, &blur2Texture.id); glDeleteTextures(1, &blur3Texture.id); glDeleteTextures(1, &blur4Texture.id); glDeleteTextures(1, &renderTexture.id); screenOpenGLTexture.release(); #if SOSCI_FEATURES glDeleteTextures(1, &glowTexture.id); reflectionOpenGLTexture.release(); glowShader.reset(); #endif simpleShader.reset(); texturedShader.reset(); blurShader.reset(); wideBlurShader.reset(); lineShader.reset(); outputShader.reset(); // this triggers setupArrays to be called again when the scope next renders scratchVertices.clear(); } void VisualiserComponent::handleAsyncUpdate() { repaint(); } void VisualiserComponent::renderOpenGL() { using namespace juce::gl; if (openGLContext.isActive()) { juce::OpenGLHelpers::clear(juce::Colours::black); // we have a new buffer to render if (sampleBufferCount != prevSampleBufferCount) { prevSampleBufferCount = sampleBufferCount; if (!record.getToggleState()) { // don't change resolution or framerate if recording if (recordingSettings.getResolution() != renderTexture.width) { setResolution(recordingSettings.getResolution()); } if (recordingSettings.getFrameRate() != currentFrameRate) { currentFrameRate = recordingSettings.getFrameRate(); prepare(sampleRate, -1); setupArrays(RESAMPLE_RATIO * sampleRate / recordingSettings.getFrameRate()); } } juce::CriticalSection::ScopedLockType lock(samplesLock); if (settings.parameters.upsamplingEnabled->getBoolValue()) { renderScope(smoothedXSamples, smoothedYSamples, smoothedZSamples); } else { renderScope(xSamples, ySamples, zSamples); } #if SOSCI_FEATURES if (sharedTextureSender != nullptr) { sharedTextureSender->renderGL(); } #endif if (record.getToggleState()) { #if SOSCI_FEATURES if (recordingVideo) { // draw frame to ffmpeg glBindTexture(GL_TEXTURE_2D, renderTexture.id); glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, framePixels.data()); ffmpegProcess.write(framePixels.data(), 4 * renderTexture.width * renderTexture.height); } #endif if (recordingAudio) { audioRecorder.audioThreadCallback(audioOutputBuffer); } } renderingSemaphore.release(); stopwatch.addTime(juce::RelativeTime::seconds(1.0 / recordingSettings.getFrameRate())); } // render texture to screen activateTargetTexture(std::nullopt); setShader(texturedShader.get()); drawTexture({renderTexture}); } } void VisualiserComponent::viewportChanged(juce::Rectangle area) { using namespace juce::gl; if (openGLContext.isAttached()) { float realWidth = area.getWidth() * renderScale; float realHeight = area.getHeight() * renderScale; float xOffset = getWidth() * renderScale - realWidth; float yOffset = getHeight() * renderScale - realHeight; float minDim = juce::jmin(realWidth, realHeight); float x = (realWidth - minDim) / 2 + area.getX() * renderScale + xOffset; float y = (realHeight - minDim) / 2 - area.getY() * renderScale + yOffset; glViewport(juce::roundToInt(x), juce::roundToInt(y), juce::roundToInt(minDim), juce::roundToInt(minDim)); } } void VisualiserComponent::setupArrays(int nPoints) { using namespace juce::gl; if (nPoints == 0) { return; } nEdges = nPoints - 1; std::vector indices(4 * nEdges); for (size_t i = 0; i < indices.size(); ++i) { indices[i] = static_cast(i); } glBindBuffer(GL_ARRAY_BUFFER, quadIndexBuffer); glBufferData(GL_ARRAY_BUFFER, indices.size() * sizeof(float), indices.data(), GL_STATIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0); // Unbind int len = nEdges * 2 * 3; std::vector vertexIndices(len); for (int i = 0, pos = 0; i < len;) { vertexIndices[i++] = pos; vertexIndices[i++] = pos + 2; vertexIndices[i++] = pos + 1; vertexIndices[i++] = pos + 1; vertexIndices[i++] = pos + 2; vertexIndices[i++] = pos + 3; pos += 4; } glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vertexIndexBuffer); glBufferData(GL_ELEMENT_ARRAY_BUFFER, vertexIndices.size() * sizeof(uint32_t), vertexIndices.data(), GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); // Unbind // Initialize scratch vertices scratchVertices.resize(12 * nPoints); } void VisualiserComponent::setupTextures() { using namespace juce::gl; // Create the framebuffer glGenFramebuffers(1, &frameBuffer); glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer); // Create textures lineTexture = makeTexture(recordingSettings.getResolution(), recordingSettings.getResolution()); blur1Texture = makeTexture(512, 512); blur2Texture = makeTexture(512, 512); blur3Texture = makeTexture(128, 128); blur4Texture = makeTexture(128, 128); renderTexture = makeTexture(recordingSettings.getResolution(), recordingSettings.getResolution()); screenOpenGLTexture.loadImage(emptyScreenImage); screenTexture = { screenOpenGLTexture.getTextureID(), screenTextureImage.getWidth(), screenTextureImage.getHeight() }; #if SOSCI_FEATURES glowTexture = makeTexture(512, 512); reflectionTexture = createReflectionTexture(); #endif glBindFramebuffer(GL_FRAMEBUFFER, 0); // Unbind } Texture VisualiserComponent::makeTexture(int width, int height, GLuint textureID) { using namespace juce::gl; // replace existing texture if it exists, otherwise create new texture if (textureID == 0) { glGenTextures(1, &textureID); } glBindTexture(GL_TEXTURE_2D, textureID); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, nullptr); // Set texture filtering and wrapping glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); float borderColor[] = { 0.0f, 0.0f, 0.0f, 1.0f }; glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor); glBindTexture(GL_TEXTURE_2D, 0); // Unbind return { textureID, width, height }; } void VisualiserComponent::setResolution(int width) { using namespace juce::gl; lineTexture = makeTexture(width, width, lineTexture.id); renderTexture = makeTexture(width, width, renderTexture.id); } void VisualiserComponent::drawLineTexture(const std::vector& xPoints, const std::vector& yPoints, const std::vector& zPoints) { using namespace juce::gl; double persistence = std::pow(0.5, settings.getPersistence()) * 0.4; persistence *= 60.0 / recordingSettings.getFrameRate(); fadeAmount = juce::jmin(1.0, persistence); activateTargetTexture(lineTexture); fade(); drawLine(xPoints, yPoints, zPoints); glBindTexture(GL_TEXTURE_2D, targetTexture.value().id); } 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); std::vector pixels = std::vector(width * height * 4); // Read the pixels from the texture glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); juce::Image image = juce::Image (juce::Image::PixelFormat::ARGB, width, height, true); 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 // This method uses colors in RGBA bitmapData.setPixelColour(x, height-y-1, juce::Colour(r, g, b, a)); } } glBindTexture(GL_TEXTURE_2D, 0); // Save the JUCE image to file (PNG in this case) juce::PNGImageFormat pngFormat; std::unique_ptr outputStream(file.createOutputStream()); if (outputStream != nullptr) { outputStream->setPosition(0); pngFormat.writeImageToStream(image, *outputStream); outputStream->flush(); } } 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); std::vector pixels = std::vector(width * height * 4); // Read the pixels from the texture glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); const qoixx::qoi::desc imageFormat{ .width = (uint32_t) width, .height = (uint32_t) height, .channels = 4, .colorspace = qoixx::qoi::colorspace::srgb }; std::vector binaryData = qoixx::qoi::encode>(pixels, imageFormat); file.replaceWithData(binaryData.data(), binaryData.size()); } void VisualiserComponent::activateTargetTexture(std::optional texture) { using namespace juce::gl; if (texture.has_value()) { glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture.value().id, 0); glViewport(0, 0, texture.value().width, texture.value().height); } else { glBindFramebuffer(GL_FRAMEBUFFER, 0); viewportChanged(viewportArea); } targetTexture = texture; } void VisualiserComponent::setShader(juce::OpenGLShaderProgram* program) { currentShader = program; program->use(); } void VisualiserComponent::drawTexture(std::vector> textures) { using namespace juce::gl; glEnableVertexAttribArray(glGetAttribLocation(currentShader->getProgramID(), "aPos")); for (int i = 0; i < textures.size(); ++i) { if (textures[i].has_value()) { glActiveTexture(GL_TEXTURE0 + i); glBindTexture(GL_TEXTURE_2D, textures[i].value().id); currentShader->setUniform(("uTexture" + juce::String(i)).toStdString().c_str(), i); } } glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); 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")); if (targetTexture.has_value()) { glBindTexture(GL_TEXTURE_2D, targetTexture.value().id); } } void VisualiserComponent::setAdditiveBlending() { using namespace juce::gl; glBlendFunc(GL_ONE, GL_ONE); } void VisualiserComponent::setNormalBlending() { using namespace juce::gl; glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } void VisualiserComponent::drawLine(const std::vector& xPoints, const std::vector& yPoints, const std::vector& zPoints) { using namespace juce::gl; setAdditiveBlending(); int nPoints = xPoints.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]; } glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); glBufferData(GL_ARRAY_BUFFER, nPoints * 12 * sizeof(float), scratchVertices.data(), GL_STATIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0); lineShader->use(); glEnableVertexAttribArray(glGetAttribLocation(lineShader->getProgramID(), "aStart")); glEnableVertexAttribArray(glGetAttribLocation(lineShader->getProgramID(), "aEnd")); glEnableVertexAttribArray(glGetAttribLocation(lineShader->getProgramID(), "aIdx")); glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); glVertexAttribPointer(glGetAttribLocation(lineShader->getProgramID(), "aStart"), 3, GL_FLOAT, GL_FALSE, 0, 0); glVertexAttribPointer(glGetAttribLocation(lineShader->getProgramID(), "aEnd"), 3, GL_FLOAT, GL_FALSE, 0, (void*)(12 * sizeof(float))); glBindBuffer(GL_ARRAY_BUFFER, quadIndexBuffer); glVertexAttribPointer(glGetAttribLocation(lineShader->getProgramID(), "aIdx"), 1, GL_FLOAT, GL_FALSE, 0, 0); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, screenTexture.id); lineShader->setUniform("uScreen", 0); lineShader->setUniform("uSize", (GLfloat) settings.getFocus()); lineShader->setUniform("uGain", 450.0f / 512.0f); lineShader->setUniform("uInvert", 1.0f); if (settings.getUpsamplingEnabled()) { lineShader->setUniform("uIntensity", intensity); } else { lineShader->setUniform("uIntensity", (GLfloat) (intensity * RESAMPLE_RATIO * 1.5)); } lineShader->setUniform("uFadeAmount", fadeAmount); lineShader->setUniform("uNEdges", (GLfloat) nEdges); setOffsetAndScale(lineShader.get()); #if SOSCI_FEATURES lineShader->setUniform("uScreenOverlay", (GLfloat) screenOverlay); lineShader->setUniform("uFishEye", screenOverlay == ScreenOverlay::VectorDisplay ? VECTOR_DISPLAY_FISH_EYE : 0.0f); #endif glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vertexIndexBuffer); int nEdgesThisTime = xPoints.size() - 1; glDrawElements(GL_TRIANGLES, nEdgesThisTime * 6, GL_UNSIGNED_INT, 0); glDisableVertexAttribArray(glGetAttribLocation(lineShader->getProgramID(), "aStart")); glDisableVertexAttribArray(glGetAttribLocation(lineShader->getProgramID(), "aEnd")); glDisableVertexAttribArray(glGetAttribLocation(lineShader->getProgramID(), "aIdx")); } void VisualiserComponent::fade() { using namespace juce::gl; setNormalBlending(); simpleShader->use(); glEnableVertexAttribArray(glGetAttribLocation(simpleShader->getProgramID(), "vertexPosition")); glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(float) * fullScreenQuad.size(), fullScreenQuad.data(), GL_STATIC_DRAW); glVertexAttribPointer(glGetAttribLocation(simpleShader->getProgramID(), "vertexPosition"), 2, GL_FLOAT, GL_FALSE, 0, 0); glBindBuffer(GL_ARRAY_BUFFER, 0); simpleShader->setUniform("colour", 0.0f, 0.0f, 0.0f, fadeAmount); glDrawArrays(GL_TRIANGLES, 0, 6); glDisableVertexAttribArray(glGetAttribLocation(simpleShader->getProgramID(), "vertexPosition")); } void VisualiserComponent::drawCRT() { using namespace juce::gl; setNormalBlending(); activateTargetTexture(blur1Texture); setShader(texturedShader.get()); texturedShader->setUniform("uResizeForCanvas", lineTexture.width / (float) recordingSettings.getResolution()); drawTexture({lineTexture}); //horizontal blur 512x512 activateTargetTexture(blur2Texture); setShader(blurShader.get()); blurShader->setUniform("uOffset", 1.0f / 512.0f, 0.0f); drawTexture({blur1Texture}); //vertical blur 512x512 activateTargetTexture(blur1Texture); blurShader->setUniform("uOffset", 0.0f, 1.0f / 512.0f); drawTexture({blur2Texture}); //preserve blur1 for later activateTargetTexture(blur3Texture); setShader(texturedShader.get()); texturedShader->setUniform("uResizeForCanvas", 1.0f); drawTexture({blur1Texture}); //horizontal blur 128x128 activateTargetTexture(blur4Texture); setShader(wideBlurShader.get()); wideBlurShader->setUniform("uOffset", 1.0f / 128.0f, 0.0f); drawTexture({blur3Texture}); //vertical blur 128x128 activateTargetTexture(blur3Texture); wideBlurShader->setUniform("uOffset", 0.0f, 1.0f / 128.0f); drawTexture({blur4Texture}); #if SOSCI_FEATURES if (settings.parameters.screenOverlay->isRealisticDisplay()) { // create glow texture activateTargetTexture(glowTexture); setShader(glowShader.get()); setOffsetAndScale(glowShader.get()); drawTexture({blur3Texture}); } #endif activateTargetTexture(renderTexture); setShader(outputShader.get()); outputShader->setUniform("uExposure", 0.25f); outputShader->setUniform("uLineSaturation", (float) settings.getLineSaturation()); #if SOSCI_FEATURES outputShader->setUniform("uScreenSaturation", (float) settings.getScreenSaturation()); #else outputShader->setUniform("uScreenSaturation", 1.0f); #endif outputShader->setUniform("uNoise", (float) settings.getNoise()); outputShader->setUniform("uRandom", juce::Random::getSystemRandom().nextFloat()); outputShader->setUniform("uGlow", (float) settings.getGlow()); outputShader->setUniform("uAmbient", (float) settings.getAmbient()); setOffsetAndScale(outputShader.get()); #if SOSCI_FEATURES outputShader->setUniform("uFishEye", screenOverlay == ScreenOverlay::VectorDisplay ? VECTOR_DISPLAY_FISH_EYE : 0.0f); outputShader->setUniform("uRealScreen", settings.parameters.screenOverlay->isRealisticDisplay() ? 1.0f : 0.0f); #endif outputShader->setUniform("uResizeForCanvas", lineTexture.width / (float) recordingSettings.getResolution()); 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()); drawTexture({ lineTexture, blur1Texture, blur3Texture, screenTexture, #if SOSCI_FEATURES reflectionTexture, glowTexture, #endif }); } void VisualiserComponent::setOffsetAndScale(juce::OpenGLShaderProgram* shader) { OsciPoint offset; OsciPoint scale = { 1.0f }; #if SOSCI_FEATURES if (settings.getScreenOverlay() == ScreenOverlay::Real) { offset = REAL_SCREEN_OFFSET; scale = REAL_SCREEN_SCALE; } else if (settings.getScreenOverlay() == ScreenOverlay::VectorDisplay) { offset = VECTOR_DISPLAY_OFFSET; scale = VECTOR_DISPLAY_SCALE; } #endif shader->setUniform("uOffset", (float) offset.x, (float) offset.y); shader->setUniform("uScale", (float) scale.x, (float) scale.y); } #if SOSCI_FEATURES Texture VisualiserComponent::createReflectionTexture() { using namespace juce::gl; if (settings.getScreenOverlay() == ScreenOverlay::VectorDisplay) { reflectionOpenGLTexture.loadImage(vectorDisplayReflectionImage); } else if (settings.getScreenOverlay() == ScreenOverlay::Real) { reflectionOpenGLTexture.loadImage(oscilloscopeReflectionImage); } else { reflectionOpenGLTexture.loadImage(emptyReflectionImage); } Texture texture = { reflectionOpenGLTexture.getTextureID(), reflectionOpenGLTexture.getWidth(), reflectionOpenGLTexture.getHeight() }; return texture; } #endif Texture VisualiserComponent::createScreenTexture() { using namespace juce::gl; if (screenOverlay == ScreenOverlay::Smudged || screenOverlay == ScreenOverlay::SmudgedGraticule) { screenOpenGLTexture.loadImage(screenTextureImage); #if SOSCI_FEATURES } else if (screenOverlay == ScreenOverlay::Real) { screenOpenGLTexture.loadImage(oscilloscopeImage); } else if (screenOverlay == ScreenOverlay::VectorDisplay) { screenOpenGLTexture.loadImage(vectorDisplayImage); #endif } else { screenOpenGLTexture.loadImage(emptyScreenImage); } checkGLErrors(__FILE__, __LINE__); Texture texture = { screenOpenGLTexture.getTextureID(), screenTextureImage.getWidth(), screenTextureImage.getHeight() }; if (screenOverlay == ScreenOverlay::Graticule || screenOverlay == ScreenOverlay::SmudgedGraticule) { activateTargetTexture(texture); checkGLErrors(__FILE__, __LINE__); setNormalBlending(); checkGLErrors(__FILE__, __LINE__); setShader(simpleShader.get()); checkGLErrors(__FILE__, __LINE__); glColorMask(true, false, false, true); std::vector data; int step = 45; for (int i = 0; i < 11; i++) { float s = i * step; // Inserting at the beginning of the vector (equivalent to splice(0,0,...)) data.insert(data.begin(), {0, s, 10.0f * step, s}); data.insert(data.begin(), {s, 0, s, 10.0f * step}); if (i != 0 && i != 10) { for (int j = 0; j < 51; j++) { float t = j * step / 5; if (i != 5) { data.insert(data.begin(), {t, s - 2, t, s + 1}); data.insert(data.begin(), {s - 2, t, s + 1, t}); } else { data.insert(data.begin(), {t, s - 5, t, s + 4}); data.insert(data.begin(), {s - 5, t, s + 4, t}); } } } } for (int j = 0; j < 51; j++) { float t = j * step / 5; if (static_cast(t) % 5 == 0) continue; data.insert(data.begin(), {t - 2, 2.5f * step, t + 2, 2.5f * step}); data.insert(data.begin(), {t - 2, 7.5f * step, t + 2, 7.5f * step}); } // Normalize the data for (size_t i = 0; i < data.size(); i++) { data[i] = (data[i] + 31.0f) / 256.0f - 1; } glEnableVertexAttribArray(glGetAttribLocation(simpleShader->getProgramID(), "vertexPosition")); glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); glBufferData(GL_ARRAY_BUFFER, sizeof(float) * data.size(), data.data(), GL_STATIC_DRAW); glVertexAttribPointer(glGetAttribLocation(simpleShader->getProgramID(), "vertexPosition"), 2, GL_FLOAT, GL_FALSE, 0, nullptr); glBindBuffer(GL_ARRAY_BUFFER, 0); simpleShader->setUniform("colour", 0.01f, 0.05f, 0.01f, 1.0f); glLineWidth(4.0f); glDrawArrays(GL_LINES, 0, data.size() / 2); glBindTexture(GL_TEXTURE_2D, targetTexture.value().id); glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); } return texture; } void VisualiserComponent::checkGLErrors(juce::String file, int line) { using namespace juce::gl; GLenum error; while ((error = glGetError()) != GL_NO_ERROR) { juce::String errorMessage; switch (error) { case GL_INVALID_ENUM: errorMessage = "GL_INVALID_ENUM"; break; case GL_INVALID_VALUE: errorMessage = "GL_INVALID_VALUE"; break; case GL_INVALID_OPERATION: errorMessage = "GL_INVALID_OPERATION"; break; case GL_STACK_OVERFLOW: errorMessage = "GL_STACK_OVERFLOW"; break; case GL_STACK_UNDERFLOW: errorMessage = "GL_STACK_UNDERFLOW"; break; case GL_OUT_OF_MEMORY: errorMessage = "GL_OUT_OF_MEMORY"; break; case GL_INVALID_FRAMEBUFFER_OPERATION: errorMessage = "GL_INVALID_FRAMEBUFFER_OPERATION"; break; default: errorMessage = "Unknown OpenGL error"; break; } DBG("OpenGL error at " + file + ":" + juce::String(line) + " - " + errorMessage); } } void VisualiserComponent::paint(juce::Graphics& g) { g.setColour(Colours::veryDark); g.fillRect(buttonRow); if (!active) { // draw a translucent overlay g.setColour(juce::Colours::black.withAlpha(0.5f)); g.fillRect(viewportArea); g.setColour(juce::Colours::white); g.setFont(30.0f); juce::String text = child == nullptr ? "Paused" : "Open in another window"; g.drawText(text, viewportArea, juce::Justification::centred); } } void VisualiserComponent::renderScope(const std::vector& xPoints, const std::vector& yPoints, const std::vector& zPoints) { if (screenOverlay != settings.getScreenOverlay()) { screenOverlay = settings.getScreenOverlay(); #if SOSCI_FEATURES reflectionTexture = createReflectionTexture(); #endif screenTexture = createScreenTexture(); } if (sampleRate != oldSampleRate || scratchVertices.empty()) { oldSampleRate = sampleRate; setupArrays(RESAMPLE_RATIO * sampleRate / recordingSettings.getFrameRate()); } intensity = settings.getIntensity() * (41000.0f / sampleRate); renderScale = (float)openGLContext.getRenderingScale(); drawLineTexture(xPoints, yPoints, zPoints); checkGLErrors(__FILE__, __LINE__); drawCRT(); checkGLErrors(__FILE__, __LINE__); }