├── .gitignore ├── screenshot.png ├── src ├── PluginEditor.h ├── PluginEditor.cpp ├── AnalysisData.h ├── PluginProcessor.h ├── VUMeter.h ├── PluginProcessor.cpp └── VUMeter.cpp ├── LICENSE ├── README.markdown └── CMakeLists.txt /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | **/build 3 | **/*.filtergraph 4 | 5 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hollance/levels/HEAD/screenshot.png -------------------------------------------------------------------------------- /src/PluginEditor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "PluginProcessor.h" 5 | #include "VUMeter.h" 6 | 7 | class AudioProcessorEditor : public juce::AudioProcessorEditor 8 | { 9 | public: 10 | AudioProcessorEditor(AudioProcessor&); 11 | 12 | void paint(juce::Graphics&) override; 13 | void resized() override; 14 | 15 | private: 16 | VUMeter meter; 17 | 18 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AudioProcessorEditor) 19 | }; 20 | -------------------------------------------------------------------------------- /src/PluginEditor.cpp: -------------------------------------------------------------------------------- 1 | #include "PluginProcessor.h" 2 | #include "PluginEditor.h" 3 | 4 | AudioProcessorEditor::AudioProcessorEditor(AudioProcessor& p) : 5 | juce::AudioProcessorEditor(&p), 6 | meter(p.analysis) 7 | { 8 | addAndMakeVisible(meter); 9 | setOpaque(true); 10 | setSize(80, 400); 11 | setWantsKeyboardFocus(false); 12 | } 13 | 14 | void AudioProcessorEditor::paint(juce::Graphics&) 15 | { 16 | // VUMeter paints the entire bounds. 17 | } 18 | 19 | void AudioProcessorEditor::resized() 20 | { 21 | meter.setBounds(getLocalBounds()); 22 | } 23 | -------------------------------------------------------------------------------- /src/AnalysisData.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct Measurement 4 | { 5 | void reset() noexcept 6 | { 7 | value = 0.0f; 8 | } 9 | 10 | void update(float newValue) noexcept 11 | { 12 | auto oldValue = value.load(); 13 | while (newValue > oldValue && !value.compare_exchange_weak(oldValue, newValue)); 14 | } 15 | 16 | float read() noexcept 17 | { 18 | return value.exchange(0.0f); 19 | } 20 | 21 | std::atomic value; 22 | }; 23 | 24 | struct AnalysisData 25 | { 26 | void reset() noexcept 27 | { 28 | levelL.reset(); 29 | levelR.reset(); 30 | levelM.reset(); 31 | levelS.reset(); 32 | } 33 | 34 | Measurement levelL; 35 | Measurement levelR; 36 | Measurement levelM; 37 | Measurement levelS; 38 | }; 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 M.I. Hollemans 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Levels 2 | 3 | Basic level meter. Useful for testing and debugging plug-ins that don't have a built-in level meter. 4 | 5 | ![](screenshot.png) 6 | 7 | For more info on how this plug-in works, [check out the blog post](https://audiodev.blog/levels-plugin/). 8 | 9 | ## How to build 10 | 11 | This plug-in uses [JUCE](https://juce.com) and is built using CMake. Make sure [JUCE's CMake support](https://github.com/juce-framework/JUCE/blob/master/docs/CMake%20API.md) has been installed on your system. 12 | 13 | ### macOS 14 | 15 | ```text 16 | $ git clone https://github.com/hollance/levels.git 17 | $ cd levels 18 | $ cmake -B build -G Xcode 19 | $ cmake --build build -j --config Release 20 | ``` 21 | 22 | The resulting AU and VST3 will be installed in `~/Library/Audio/Plug-Ins/`. 23 | 24 | I have only tried it on an Intel Mac running macOS 12 (Monterey) with Xcode 14.2 and JUCE 7.0.8. 25 | 26 | ### Windows 27 | 28 | ```text 29 | $ git clone https://github.com/hollance/levels.git 30 | $ cd levels 31 | $ cmake -B build -G "Visual Studio 17 2022" 32 | $ cmake --build build -j --config Release 33 | ``` 34 | 35 | The resulting VST will be installed in `C:\Program Files\Common Files\VST3` (you may need to change permissions on this folder). 36 | 37 | I have only tried it on Windows 10 with Visual Studio 2022 and JUCE 7.0.9. 38 | 39 | ## License 40 | 41 | The source code in this repo is licensed under the terms of the [MIT license](LICENSE). 42 | -------------------------------------------------------------------------------- /src/PluginProcessor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "AnalysisData.h" 5 | 6 | class AudioProcessor : public juce::AudioProcessor 7 | { 8 | public: 9 | AudioProcessor(); 10 | 11 | bool hasEditor() const override { return true; } 12 | const juce::String getName() const override { return JucePlugin_Name; } 13 | bool acceptsMidi() const override { return false; } 14 | bool producesMidi() const override { return false; } 15 | bool isMidiEffect() const override { return false; } 16 | double getTailLengthSeconds() const override { return 0.0; } 17 | 18 | int getNumPrograms() override { return 1; } 19 | int getCurrentProgram() override { return 0; } 20 | void setCurrentProgram(int) override { } 21 | const juce::String getProgramName(int) override { return {}; } 22 | void changeProgramName(int, const juce::String&) override { } 23 | 24 | bool isBusesLayoutSupported(const BusesLayout& layouts) const override; 25 | void prepareToPlay(double sampleRate, int samplesPerBlock) override; 26 | void releaseResources() override; 27 | void reset() override; 28 | void processBlock(juce::AudioBuffer&, juce::MidiBuffer&) override; 29 | 30 | void getStateInformation(juce::MemoryBlock& destData) override; 31 | void setStateInformation(const void* data, int sizeInBytes) override; 32 | juce::AudioProcessorEditor* createEditor() override; 33 | 34 | AnalysisData analysis; 35 | 36 | private: 37 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AudioProcessor) 38 | }; 39 | -------------------------------------------------------------------------------- /src/VUMeter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "AnalysisData.h" 5 | 6 | class VUMeter : public juce::Component, private juce::Timer 7 | { 8 | public: 9 | VUMeter(AnalysisData& analysis); 10 | 11 | void paint(juce::Graphics&) override; 12 | void resized() override; 13 | 14 | private: 15 | struct Channel 16 | { 17 | float level; 18 | float leveldB; 19 | float peak; 20 | float peakdB; 21 | int peakHold; 22 | int x; 23 | }; 24 | 25 | void timerCallback() override; 26 | void updateChannel(Channel& channel, Measurement& measurement); 27 | void drawLevel(juce::Graphics& g, const Channel& channel); 28 | void drawPeak(juce::Graphics& g, const Channel& channel); 29 | 30 | int positionForLevel(float dbLevel) const noexcept 31 | { 32 | return int(std::round(juce::jmap(dbLevel, maxdB, mindB, maxPos, minPos))); 33 | } 34 | 35 | AnalysisData& analysis; 36 | 37 | static constexpr float maxdB = 12.0f; // highest dB shown 38 | static constexpr float mindB = -60.0f; // lowest dB shown 39 | static constexpr float stepdB = 6.0f; // draw a tick every 6 dB 40 | 41 | static constexpr float clampdB = -120.0f; 42 | static constexpr float clampLevel = 0.000001f; // -120 dB 43 | 44 | static constexpr int refreshRate = 60; 45 | static constexpr int holdMax = refreshRate * 2; 46 | 47 | Channel channels[4]; // L, R, M, S 48 | 49 | float maxPos; // maxdB line 50 | float minPos; // mindB line 51 | 52 | float levelDecay; // filter coefficients 53 | float peakDecay; 54 | 55 | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(VUMeter) 56 | }; 57 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.22) 2 | 3 | project(Levels VERSION 1.0.0 LANGUAGES CXX) 4 | 5 | set_property(GLOBAL PROPERTY USE_FOLDERS YES) 6 | option(JUCE_ENABLE_MODULE_SOURCE_GROUPS "Enable Module Source Groups" ON) 7 | 8 | find_package(JUCE CONFIG REQUIRED) 9 | 10 | set(CMAKE_CXX_EXTENSIONS OFF) 11 | set(CMAKE_CXX_STANDARD 17) 12 | set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13" CACHE STRING "Minimum OS X deployment target") 13 | set(CMAKE_OSX_ARCHITECTURES arm64 x86_64) 14 | set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") 15 | 16 | if(MSVC) 17 | add_compile_options("/W4") 18 | else() 19 | add_compile_options( 20 | -Wall 21 | -Wbool-conversion 22 | -Wcast-align 23 | -Wconditional-uninitialized 24 | -Wconstant-conversion 25 | -Wconversion 26 | -Wdeprecated 27 | -Wextra-semi 28 | #-Wfloat-equal 29 | -Winconsistent-missing-destructor-override 30 | -Wint-conversion 31 | -Wmissing-field-initializers 32 | -Wmissing-prototypes 33 | -Wno-ignored-qualifiers 34 | -Wnullable-to-nonnull-conversion 35 | -Woverloaded-virtual 36 | -Wpedantic 37 | -Wreorder 38 | -Wshadow-all 39 | -Wshift-sign-overflow 40 | -Wshorten-64-to-32 41 | -Wsign-compare 42 | -Wsign-conversion 43 | -Wstrict-aliasing 44 | -Wswitch-enum 45 | -Wuninitialized 46 | -Wunreachable-code 47 | -Wunused-parameter 48 | -Wunused-private-field 49 | -Wzero-as-null-pointer-constant 50 | ) 51 | endif() 52 | 53 | juce_add_plugin(${PROJECT_NAME} 54 | COMPANY_NAME "audiodev.blog" 55 | PLUGIN_MANUFACTURER_CODE Dvbl 56 | PLUGIN_CODE Levl 57 | BUNDLE_ID "blog.audiodev.levels" 58 | IS_SYNTH FALSE 59 | NEEDS_MIDI_INPUT FALSE 60 | NEEDS_MIDI_OUTPUT FALSE 61 | IS_MIDI_EFFECT FALSE 62 | EDITOR_WANTS_KEYBOARD_FOCUS FALSE 63 | COPY_PLUGIN_AFTER_BUILD TRUE 64 | FORMATS AU VST3 Standalone 65 | PRODUCT_NAME "Levels" 66 | ) 67 | 68 | juce_generate_juce_header(${PROJECT_NAME}) 69 | 70 | target_sources(${PROJECT_NAME} PRIVATE 71 | src/AnalysisData.h 72 | src/PluginEditor.cpp 73 | src/PluginEditor.h 74 | src/PluginProcessor.cpp 75 | src/PluginProcessor.h 76 | src/VUMeter.cpp 77 | src/VUMeter.h 78 | ) 79 | 80 | target_compile_definitions(${PROJECT_NAME} PUBLIC 81 | JUCE_WEB_BROWSER=0 82 | JUCE_USE_CURL=0 83 | JUCE_VST3_CAN_REPLACE_VST2=0 84 | JUCE_DISPLAY_SPLASH_SCREEN=0 85 | JUCE_REPORT_APP_USAGE=0 86 | JUCE_MODAL_LOOPS_PERMITTED=0 87 | DONT_SET_USING_JUCE_NAMESPACE=1 88 | ) 89 | 90 | target_link_libraries(${PROJECT_NAME} 91 | PRIVATE 92 | juce::juce_audio_utils 93 | PUBLIC 94 | juce::juce_recommended_config_flags 95 | juce::juce_recommended_lto_flags 96 | ) 97 | -------------------------------------------------------------------------------- /src/PluginProcessor.cpp: -------------------------------------------------------------------------------- 1 | #include "PluginProcessor.h" 2 | #include "PluginEditor.h" 3 | 4 | AudioProcessor::AudioProcessor() : 5 | juce::AudioProcessor( 6 | BusesProperties() 7 | .withInput("Input", juce::AudioChannelSet::stereo(), true) 8 | .withOutput("Output", juce::AudioChannelSet::stereo(), true)) 9 | { 10 | } 11 | 12 | void AudioProcessor::prepareToPlay(double, int) 13 | { 14 | reset(); 15 | } 16 | 17 | void AudioProcessor::releaseResources() 18 | { 19 | // do nothing 20 | } 21 | 22 | void AudioProcessor::reset() 23 | { 24 | analysis.reset(); 25 | } 26 | 27 | bool AudioProcessor::isBusesLayoutSupported(const BusesLayout& layouts) const 28 | { 29 | return layouts.getMainOutputChannelSet() == juce::AudioChannelSet::stereo() 30 | || layouts.getMainOutputChannelSet() == juce::AudioChannelSet::mono(); 31 | } 32 | 33 | void AudioProcessor::processBlock( 34 | juce::AudioBuffer& buffer, [[maybe_unused]] juce::MidiBuffer& midiMessages) 35 | { 36 | juce::ScopedNoDenormals noDenormals; 37 | auto numInputChannels = getTotalNumInputChannels(); 38 | auto numOutputChannels = getTotalNumOutputChannels(); 39 | auto numSamples = buffer.getNumSamples(); 40 | 41 | // Clear any output channels that don't contain input data. 42 | for (auto i = numInputChannels; i < numOutputChannels; ++i) { 43 | buffer.clear(i, 0, numSamples); 44 | } 45 | 46 | bool stereo = numInputChannels > 1; 47 | const float* channelL = buffer.getReadPointer(0); 48 | const float* channelR = buffer.getReadPointer(stereo ? 1 : 0); 49 | 50 | float levelL = 0.0f; 51 | float levelR = 0.0f; 52 | float levelM = 0.0f; 53 | float levelS = 0.0f; 54 | 55 | for (int sample = 0; sample < numSamples; ++sample) { 56 | float sampleL = channelL[sample]; 57 | float sampleR = channelR[sample]; 58 | float sampleM = (sampleL + sampleR) * 0.5f; 59 | float sampleS = (sampleL - sampleR) * 0.5f; 60 | 61 | levelL = std::max(levelL, std::abs(sampleL)); 62 | levelR = std::max(levelR, std::abs(sampleR)); 63 | levelM = std::max(levelM, std::abs(sampleM)); 64 | levelS = std::max(levelS, std::abs(sampleS)); 65 | } 66 | 67 | analysis.levelL.update(levelL); 68 | analysis.levelR.update(levelR); 69 | analysis.levelM.update(levelM); 70 | analysis.levelS.update(levelS); 71 | } 72 | 73 | void AudioProcessor::getStateInformation(juce::MemoryBlock&) 74 | { 75 | // do nothing 76 | } 77 | 78 | void AudioProcessor::setStateInformation(const void*, int) 79 | { 80 | // do nothing 81 | } 82 | 83 | juce::AudioProcessorEditor* AudioProcessor::createEditor() 84 | { 85 | return new AudioProcessorEditor(*this); 86 | } 87 | 88 | juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() 89 | { 90 | return new AudioProcessor(); 91 | } 92 | -------------------------------------------------------------------------------- /src/VUMeter.cpp: -------------------------------------------------------------------------------- 1 | #include "VUMeter.h" 2 | 3 | VUMeter::VUMeter(AnalysisData& analysis_) : analysis(analysis_) 4 | { 5 | levelDecay = 1.0f - std::exp(-1.0f / (float(refreshRate) * 0.2f)); 6 | peakDecay = 1.0f - std::exp(-1.0f / (float(refreshRate) * 0.5f)); 7 | 8 | int xs[] = { 0, 16, 50, 66 }; 9 | for (int i = 0; i < 4; ++i) { 10 | channels[i].level = 0.0f; 11 | channels[i].leveldB = clampdB; 12 | channels[i].peak = 0.0f; 13 | channels[i].peakdB = clampdB; 14 | channels[i].peakHold = 0; 15 | channels[i].x = xs[i]; 16 | } 17 | 18 | setOpaque(true); 19 | startTimerHz(refreshRate); 20 | } 21 | 22 | void VUMeter::paint(juce::Graphics& g) 23 | { 24 | const auto bounds = getLocalBounds(); 25 | 26 | g.fillAll(juce::Colour(245, 240, 235)); 27 | 28 | for (int i = 0; i < 4; ++i) { 29 | drawLevel(g, channels[i]); 30 | } 31 | 32 | g.setFont(11.0f); 33 | for (float db = maxdB; db >= mindB; db -= stepdB) { 34 | int y = positionForLevel(db); 35 | 36 | g.setColour(db == 0.0f ? juce::Colour(120, 120, 120) : juce::Colour(200, 200, 200)); 37 | g.fillRect(0, y, 30, 1); 38 | g.fillRect(50, y, 30, 1); 39 | 40 | g.setColour(juce::Colour(80, 80, 80)); 41 | g.drawSingleLineText(juce::String(int(db)), bounds.getCentreX(), y + 4, 42 | juce::Justification::horizontallyCentred); 43 | } 44 | 45 | for (int i = 0; i < 4; ++i) { 46 | drawPeak(g, channels[i]); 47 | } 48 | 49 | int y = bounds.getHeight() - 5; 50 | g.setFont(14.0f); 51 | g.setColour(juce::Colour(0, 0, 0)); 52 | g.drawSingleLineText("L", 4, y); 53 | g.drawSingleLineText("R", 19, y); 54 | g.drawSingleLineText("M", 52, y); 55 | g.drawSingleLineText("S", 70, y); 56 | } 57 | 58 | void VUMeter::drawLevel(juce::Graphics& g, const Channel& channel) 59 | { 60 | if (channel.level > 0.0f) { 61 | int y = positionForLevel(channel.leveldB); 62 | if (channel.leveldB > 0.0f) { 63 | int y0 = positionForLevel(0.0f); 64 | g.setColour(juce::Colour(226, 74, 81)); 65 | g.fillRect(channel.x, y, 14, y0 - y); 66 | g.setColour(juce::Colour(65, 206, 88)); 67 | g.fillRect(channel.x, y0, 14, getHeight() - y0); 68 | } else if (y < getHeight()) { 69 | g.setColour(juce::Colour(65, 206, 88)); 70 | g.fillRect(channel.x, y, 14, getHeight() - y); 71 | } 72 | } 73 | } 74 | 75 | void VUMeter::drawPeak(juce::Graphics& g, const Channel& channel) 76 | { 77 | if (channel.peakdB > clampdB) { 78 | g.setColour(juce::Colour(0, 0, 0)); 79 | g.fillRect(channel.x, positionForLevel(channel.peakdB) - 1, 14, 3); 80 | } 81 | } 82 | 83 | void VUMeter::resized() 84 | { 85 | maxPos = 10.0f; 86 | minPos = float(getHeight()) - 30.0f; 87 | } 88 | 89 | void VUMeter::timerCallback() 90 | { 91 | updateChannel(channels[0], analysis.levelL); 92 | updateChannel(channels[1], analysis.levelR); 93 | updateChannel(channels[2], analysis.levelM); 94 | updateChannel(channels[3], analysis.levelS); 95 | 96 | repaint(); 97 | } 98 | 99 | void VUMeter::updateChannel(Channel& channel, Measurement& measurement) 100 | { 101 | float newLevel = measurement.read(); 102 | if (newLevel > channel.level) { 103 | channel.level = newLevel; // instantaneous attack 104 | } else { 105 | channel.level += (newLevel - channel.level) * levelDecay; 106 | } 107 | if (channel.level > clampLevel) { 108 | channel.leveldB = juce::Decibels::gainToDecibels(channel.level); 109 | } else { 110 | channel.leveldB = clampdB; 111 | } 112 | 113 | if (channel.level > channel.peak) { 114 | channel.peak = channel.level; 115 | channel.peakdB = channel.leveldB; 116 | channel.peakHold = holdMax; 117 | } else if (channel.peakHold > 0) { 118 | channel.peakHold -= 1; 119 | } else if (channel.peakdB > clampdB) { 120 | channel.peak += (channel.level - channel.peak) * peakDecay; 121 | if (channel.peak > clampLevel) { 122 | channel.peakdB = juce::Decibels::gainToDecibels(channel.peak); 123 | } else { 124 | channel.peakdB = clampdB; 125 | } 126 | } 127 | } 128 | --------------------------------------------------------------------------------