Skip to content

Commit

Permalink
LevelsProcessor fixes and Meter work.
Browse files Browse the repository at this point in the history
  • Loading branch information
jrlanglois committed Jan 22, 2025
1 parent f993638 commit 79f15e7
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 79 deletions.
3 changes: 3 additions & 0 deletions modules/squarepine_audio/effects/LevelsProcessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ void LevelsProcessor::processBlock (juce::AudioBuffer<double>& b, MidiBuffer&)
template<typename FloatType>
void LevelsProcessor::process (juce::AudioBuffer<FloatType>& buffer)
{
volatile ScopedNoDenormals noDenormals;
ignoreUnused (noDenormals);

const auto mode = getMeteringMode();
const auto b = isBypassed();

Expand Down
10 changes: 5 additions & 5 deletions modules/squarepine_audio/effects/LevelsProcessor.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class LevelsProcessor final : public InternalProcessor
for (auto& a : { &channels, &tempBuffer })
{
a->resize (numChannels);
a->clearQuick();
a->fill (static_cast<FloatType> (0));
}
}

Expand All @@ -68,7 +68,7 @@ class LevelsProcessor final : public InternalProcessor
MeteringMode meteringMode,
bool possiblyBypassed)
{
tempBuffer.clearQuick();
tempBuffer.fill (static_cast<FloatType> (0));

if (! possiblyBypassed)
{
Expand All @@ -78,19 +78,19 @@ class LevelsProcessor final : public InternalProcessor
{
case MeteringMode::peak:
for (int i = 0; i < numChannels; ++i)
tempBuffer.add ((FloatType) buffer.getMagnitude (i, 0, buffer.getNumSamples()));
tempBuffer.set (i, (FloatType) buffer.getMagnitude (i, 0, buffer.getNumSamples()));
break;

case MeteringMode::rms:
for (int i = 0; i < numChannels; ++i)
tempBuffer.add ((FloatType) buffer.getRMSLevel (i, 0, buffer.getNumSamples()));
tempBuffer.set (i, (FloatType) buffer.getRMSLevel (i, 0, buffer.getNumSamples()));
break;

case MeteringMode::midSide:
for (int i = 0; i < numChannels; ++i)
{
const auto v = buffer.getMagnitude (i, 0, buffer.getNumSamples());
tempBuffer.add ((FloatType) square (v));
tempBuffer.set (i, (FloatType) square (v));
}
break;

Expand Down
92 changes: 64 additions & 28 deletions modules/squarepine_audio/graphics/Meter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,38 +87,60 @@ void Meter::checkModelPtrIsValid() const
void Meter::resized()
{
gradient = {};
imageSource = {};
imageClipped = {};

if (model == nullptr)
return;

auto positions = model->getColourPositions();
if (positions.size() < 2)
{
jassertfalse;
return;
}

// Must have at least 1 colour!
jassert (! positions.empty());

const auto b = getLocalBounds();

std::sort (std::begin (positions), std::end (positions),
[] (const auto& lhs, const auto& rhs) { return lhs.decibels < rhs.decibels; });

const auto b = getLocalBounds();
const bool isHorizontal = model->isHorizontal();
if (isHorizontal)
gradient = ColourGradient::horizontal (positions.front().colour, positions.back().colour, b);
else
gradient = ColourGradient::vertical (positions.front().colour, positions.back().colour, b);

for (size_t i = 1; i < (positions.size() - 1); ++i)
positions[i].addToGradient (gradient, isHorizontal);
if (const auto numPos = (int64) positions.size(); numPos > 1)
for (int64 i = 1; i < (numPos - 1); ++i)
positions[(size_t) i].addToGradient (gradient, isHorizontal);

{
imageSource = { Image::RGB, getWidth(), getHeight(), false };
Graphics ig (imageSource);
ig.setGradientFill (gradient);
ig.fillAll();
}

imageClipped = imageSource;
// TODO force refresh async + repaint
}

void Meter::paint (Graphics& g)
{
if (gradient.getNumColours() <= 0)
//auto b = getLocalBounds();
//DBG (b.toString());

if (imageSource.isNull())
return; // Nothing to draw.

g.setGradientFill (gradient);
g.fillAll();
g.setColour (Colours::red);

for (const auto& c : channels)
{
// imageClipped = imageSource.getClippedImage (c.meterArea);
// g.drawImage (imageClipped, c.meterArea.toFloat(), RectanglePlacement::doNotResize);

g.fillRect (c.meterArea);
}
}

bool Meter::refresh()
Expand All @@ -135,37 +157,51 @@ bool Meter::refresh()
needsMaxLevel = model->needsMaxLevel();
}

bool areLevelsDifferent = false;
bool isMaxLevelDelayExpired = false;
const auto numChans = std::min (levels.size(), channels.size());
const auto chanWidthPx = [&]()
{
auto v = roundToIntAccurate ((double) getWidth() / (double) numChans);
v -= 2;
jassert (v > 2);
return v;
}();

const auto hPx = getHeight();

bool areLevelsDifferent = channels.getFirst().meterArea.getWidth() != chanWidthPx;
bool isMaxLevelDelayExpired = false;

for (int i = 0; i < numChans; ++i)
{
auto& channel = channels.getReference (i);
auto level = lerp (channel.getLevel(), levels[i], 0.9f);
dsp::util::snapToZero (level);
channel.setLevel (level);
const auto lastLevel = channel.level;
const auto lastMaxLevel = channel.maxLevel;

channel.level = lerp (lastLevel, levels[i], 0.9f);

if (needsMaxLevel)
{
if (channel.getLevel() > channel.getMaxLevel())
if (channel.level > lastMaxLevel)
{
channel.setLastMaxAudioLevelTime (Time::currentTimeMillis());
channel.setMaxLevel (channel.getLevel());
channel.timeOfMaximumMs = Time::currentTimeMillis();
channel.maxLevel = lastLevel;
}
else if (Time::currentTimeMillis() - channel.getLastMaxAudioLevelTime() > maxLevelExpiryMs)
else if (Time::currentTimeMillis() - channel.timeOfMaximumMs > maxLevelExpiryMs)
{
channel.setMaxLevel (channel.getMaxLevel() * decayRate);
channel.maxLevel = lastMaxLevel * decayRate;
isMaxLevelDelayExpired = true;
}

areLevelsDifferent |= ! approximatelyEqual (channel.getMaxLevel(), channel.getLastMaxLevel());
areLevelsDifferent |= ! approximatelyEqual (channel.maxLevel, lastMaxLevel);
}

areLevelsDifferent |= ! approximatelyEqual (channel.getLevel(), channel.getLastLevel());
areLevelsDifferent |= ! approximatelyEqual (channel.level, lastLevel);

const auto h = (double) hPx * (double) DecibelHelpers::gainToMeterProportion ((double) channel.level);

channel.setLastLevel (channel.getLevel());
channel.setLastMaxLevel (channel.getMaxLevel());
channel.meterArea = channel.meterArea
.withWidth (chanWidthPx)
.withHeight (roundToIntAccurate (h));
}

if (areLevelsDifferent)
Expand All @@ -174,11 +210,11 @@ bool Meter::refresh()
return areLevelsDifferent;
}

void Meter::updateClippingLevel (bool timeToUpdate)
void Meter::updateClippingLevel (bool forceUpdate)
{
auto maxLevel = 0.0f;
for (const auto& channel : channels)
maxLevel = std::max (channel.getLevel(), maxLevel);
maxLevel = std::max (channel.level, maxLevel);

maxLevel = Decibels::gainToDecibels (maxLevel);

Expand All @@ -190,6 +226,6 @@ void Meter::updateClippingLevel (bool timeToUpdate)
currClippingLevel = ClippingLevel::warning;

// Only update if higher, or if the time delay has expired.
if (clippingLevel < currClippingLevel || timeToUpdate)
if (clippingLevel < currClippingLevel || forceUpdate)
clippingLevel = currClippingLevel;
}
57 changes: 11 additions & 46 deletions modules/squarepine_audio/graphics/Meter.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@ struct DecibelHelpers final
{
enum
{
// The maximum gain value of the slider in decibels.
maxSliderLevelDb = 12,

// The minimum decibel level of the slider.
minSliderLevelDb = -100,
maxSliderLevelDb = 12, // The maximum gain value of the slider in decibels.
minSliderLevelDb = -100, // The minimum decibel level of the slider.

// The magnitude of spatial compression of lower decibel values versus higher values.
// Ranges from 0 (linear) to infinity (hard knee).
Expand Down Expand Up @@ -41,7 +38,7 @@ struct DecibelHelpers final
int maximumDecibels = maxSliderLevelDb) noexcept;

/** @returns the proportion of the meter length for the decibel value.
*
@param meterProportion The proportion of the meter length.
@param minimumDecibels The minimum level of the meter in decibels.
@param maximumDecibels The maximum level of the meter in decibels.
Expand All @@ -51,7 +48,7 @@ struct DecibelHelpers final
int maximumDecibels = maxSliderLevelDb) noexcept;

/** @returns the proportion of the meter height for the decibel value.
*
@param decibels The volume value in decibels.
@param minimumDecibels The minimum level of the meter in decibels
@param maximumDecibels The maximum level of the meter in decibels
Expand Down Expand Up @@ -213,53 +210,19 @@ class Meter final : public Component

//==============================================================================
/** */
struct ChannelContext
struct ChannelContext final
{
ChannelContext() = default;
ChannelContext (const ChannelContext&) = default;
ChannelContext (ChannelContext&&) = default;
~ChannelContext() = default;
ChannelContext& operator= (const ChannelContext&) = default;
ChannelContext& operator= (ChannelContext&&) = default;

void setLevel (float newLevel) { set (level, newLevel); }
float getLevel() const noexcept { return level; }

void setLastLevel (float newLastLevel) { set (lastLevel, newLastLevel); }
float getLastLevel() const noexcept { return lastLevel; }

void setMaxLevel (float newMaxLevel) { set (maxLevel, newMaxLevel); }
float getMaxLevel() const noexcept { return maxLevel; }

void setLastMaxLevel (float newLastMaxLevel) { set (lastMaxLevel, newLastMaxLevel); }
float getLastMaxLevel() const noexcept { return lastMaxLevel; }

void setLastMaxAudioLevelTime (int64 t) { timeOfMaximumMs = t; }
int64 getLastMaxAudioLevelTime() const noexcept { return timeOfMaximumMs; }

void setMeterArea (const Rectangle<int>& area) { meterArea = area; }
const Rectangle<int>& getMeterArea() const noexcept { return meterArea; }

private:
float level = 0.0f, // The last measured audio absolute volume level.
lastLevel = 0.0f, // The volume level of the last update, used to check if levels have changed for repainting.
maxLevel = 0.0f, // The maximum audio levels of the trailing 3 seconds.
lastMaxLevel = 0.0f; // The max volume level of the last update.
maxLevel = 0.0f; // The maximum audio levels of the trailing level (by default this is 3 seconds).
int64 timeOfMaximumMs = 0; // The time of the last maximum audio level.
Rectangle<int> meterArea; // The left/right drawable regions for the meter.

void set (float& value, float newValue)
{
dsp::util::snapToZero (newValue);
value = newValue;
}
Rectangle<int> meterArea; // The drawable regions for the meter's channels (eg: left, right).
};

/** */
const ChannelContext& getChannel (int channel) const noexcept { return channels.getReference (channel); }

/** */
float getChannelLevel (int channel) const noexcept { return getChannel (channel).getLevel(); }
float getChannelLevel (int channel) const noexcept { return getChannel (channel).level; }

//==============================================================================
/** */
Expand Down Expand Up @@ -288,7 +251,9 @@ class Meter final : public Component
Array<ChannelContext> channels;
Array<float> levels;
ClippingLevel clippingLevel = ClippingLevel::none;

ColourGradient gradient;
Image imageSource, imageClipped;

void assignModelPtr (MeterModel*);

Expand All @@ -298,7 +263,7 @@ class Meter final : public Component
#endif

//==============================================================================
void updateClippingLevel (bool timeToUpdate);
void updateClippingLevel (bool forceUpdate);

//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Meter)
Expand Down

0 comments on commit 79f15e7

Please sign in to comment.