diff --git a/Resources/oscilloscope/empty.jpg b/Resources/oscilloscope/empty.jpg index 51952db..49d2e17 100644 Binary files a/Resources/oscilloscope/empty.jpg and b/Resources/oscilloscope/empty.jpg differ diff --git a/Resources/oscilloscope/no_reflection.jpg b/Resources/oscilloscope/no_reflection.jpg index e83a073..c7904e2 100644 Binary files a/Resources/oscilloscope/no_reflection.jpg and b/Resources/oscilloscope/no_reflection.jpg differ diff --git a/Resources/oscilloscope/noise.jpg b/Resources/oscilloscope/noise.jpg index 7df423f..ef00989 100644 Binary files a/Resources/oscilloscope/noise.jpg and b/Resources/oscilloscope/noise.jpg differ diff --git a/Resources/oscilloscope/real.jpg b/Resources/oscilloscope/real.jpg deleted file mode 100644 index b961a43..0000000 Binary files a/Resources/oscilloscope/real.jpg and /dev/null differ diff --git a/Resources/oscilloscope/real.png b/Resources/oscilloscope/real.png new file mode 100644 index 0000000..f7cf7af Binary files /dev/null and b/Resources/oscilloscope/real.png differ diff --git a/Resources/oscilloscope/real_reflection.jpg b/Resources/oscilloscope/real_reflection.jpg deleted file mode 100644 index b0199ef..0000000 Binary files a/Resources/oscilloscope/real_reflection.jpg and /dev/null differ diff --git a/Resources/oscilloscope/real_reflection.png b/Resources/oscilloscope/real_reflection.png new file mode 100644 index 0000000..97144aa Binary files /dev/null and b/Resources/oscilloscope/real_reflection.png differ diff --git a/Resources/oscilloscope/vector_display.jpg b/Resources/oscilloscope/vector_display.jpg deleted file mode 100644 index 12f1c6a..0000000 Binary files a/Resources/oscilloscope/vector_display.jpg and /dev/null differ diff --git a/Resources/oscilloscope/vector_display.png b/Resources/oscilloscope/vector_display.png new file mode 100644 index 0000000..7be1c01 Binary files /dev/null and b/Resources/oscilloscope/vector_display.png differ diff --git a/Resources/oscilloscope/vector_display_reflection.jpg b/Resources/oscilloscope/vector_display_reflection.jpg deleted file mode 100644 index 66028f4..0000000 Binary files a/Resources/oscilloscope/vector_display_reflection.jpg and /dev/null differ diff --git a/Resources/oscilloscope/vector_display_reflection.png b/Resources/oscilloscope/vector_display_reflection.png new file mode 100644 index 0000000..15e3b46 Binary files /dev/null and b/Resources/oscilloscope/vector_display_reflection.png differ diff --git a/Source/CommonPluginEditor.cpp b/Source/CommonPluginEditor.cpp index dbac88c..b1c717a 100644 --- a/Source/CommonPluginEditor.cpp +++ b/Source/CommonPluginEditor.cpp @@ -43,8 +43,8 @@ CommonPluginEditor::CommonPluginEditor(CommonAudioProcessor& p, juce::String app visualiserSettings.setColour(juce::ResizableWindow::backgroundColourId, Colours::dark); recordingSettings.setLookAndFeel(&getLookAndFeel()); - recordingSettings.setSize(350, 230); - recordingSettingsWindow.centreWithSize(350, 260); + recordingSettings.setSize(350, 280); + recordingSettingsWindow.centreWithSize(350, 320); #if JUCE_WINDOWS // if not standalone, use native title bar for compatibility with DAWs recordingSettingsWindow.setUsingNativeTitleBar(processor.wrapperType == juce::AudioProcessor::WrapperType::wrapperType_Standalone); diff --git a/Source/components/EffectComponent.cpp b/Source/components/EffectComponent.cpp index c629482..0d900dd 100644 --- a/Source/components/EffectComponent.cpp +++ b/Source/components/EffectComponent.cpp @@ -17,7 +17,11 @@ EffectComponent::EffectComponent(Effect& effect, int index) : effect(effect), in slider.setSliderStyle(juce::Slider::LinearHorizontal); slider.setTextBoxStyle(juce::Slider::TextBoxRight, false, TEXT_BOX_WIDTH, slider.getTextBoxHeight()); - slider.setNumDecimalPlacesToDisplay(4); + if (effect.parameters[index]->step == 1.0) { + slider.setNumDecimalPlacesToDisplay(0); + } else { + slider.setNumDecimalPlacesToDisplay(4); + } lfoSlider.setSliderStyle(juce::Slider::LinearHorizontal); lfoSlider.setTextBoxStyle(juce::Slider::TextBoxRight, false, TEXT_BOX_WIDTH, lfoSlider.getTextBoxHeight()); diff --git a/Source/concurrency/AudioBackgroundThread.cpp b/Source/concurrency/AudioBackgroundThread.cpp index b8897d2..d82ea11 100644 --- a/Source/concurrency/AudioBackgroundThread.cpp +++ b/Source/concurrency/AudioBackgroundThread.cpp @@ -15,18 +15,15 @@ AudioBackgroundThread::~AudioBackgroundThread() { } void AudioBackgroundThread::prepare(double sampleRate, int samplesPerBlock) { - if (isThreadRunning()) { - stop(); - } + bool threadShouldBeRunning = shouldBeRunning; + setShouldBeRunning(false); isPrepared = false; int requestedDataSize = prepareTask(sampleRate, samplesPerBlock); consumer = std::make_unique(requestedDataSize); isPrepared = true; - if (shouldBeRunning) { - start(); - } + setShouldBeRunning(threadShouldBeRunning); } void AudioBackgroundThread::setShouldBeRunning(bool shouldBeRunning, std::function stopCallback) { diff --git a/Source/visualiser/OutputFragmentShader.glsl b/Source/visualiser/OutputFragmentShader.glsl index c362b5d..f901c17 100644 --- a/Source/visualiser/OutputFragmentShader.glsl +++ b/Source/visualiser/OutputFragmentShader.glsl @@ -58,7 +58,7 @@ void main() { if (uRealScreen > 0.5) { vec4 reflection = texture2D(uTexture4, vTexCoord); vec4 screenGlow = texture2D(uTexture5, vTexCoord); - scatter += max4(screenGlow * reflection * max(1.0 - uAmbient, 0.0), vec4(0.0)); + scatter += max4(screenGlow * reflection * max(1.0 - 0.5 * uAmbient, 0.0), vec4(0.0)); } float light = line.r + uGlow * 1.5 * screen.g * screen.g * tightGlow.r; diff --git a/Source/visualiser/RecordingSettings.cpp b/Source/visualiser/RecordingSettings.cpp index 1df5fc9..f2cbfdb 100644 --- a/Source/visualiser/RecordingSettings.cpp +++ b/Source/visualiser/RecordingSettings.cpp @@ -6,6 +6,8 @@ RecordingSettings::RecordingSettings(RecordingParameters& ps) : parameters(ps) { #if SOSCI_FEATURES addAndMakeVisible(quality); + addAndMakeVisible(resolution); + addAndMakeVisible(frameRate); addAndMakeVisible(losslessVideo); addAndMakeVisible(recordAudio); addAndMakeVisible(recordVideo); @@ -16,6 +18,11 @@ RecordingSettings::RecordingSettings(RecordingParameters& ps) : parameters(ps) { quality.setSliderOnValueChange(); quality.setRangeEnabled(false); + resolution.setSliderOnValueChange(); + resolution.setRangeEnabled(false); + frameRate.setSliderOnValueChange(); + frameRate.setRangeEnabled(false); + recordAudio.onClick = [this] { if (!recordAudio.getToggleState() && !recordVideo.getToggleState()) { recordVideo.setToggleState(true, juce::NotificationType::sendNotification); @@ -66,6 +73,8 @@ void RecordingSettings::resized() { #if SOSCI_FEATURES losslessVideo.setBounds(area.removeFromTop(rowHeight)); quality.setBounds(area.removeFromTop(rowHeight).expanded(6, 0)); + resolution.setBounds(area.removeFromTop(rowHeight).expanded(6, 0)); + frameRate.setBounds(area.removeFromTop(rowHeight).expanded(6, 0)); recordAudio.setBounds(area.removeFromTop(rowHeight)); recordVideo.setBounds(area.removeFromTop(rowHeight)); auto row = area.removeFromTop(rowHeight); diff --git a/Source/visualiser/RecordingSettings.h b/Source/visualiser/RecordingSettings.h index 9919c48..8a71442 100644 --- a/Source/visualiser/RecordingSettings.h +++ b/Source/visualiser/RecordingSettings.h @@ -13,6 +13,10 @@ public: RecordingParameters() { qualityParameter.disableLfo(); qualityParameter.disableSidechain(); + resolution.disableLfo(); + resolution.disableSidechain(); + frameRate.disableLfo(); + frameRate.disableSidechain(); } private: @@ -36,6 +40,22 @@ public: BooleanParameter recordAudio = BooleanParameter("Record Audio", "recordAudio", VERSION_HINT, true, "Record audio along with the video."); BooleanParameter recordVideo = BooleanParameter("Record Video", "recordVideo", VERSION_HINT, sosciFeatures, "Record video output of the visualiser."); + + EffectParameter resolution = EffectParameter( + "Resolution", + "The resolution of the recorded video. This only changes when not recording.", + "resolution", + VERSION_HINT, 1024, 128, 2048, 1.0 + ); + Effect resolutionEffect = Effect(&resolution); + + EffectParameter frameRate = EffectParameter( + "Frame Rate", + "The frame rate of the recorded video. This only changes when not recording.", + "frameRate", + VERSION_HINT, 60.0, 10, 240, 0.01 + ); + Effect frameRateEffect = Effect(&frameRate); juce::String compressionPreset = "fast"; @@ -49,6 +69,12 @@ public: auto qualityXml = settingsXml->createNewChildElement("quality"); qualityEffect.save(qualityXml); + + auto resolutionXml = settingsXml->createNewChildElement("resolution"); + resolutionEffect.save(resolutionXml); + + auto frameRateXml = settingsXml->createNewChildElement("frameRate"); + frameRateEffect.save(frameRateXml); } // opt to not change any values if not found @@ -72,6 +98,12 @@ public: if (auto* qualityXml = settingsXml->getChildByName("quality")) { qualityEffect.load(qualityXml); } + if (auto* resolutionXml = settingsXml->getChildByName("resolution")) { + resolutionEffect.load(resolutionXml); + } + if (auto* frameRateXml = settingsXml->getChildByName("frameRate")) { + frameRateEffect.load(frameRateXml); + } } } @@ -95,6 +127,14 @@ public: // not supported by all media players) return 50 * (1.0 - quality) + 1; } + + int getVideoToolboxQuality() { + if (parameters.losslessVideo.getBoolValue()) { + return 100; + } + double quality = juce::jlimit(0.0, 1.0, parameters.qualityEffect.getValue()); + return 100 * quality; + } bool recordingVideo() { return parameters.recordVideo.getBoolValue(); @@ -114,11 +154,21 @@ public: } return parameters.customSharedTextureServerName; } + + int getResolution() { + return parameters.resolution.getValueUnnormalised(); + } + + double getFrameRate() { + return parameters.frameRate.getValueUnnormalised(); + } RecordingParameters& parameters; private: EffectComponent quality{parameters.qualityEffect}; + EffectComponent resolution{parameters.resolutionEffect}; + EffectComponent frameRate{parameters.frameRateEffect}; jux::SwitchButton losslessVideo{¶meters.losslessVideo}; jux::SwitchButton recordAudio{¶meters.recordAudio}; diff --git a/Source/visualiser/VisualiserComponent.cpp b/Source/visualiser/VisualiserComponent.cpp index 8d2fdab..59e363e 100644 --- a/Source/visualiser/VisualiserComponent.cpp +++ b/Source/visualiser/VisualiserComponent.cpp @@ -284,7 +284,7 @@ int VisualiserComponent::prepareTask(double sampleRate, int bufferSize) { audioRecorder.setSampleRate(sampleRate); - int desiredBufferSize = sampleRate / FRAME_RATE; + int desiredBufferSize = sampleRate / recordingSettings.getFrameRate(); return desiredBufferSize; } @@ -411,7 +411,7 @@ void VisualiserComponent::setRecording(bool recording) { 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(FRAME_RATE) + + " -r " + juce::String(recordingSettings.getFrameRate()) + " -f rawvideo" + " -pix_fmt rgba" + " -s " + resolution + @@ -421,6 +421,14 @@ void VisualiserComponent::setRecording(bool recording) { " -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() + "\""; @@ -717,6 +725,19 @@ void VisualiserComponent::renderOpenGL() { // 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()) { @@ -746,7 +767,7 @@ void VisualiserComponent::renderOpenGL() { } renderingSemaphore.release(); - stopwatch.addTime(juce::RelativeTime::seconds(1.0 / FRAME_RATE)); + stopwatch.addTime(juce::RelativeTime::seconds(1.0 / recordingSettings.getFrameRate())); } // render texture to screen @@ -821,12 +842,12 @@ void VisualiserComponent::setupTextures() { glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer); // Create textures - lineTexture = makeTexture(1024, 1024); + lineTexture = makeTexture(recordingSettings.getResolution(), recordingSettings.getResolution()); blur1Texture = makeTexture(512, 512); blur2Texture = makeTexture(512, 512); blur3Texture = makeTexture(128, 128); blur4Texture = makeTexture(128, 128); - renderTexture = makeTexture(1024, 1024); + renderTexture = makeTexture(recordingSettings.getResolution(), recordingSettings.getResolution()); screenOpenGLTexture.loadImage(emptyScreenImage); screenTexture = { screenOpenGLTexture.getTextureID(), screenTextureImage.getWidth(), screenTextureImage.getHeight() }; @@ -839,11 +860,13 @@ void VisualiserComponent::setupTextures() { glBindFramebuffer(GL_FRAMEBUFFER, 0); // Unbind } -Texture VisualiserComponent::makeTexture(int width, int height) { +Texture VisualiserComponent::makeTexture(int width, int height, GLuint textureID) { using namespace juce::gl; - GLuint textureID; - glGenTextures(1, &textureID); + // 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); @@ -860,10 +883,20 @@ Texture VisualiserComponent::makeTexture(int width, int height) { 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; - fadeAmount = juce::jmin(1.0, std::pow(0.5, settings.getPersistence()) * 0.4); + 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); @@ -1072,7 +1105,7 @@ void VisualiserComponent::drawCRT() { activateTargetTexture(blur1Texture); setShader(texturedShader.get()); - texturedShader->setUniform("uResizeForCanvas", lineTexture.width / 1024.0f); + texturedShader->setUniform("uResizeForCanvas", lineTexture.width / (float) recordingSettings.getResolution()); drawTexture({lineTexture}); //horizontal blur 512x512 @@ -1131,7 +1164,7 @@ void VisualiserComponent::drawCRT() { 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 / 1024.0f); + 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({ @@ -1250,7 +1283,7 @@ Texture VisualiserComponent::createScreenTexture() { 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(2.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); @@ -1306,7 +1339,7 @@ void VisualiserComponent::renderScope(const std::vector& xPoints, const s if (sampleRate != oldSampleRate || scratchVertices.empty()) { oldSampleRate = sampleRate; - setupArrays(RESAMPLE_RATIO * sampleRate / FRAME_RATE); + setupArrays(RESAMPLE_RATIO * sampleRate / recordingSettings.getFrameRate()); } intensity = settings.getIntensity() * (41000.0f / sampleRate); diff --git a/Source/visualiser/VisualiserComponent.h b/Source/visualiser/VisualiserComponent.h index 70ea65c..a8aec5b 100644 --- a/Source/visualiser/VisualiserComponent.h +++ b/Source/visualiser/VisualiserComponent.h @@ -82,7 +82,6 @@ private: CommonAudioProcessor& audioProcessor; float intensity; - const double FRAME_RATE = 60.0; bool visualiserOnly; AudioPlayerComponent audioPlayer{audioProcessor}; @@ -194,6 +193,8 @@ private: std::vector fullScreenQuad; GLuint frameBuffer = 0; + + double currentFrameRate = 60.0; Texture lineTexture; Texture blur1Texture; Texture blur2Texture; @@ -209,12 +210,12 @@ private: juce::Image emptyScreenImage = juce::ImageFileFormat::loadFrom(BinaryData::empty_jpg, BinaryData::empty_jpgSize); #if SOSCI_FEATURES - juce::Image oscilloscopeImage = juce::ImageFileFormat::loadFrom(BinaryData::real_jpg, BinaryData::real_jpgSize); - juce::Image vectorDisplayImage = juce::ImageFileFormat::loadFrom(BinaryData::vector_display_jpg, BinaryData::vector_display_jpgSize); + juce::Image oscilloscopeImage = juce::ImageFileFormat::loadFrom(BinaryData::real_png, BinaryData::real_pngSize); + juce::Image vectorDisplayImage = juce::ImageFileFormat::loadFrom(BinaryData::vector_display_png, BinaryData::vector_display_pngSize); juce::Image emptyReflectionImage = juce::ImageFileFormat::loadFrom(BinaryData::no_reflection_jpg, BinaryData::no_reflection_jpgSize); - juce::Image oscilloscopeReflectionImage = juce::ImageFileFormat::loadFrom(BinaryData::real_reflection_jpg, BinaryData::real_reflection_jpgSize); - juce::Image vectorDisplayReflectionImage = juce::ImageFileFormat::loadFrom(BinaryData::vector_display_reflection_jpg, BinaryData::vector_display_reflection_jpgSize); + juce::Image oscilloscopeReflectionImage = juce::ImageFileFormat::loadFrom(BinaryData::real_reflection_png, BinaryData::real_reflection_pngSize); + juce::Image vectorDisplayReflectionImage = juce::ImageFileFormat::loadFrom(BinaryData::vector_display_reflection_png, BinaryData::vector_display_reflection_pngSize); OsciPoint REAL_SCREEN_OFFSET = { 0.02, -0.15 }; OsciPoint REAL_SCREEN_SCALE = { 0.6 }; @@ -252,7 +253,8 @@ private: void initialiseSharedTexture(); void closeSharedTexture(); #endif - Texture makeTexture(int width, int height); + Texture makeTexture(int width, int height, GLuint textureID = 0); + void setResolution(int width); void setupArrays(int num_points); void setupTextures(); void drawLineTexture(const std::vector& xPoints, const std::vector& yPoints, const std::vector& zPoints); diff --git a/osci-render.jucer b/osci-render.jucer index 94fcc69..dc5bb42 100644 --- a/osci-render.jucer +++ b/osci-render.jucer @@ -4,7 +4,7 @@ addUsingNamespaceToJuceHeader="0" jucerFormatVersion="1" pluginCharacteristicsValue="pluginWantsMidiIn" pluginManufacturer="jameshball" aaxIdentifier="sh.ball.oscirender" cppLanguageStandard="20" projectLineFeed=" " headerPath="./include" - version="2.4.4.0" companyName="James H Ball" companyWebsite="https://osci-render.com" + version="2.4.5.0" companyName="James H Ball" companyWebsite="https://osci-render.com" companyEmail="james@ball.sh" defines="NOMINMAX=1 INTERNET_FLAG_NO_AUTO_REDIRECT=0 SOSCI_FEATURES=1" pluginAUMainType="'aumf'"> @@ -37,13 +37,13 @@ - - - - + + + + diff --git a/sosci.jucer b/sosci.jucer index 2d4db35..48a70e3 100644 --- a/sosci.jucer +++ b/sosci.jucer @@ -3,7 +3,7 @@ @@ -32,13 +32,13 @@ - - - - + + + +