Mema
Memory Matrix — multi-channel audio matrix monitor and router
Loading...
Searching...
No Matches
TwoDFieldOutputComponent.cpp
Go to the documentation of this file.
1/* Copyright (c) 2024-2025, Christian Ahrens
2 *
3 * This file is part of Mema <https://github.com/ChristianAhrens/Mema>
4 *
5 * This tool is free software; you can redistribute it and/or modify it under
6 * the terms of the GNU Lesser General Public License version 3.0 as published
7 * by the Free Software Foundation.
8 *
9 * This tool is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
12 * details.
13 *
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this tool; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 */
18
20
21#include <CustomLookAndFeel.h>
22
23
24namespace Mema
25{
26
27//#define PAINTINGHELPER
28
29//==============================================================================
35
39
40void TwoDFieldOutputComponent::paint (juce::Graphics& g)
41{
43
44 // (Our component is opaque, so we must completely fill the background with a solid colour)
45 g.fillAll(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId));
46
47 // paint the level indications where applicable
48 if (!m_positionedChannelsArea.isEmpty())
49 paintCircularLevelIndication(g, m_positionedChannelsArea, m_channelLevelMaxPoints, m_clockwiseOrderedChannelTypes);
50 if (!m_positionedHeightChannelsArea.isEmpty())
51 paintCircularLevelIndication(g, m_positionedHeightChannelsArea, m_channelHeightLevelMaxPoints, m_clockwiseOrderedHeightChannelTypes);
52 if (!m_directionlessChannelsArea.isEmpty())
53 paintLevelMeterIndication(g, m_directionlessChannelsArea, m_directionLessChannelTypes);
54
55 // draw dBFS
56 g.setFont(12.0f);
57 g.setColour(getLookAndFeel().findColour(juce::TextButton::textColourOffId));
58 juce::String rangeText;
60 rangeText = juce::String(ProcessorDataAnalyzer::getGlobalMindB()) + " ... " + juce::String(ProcessorDataAnalyzer::getGlobalMaxdB()) + " dBFS";
61 else
62 rangeText = "0 ... 1";
63 g.drawText(rangeText, getLocalBounds(), juce::Justification::topRight, true);
64}
65
66void TwoDFieldOutputComponent::paintCircularLevelIndication(juce::Graphics& g, const juce::Rectangle<float>& circleArea, const std::map<int, juce::Point<float>>& channelLevelMaxPoints, const juce::Array<juce::AudioChannelSet::ChannelType>& channelsToPaint)
67{
68#if defined DEBUG && defined PAINTINGHELPER
69 g.setColour(juce::Colours::blueviolet);
70 g.drawRect(circleArea);
71#endif
72
73 // fill circle background
74 g.setColour(getLookAndFeel().findColour(juce::Slider::backgroundColourId));
75 g.fillEllipse(circleArea);
76
77#if defined DEBUG && defined PAINTINGHELPER
78 g.setColour(juce::Colours::red);
79 g.drawRect(circleArea);
80 g.setColour(juce::Colours::blue);
81 g.drawRect(getLocalBounds());
82#endif
83
84
85 const float meterWidth = 5.0f;
86 const float halfMeterWidth = 2.0f;
87
88
89 // helper std::function to avoid codeclones below
90 auto calcLevelVals = [=](std::map<int, float>& levels, bool isHold, bool isPeak, bool isRms) {
91 for (auto const& channelType : channelsToPaint)
92 {
93 if (isHold)
94 {
96 levels[channelType] = m_levelData.GetLevel(getChannelNumberForChannelTypeInCurrentConfiguration(channelType)).GetFactorHOLDdB();
97 else
98 levels[channelType] = m_levelData.GetLevel(getChannelNumberForChannelTypeInCurrentConfiguration(channelType)).hold;
99 }
100 else if (isPeak)
101 {
102 if (getUsesValuesInDB())
103 levels[channelType] = m_levelData.GetLevel(getChannelNumberForChannelTypeInCurrentConfiguration(channelType)).GetFactorPEAKdB();
104 else
105 levels[channelType] = m_levelData.GetLevel(getChannelNumberForChannelTypeInCurrentConfiguration(channelType)).peak;
106 }
107 else if (isRms)
108 {
109 if (getUsesValuesInDB())
110 levels[channelType] = m_levelData.GetLevel(getChannelNumberForChannelTypeInCurrentConfiguration(channelType)).GetFactorRMSdB();
111 else
112 levels[channelType] = m_levelData.GetLevel(getChannelNumberForChannelTypeInCurrentConfiguration(channelType)).rms;
113 }
114 }
115 };
116 // calculate hold values
117 std::map<int, float> holdLevels;
118 calcLevelVals(holdLevels, true, false, false);
119 // calculate peak values
120 std::map<int, float> peakLevels;
121 calcLevelVals(peakLevels, false, true, false);
122 // calculate rms values
123 std::map<int, float> rmsLevels;
124 calcLevelVals(rmsLevels, false, false, true);
125
126
127 auto circleCenter = circleArea.getCentre();
128
129 // prepare max points
130 std::map<int, juce::Point<float>> centerToMaxVectors;
131 std::map<int, juce::Point<float>> meterWidthOffsetVectors;
132 for (int i = 0; i < channelsToPaint.size(); i++)
133 {
134 auto const& channelType = channelsToPaint[i];
135 if (0 < channelLevelMaxPoints.count(channelType))
136 {
137 auto angleRad = juce::degreesToRadians(getAngleForChannelTypeInCurrentConfiguration(channelType));
138 centerToMaxVectors[channelType] = circleCenter - channelLevelMaxPoints.at(channelType);
139 meterWidthOffsetVectors[channelType] = { cosf(angleRad) * halfMeterWidth, sinf(angleRad) * halfMeterWidth };
140 }
141 }
142
143 // helper std::function to avoid codeclones below
144 auto createAndPaintLevelPath = [=](std::map<int, juce::Point<float>>& centerToMaxPoints, std::map<int, juce::Point<float>>& meterWidthOffsetPoints, std::map<int, float>& levels, juce::Graphics& g, const juce::Colour& colour, bool stroke) {
145 juce::Path path;
146 auto pathStarted = false;
147 for (auto const& channelType : channelsToPaint)
148 {
149 auto channelMaxPoint = circleCenter - (centerToMaxPoints[channelType] * levels[channelType]);
150
151 if (!pathStarted)
152 {
153 path.startNewSubPath(channelMaxPoint - meterWidthOffsetPoints[channelType]);
154 pathStarted = true;
155 }
156 else
157 path.lineTo(channelMaxPoint - meterWidthOffsetPoints[channelType]);
158
159 path.lineTo(channelMaxPoint + meterWidthOffsetPoints[channelType]);
160 }
161 path.closeSubPath();
162
163 g.setColour(colour);
164 if (stroke)
165 g.strokePath(path, juce::PathStrokeType(1));
166 else
167 g.fillPath(path);
168#if defined DEBUG && defined PAINTINGHELPER
169 g.setColour(juce::Colours::yellow);
170 g.drawRect(path.getBounds());
171#endif
172 };
173 // paint hold values as path
174 createAndPaintLevelPath(centerToMaxVectors, meterWidthOffsetVectors, holdLevels, g, getLookAndFeel().findColour(JUCEAppBasics::CustomLookAndFeel::ColourIds::MeteringHoldColourId), true);
175 // paint peak values as path
176 createAndPaintLevelPath(centerToMaxVectors, meterWidthOffsetVectors, peakLevels, g, getLookAndFeel().findColour(JUCEAppBasics::CustomLookAndFeel::ColourIds::MeteringPeakColourId), false);
177 // paint rms values as path
178 createAndPaintLevelPath(centerToMaxVectors, meterWidthOffsetVectors, rmsLevels, g, getLookAndFeel().findColour(JUCEAppBasics::CustomLookAndFeel::ColourIds::MeteringRmsColourId), false);
179
180
181 // helper std::function to avoid codeclones below
182 auto paintLevelMeterLines = [=](std::map<int, juce::Point<float>>& centerToMaxPoints, std::map<int, juce::Point<float>>& meterWidthOffsetPoints, std::map<int, float>& levels, juce::Graphics& g, const juce::Colour& colour, bool isHoldLine) {
183 g.setColour(colour);
184 for (auto const& channelType : channelsToPaint)
185 {
186 auto channelMaxPoint = circleCenter - (centerToMaxPoints[channelType] * levels[channelType]);
187
188 if (isHoldLine)
189 g.drawLine(juce::Line<float>(channelMaxPoint - meterWidthOffsetPoints[channelType], channelMaxPoint + meterWidthOffsetPoints[channelType]), 1.0f);
190 else
191 g.drawLine(juce::Line<float>(circleCenter, channelMaxPoint), meterWidth);
192 }
193 };
194 // paint hold values as max line
195 paintLevelMeterLines(centerToMaxVectors, meterWidthOffsetVectors, holdLevels, g, getLookAndFeel().findColour(JUCEAppBasics::CustomLookAndFeel::ColourIds::MeteringHoldColourId), true);
196 // paint peak values as line
197 paintLevelMeterLines(centerToMaxVectors, meterWidthOffsetVectors, peakLevels, g, getLookAndFeel().findColour(JUCEAppBasics::CustomLookAndFeel::ColourIds::MeteringPeakColourId), false);
198 // paint rms values as line
199 paintLevelMeterLines(centerToMaxVectors, meterWidthOffsetVectors, rmsLevels, g, getLookAndFeel().findColour(JUCEAppBasics::CustomLookAndFeel::ColourIds::MeteringRmsColourId), false);
200
201 // draw a simple circle surrounding
202 g.setColour(getLookAndFeel().findColour(juce::TextButton::textColourOffId));
203 g.drawEllipse(circleArea, 1);
204
205 // draw dashed field dimension indication lines
206 float dparam[]{ 4.0f, 5.0f };
207 for (auto const& channelType : channelsToPaint)
208 if (0 < channelLevelMaxPoints.count(channelType))
209 g.drawDashedLine(juce::Line<float>(channelLevelMaxPoints.at(channelType), circleCenter), dparam, 2);
210
211 // draw channelType naming legend
212 g.setColour(getLookAndFeel().findColour(juce::TextButton::textColourOffId));
213 for (auto const& channelType : channelsToPaint)
214 {
215 if (0 >= channelLevelMaxPoints.count(channelType))
216 continue;
217
218 auto channelName = juce::AudioChannelSet::getAbbreviatedChannelTypeName(channelType);
219 auto textRect = juce::Rectangle<float>(juce::GlyphArrangement::getStringWidth(g.getCurrentFont(), channelName), g.getCurrentFont().getHeight());
220 auto angle = getAngleForChannelTypeInCurrentConfiguration(channelType);
221 auto textRectOffset = juce::Point<int>(-int(textRect.getWidth() / 2.0f), 0);
222 if (90.0f < angle)
223 angle += 180.0f;
224 else if (-90.0f > angle)
225 angle -= 180.0f;
226 else
227 textRectOffset.addXY(0, -int(g.getCurrentFont().getHeight()));
228 auto angleRad = juce::degreesToRadians(angle);
229
230 g.saveState();
231 g.setOrigin(channelLevelMaxPoints.at(channelType).toInt());
232 g.addTransform(juce::AffineTransform().translated(textRectOffset).rotated(angleRad));
233 g.drawText(channelName, textRect, Justification::centred, true);
234
235#if defined DEBUG && defined PAINTINGHELPER
236 g.setColour(juce::Colours::lightblue);
237 g.drawRect(textRect);
238 g.setColour(getLookAndFeel().findColour(juce::TextButton::textColourOffId));
239#endif
240
241 g.restoreState();
242 }
243}
244
245void TwoDFieldOutputComponent::paintLevelMeterIndication(juce::Graphics& g, const juce::Rectangle<float>& levelMeterArea, const juce::Array<juce::AudioChannelSet::ChannelType>& channelsToPaint)
246{
247#if defined DEBUG && defined PAINTINGHELPER
248 g.setColour(juce::Colours::aqua);
249 g.drawRect(levelMeterArea);
250#endif
251
252 auto channelCount = channelsToPaint.size();
253 auto margin = levelMeterArea.getWidth() / ((2 * channelCount) + 1);
254
255 auto visuArea = levelMeterArea;
256 auto visuAreaOrigY = visuArea.getBottom();
257
258 // draw meters
259 auto meterSpacing = margin;
260 auto meterThickness = float(visuArea.getWidth() - (channelCount)*meterSpacing) / float(channelCount);
261 auto meterMaxLength = visuArea.getHeight();
262 auto meterLeft = levelMeterArea.getX() + 0.5f * meterSpacing;
263
264 g.setFont(14.0f);
265 for (auto const& channelType : channelsToPaint)
266 {
267 auto level = m_levelData.GetLevel(getChannelNumberForChannelTypeInCurrentConfiguration(channelType));
268 float peakMeterLength{ 0 };
269 float rmsMeterLength{ 0 };
270 float holdMeterLength{ 0 };
271 if (getUsesValuesInDB())
272 {
273 peakMeterLength = meterMaxLength * level.GetFactorPEAKdB();
274 rmsMeterLength = meterMaxLength * level.GetFactorRMSdB();
275 holdMeterLength = meterMaxLength * level.GetFactorHOLDdB();
276 }
277 else
278 {
279 peakMeterLength = meterMaxLength * level.peak;
280 rmsMeterLength = meterMaxLength * level.rms;
281 holdMeterLength = meterMaxLength * level.hold;
282 }
283
284 // peak bar
285 g.setColour(getLookAndFeel().findColour(JUCEAppBasics::CustomLookAndFeel::ColourIds::MeteringPeakColourId));
286 g.fillRect(juce::Rectangle<float>(meterLeft, visuAreaOrigY - peakMeterLength, meterThickness, peakMeterLength));
287 // rms bar
288 g.setColour(getLookAndFeel().findColour(JUCEAppBasics::CustomLookAndFeel::ColourIds::MeteringRmsColourId));
289 g.fillRect(juce::Rectangle<float>(meterLeft, visuAreaOrigY - rmsMeterLength, meterThickness, rmsMeterLength));
290 // hold strip
291 g.setColour(getLookAndFeel().findColour(JUCEAppBasics::CustomLookAndFeel::ColourIds::MeteringHoldColourId));
292 g.drawLine(juce::Line<float>(meterLeft, visuAreaOrigY - holdMeterLength, meterLeft + meterThickness, visuAreaOrigY - holdMeterLength));
293 // channel # label
294 g.setColour(getLookAndFeel().findColour(juce::TextButton::textColourOffId));
295 g.drawText(juce::AudioChannelSet::getAbbreviatedChannelTypeName(channelType), juce::Rectangle<float>(meterLeft - (0.5f * meterSpacing), visuAreaOrigY - float(margin + 2), meterThickness + meterSpacing, float(margin)), juce::Justification::centred);
296
297 meterLeft += meterThickness + meterSpacing;
298 }
299
300 // draw a simple baseline
301 g.setColour(getLookAndFeel().findColour(juce::TextButton::textColourOffId));
302 g.drawLine(juce::Line<float>(levelMeterArea.getX(), visuAreaOrigY, levelMeterArea.getX() + visuArea.getWidth(), visuAreaOrigY));
303}
304
306{
307 // process areas for level indication painting
308 auto coreTwoDFieldOnly = usesPositionedChannels() && !usesPositionedHeightChannels() && !usesDirectionlessChannels();
309 auto coreTwoDFieldWithMeterbridge = usesPositionedChannels() && !usesPositionedHeightChannels() && usesDirectionlessChannels();
310 auto bothTwoDFields = usesPositionedChannels() && usesPositionedHeightChannels() && !usesDirectionlessChannels();
311 auto bothTwoDFieldsWithMeterbridge = usesPositionedChannels() && usesPositionedHeightChannels() && usesDirectionlessChannels();
312
313 auto margin = 12.0f;
314 auto bounds = getLocalBounds().toFloat();
315 auto width = bounds.getWidth();
316 auto height = bounds.getHeight();
317 if (coreTwoDFieldOnly)
318 {
319 m_positionedChannelsArea = bounds.reduced(margin);
320 m_positionedHeightChannelsArea = {};
321 m_directionlessChannelsArea = {};
322 }
323 else if (coreTwoDFieldWithMeterbridge)
324 {
325 m_positionedChannelsArea = bounds.reduced(margin);
326 m_positionedChannelsArea.removeFromRight(width * (1.0f / 11.0f));
327
328 m_positionedHeightChannelsArea = {};
329
330 m_directionlessChannelsArea = bounds;
331 m_directionlessChannelsArea.removeFromLeft(width * (10.0f / 11.0f));
332 }
333 else if (bothTwoDFields)
334 {
335 m_positionedHeightChannelsArea = bounds.reduced(margin);
336 m_positionedHeightChannelsArea.removeFromRight(width * (8.4f / 12.0f));
337 m_positionedHeightChannelsArea.removeFromBottom(height * (5.4f / 10.0f));
338
339 m_positionedChannelsArea = bounds.reduced(margin);
340 m_positionedChannelsArea.removeFromLeft(width * (3.4f / 12.0f));
341 m_positionedChannelsArea.removeFromTop(height * (1.4f / 10.0f));
342
343 m_directionlessChannelsArea = {};
344 }
345 else if (bothTwoDFieldsWithMeterbridge)
346 {
347 m_positionedHeightChannelsArea = bounds.reduced(margin);
348 m_positionedHeightChannelsArea.removeFromRight(width * (8.4f / 13.0f));
349 m_positionedHeightChannelsArea.removeFromBottom(height * (5.4f / 10.0f));
350
351 m_positionedChannelsArea = bounds;
352 m_directionlessChannelsArea = m_positionedChannelsArea.removeFromRight(width * (1.0f / 13.0f));
353 m_positionedChannelsArea.reduce(margin, margin);
354 m_positionedChannelsArea.removeFromLeft(width * (3.4f / 13.0f));
355 m_positionedChannelsArea.removeFromTop(height * (1.4f / 10.0f));
356 }
357
358 for (auto const& channelType : m_clockwiseOrderedChannelTypes)
359 {
360 auto angleRad = juce::degreesToRadians(getAngleForChannelTypeInCurrentConfiguration(channelType));
361 auto xLength = sinf(angleRad) * (m_positionedChannelsArea.getHeight() / 2);
362 auto yLength = cosf(angleRad) * (m_positionedChannelsArea.getWidth() / 2);
363 m_channelLevelMaxPoints[channelType] = m_positionedChannelsArea.getCentre() + juce::Point<float>(xLength, -yLength);
364 }
365
366 for (auto const& channelType : m_clockwiseOrderedHeightChannelTypes)
367 {
368 auto angleRad = juce::degreesToRadians(getAngleForChannelTypeInCurrentConfiguration(channelType));
369 auto xLength = sinf(angleRad) * (m_positionedHeightChannelsArea.getHeight() / 2);
370 auto yLength = cosf(angleRad) * (m_positionedHeightChannelsArea.getWidth() / 2);
371 m_channelHeightLevelMaxPoints[channelType] = m_positionedHeightChannelsArea.getCentre() + juce::Point<float>(xLength, -yLength);
372 }
373
375}
376
378{
379 if(!data)
380 return;
381
382 switch(data->GetDataType())
383 {
385 m_levelData = *(static_cast<ProcessorLevelData*>(data));
387 break;
391 default:
392 break;
393 }
394}
395
396
397}
Base class for all audio-data visualisation components in the Mema processor editor.
void paint(Graphics &) override
Paints the visualiser background.
void notifyChanges()
Marks that new data is available and triggers a repaint on the next timer tick.
void setUsesValuesInDB(bool useValuesInDB)
void resized() override
Lays out child components.
Base class for all data objects exchanged between the audio processor and its analyzers/visualisers.
@ Invalid
Uninitialised or unknown data.
@ AudioSignal
Raw audio buffer data.
@ Level
Peak/RMS/hold level metering data.
@ Spectrum
FFT frequency-spectrum data.
Type GetDataType()
Returns the concrete type of this data object.
LevelVal GetLevel(unsigned long channel)
void processingDataChanged(AbstractProcessorData *data) override
Definition Mema.cpp:27