Mema
Memory Matrix — multi-channel audio matrix monitor and router
Loading...
Searching...
No Matches
SpectrumAudioComponent.cpp
Go to the documentation of this file.
1/* Copyright (c) 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 "../MemaMoAppConfiguration.h" // include to enable trigger cfg dump
22
23#include <CustomLookAndFeel.h>
24
25
26namespace Mema
27{
28
29
30//==============================================================================
33{
35
36 m_chNumSelButton = std::make_unique<juce::DrawableButton>("SelectChannelcount", juce::DrawableButton::ButtonStyle::ImageFitted);
37 m_chNumSelButton->setTooltip("Select number of visible channels.");
38 m_chNumSelButton->onClick = [this] {
39 juce::PopupMenu settingsMenu;
40 for (int i = 1; i <= m_numAvailableChannels; i++)
41 settingsMenu.addItem(i, juce::String(i));
42 settingsMenu.showMenuAsync(juce::PopupMenu::Options(), [=](int selectedId) {
43 setNumVisibleChannels(selectedId);
44 if (JUCEAppBasics::AppConfigurationBase::getInstance())
45 JUCEAppBasics::AppConfigurationBase::getInstance()->triggerConfigurationDump(false);
46 });
47 };
48 m_chNumSelButton->setAlwaysOnTop(true);
49 m_chNumSelButton->setColour(juce::DrawableButton::ColourIds::backgroundColourId, juce::Colours::transparentBlack);
50 m_chNumSelButton->setColour(juce::DrawableButton::ColourIds::backgroundOnColourId, juce::Colours::transparentBlack);
51 addAndMakeVisible(m_chNumSelButton.get());
52
54}
55
59
60void SpectrumAudioComponent::paint(juce::Graphics & g)
61{
63
64 g.fillAll(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId));
65
66 // calculate what we need for our visualization area
67 auto width = getWidth();
68 auto height = getHeight();
69 auto outerMargin = 6;
70 auto channelMargin = outerMargin; // margin between channel plots
71 auto channelLabelWidth = 20; // width for channel number area
72 auto numVisibleChannels = getNumVisibleChannels();
73
74 if (numVisibleChannels == 0)
75 return;
76
77 auto totalWidth = width - 2 * outerMargin - channelLabelWidth;
78 auto totalHeight = height - 2 * outerMargin - outerMargin; // space for legend at bottom
79
80 // Calculate height per channel plot including margins
81 auto totalChannelMargins = (numVisibleChannels - 1) * channelMargin;
82 auto availableHeightForPlots = totalHeight - totalChannelMargins;
83 auto channelPlotHeight = availableHeightForPlots / numVisibleChannels;
84
85 auto maxPlotFreq = 20000.0f;
86 auto minPlotFreq = 20.0f;
87
88 // Calculate the log scale for frequency mapping
89 auto logScaleMin = log10(minPlotFreq);
90 auto logScaleMax = log10(maxPlotFreq);
91 auto logScaleRange = logScaleMax - logScaleMin;
92
93 auto holdColour = getLookAndFeel().findColour(JUCEAppBasics::CustomLookAndFeel::MeteringHoldColourId);
94 auto peakColour = getLookAndFeel().findColour(JUCEAppBasics::CustomLookAndFeel::MeteringPeakColourId);
95 auto markerColour = getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId);
96 auto legendColour = getLookAndFeel().findColour(juce::DrawableButton::textColourOnId);
97
98 // Draw each channel in its own plot area
99 for (int channelIdx = 0; channelIdx < numVisibleChannels; ++channelIdx)
100 {
101 if (channelIdx >= m_plotPoints.size())
102 break;
103
104 auto const& plotPoints = m_plotPoints[channelIdx];
105
106 // Calculate this channel's plot area
107 auto visuAreaY = outerMargin + channelIdx * (channelPlotHeight + channelMargin);
108 juce::Rectangle<int> visuArea(outerMargin, visuAreaY, totalWidth, channelPlotHeight);
109
110 auto visuAreaOrigX = float(outerMargin);
111 auto visuAreaOrigY = float(visuAreaY + channelPlotHeight);
112 auto visuAreaWidth = totalWidth;
113 auto visuAreaHeight = channelPlotHeight;
114
115 // Fill this channel's visualization area background
116 g.setColour(getLookAndFeel().findColour(juce::Slider::backgroundColourId));
117 g.fillRect(visuArea);
118
119 // Draw channel number area on the right
120 juce::Rectangle<int> channelLabelArea(outerMargin + totalWidth, visuAreaY, channelLabelWidth, channelPlotHeight);
121 g.setColour(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId));
122 g.fillRect(channelLabelArea);
123
124 // Draw channel number
125 g.setFont(12.0f);
126 g.setColour(legendColour);
127 g.drawText(juce::String(channelIdx + 1),
128 channelLabelArea.toFloat(),
129 juce::Justification::centred, true);
130
131 // Draw marker lines
132 auto markerLineValues = std::vector<float>{ 20.f, 30.f, 40.f, 50.f, 60.f, 70.f, 80.f, 90.f, 100.f, 200.f, 300.f, 400.f, 500.f, 600.f, 700.f, 800.f, 900.f, 1000.f, 2000.f, 3000.f, 4000.f, 5000.f, 6000.f, 7000.f, 8000.f, 9000.f, 10000.f, 20000.f };
133
134 for (auto i = 0; i < markerLineValues.size(); ++i)
135 {
136 auto skewedProportionX = (log10(markerLineValues.at(i)) - logScaleMin) / logScaleRange;
137 auto posX = visuAreaOrigX + (static_cast<float>(visuAreaWidth) * skewedProportionX);
138 g.setColour(markerColour);
139 g.drawLine(juce::Line<float>(posX, visuAreaOrigY, posX, visuAreaOrigY - visuAreaHeight));
140 }
141
142 // Draw dBFS label
143 g.setFont(12.0f);
144 g.setColour(getLookAndFeel().findColour(juce::LookAndFeel_V4::ColourScheme::menuText));
145 g.drawText(juce::String(ProcessorDataAnalyzer::getGlobalMindB()) + " ... " + juce::String(ProcessorDataAnalyzer::getGlobalMaxdB()) + " dBFS",
146 juce::Rectangle<float>(visuAreaOrigX + visuAreaWidth - 120.0f, float(visuAreaY), 110.0f, float(outerMargin)),
147 juce::Justification::centred, true);
148
149 // Draw spectrum curve for this channel
150 if (!plotPoints.peaks.empty() && plotPoints.holds.size() == plotPoints.peaks.size())
151 {
152 // Helper lambda to calculate frequency for a given band index (logarithmic mapping)
153 auto getBandFrequency = [&plotPoints](int bandIndex) -> float
154 {
155 if (plotPoints.minFreq <= 0.0f || plotPoints.maxFreq <= plotPoints.minFreq)
156 return 0.0f;
157
158 float ratio = plotPoints.maxFreq / plotPoints.minFreq;
159 float t = static_cast<float>(bandIndex) / (plotPoints.peaks.size() - 1);
160 return plotPoints.minFreq * std::pow(ratio, t);
161 };
162
163 // Helper lambda to convert frequency to screen X position (logarithmic scale)
164 auto frequencyToScreenX = [&](float frequency) -> float
165 {
166 if (frequency <= 0.0f)
167 return visuAreaOrigX;
168
169 float skewedProportionX = (log10(frequency) - logScaleMin) / logScaleRange;
170 return visuAreaOrigX + (static_cast<float>(visuAreaWidth) * skewedProportionX);
171 };
172
173 // Find the band indices that correspond to our plot frequency range
174 int minPlotIdx = -1;
175 int maxPlotIdx = -1;
176
177 for (int i = 0; i < plotPoints.peaks.size(); ++i)
178 {
179 float bandFreq = getBandFrequency(i);
180
181 if (minPlotIdx == -1 && bandFreq >= minPlotFreq)
182 minPlotIdx = i;
183
184 if (bandFreq <= maxPlotFreq)
185 maxPlotIdx = i;
186 }
187
188 // Ensure valid indices
189 if (minPlotIdx == -1)
190 minPlotIdx = 0;
191 if (maxPlotIdx == -1 || maxPlotIdx >= plotPoints.peaks.size())
192 maxPlotIdx = static_cast<int>(plotPoints.peaks.size() - 1);
193
194 if (minPlotIdx <= maxPlotIdx)
195 {
196 // Build hold and peak paths
197 auto holdPath = juce::Path{};
198 auto peakPath = juce::Path{};
199
200 // Start paths at first band
201 float startFreq = getBandFrequency(minPlotIdx);
202 float startX = frequencyToScreenX(startFreq);
203 float startHoldY = visuAreaOrigY - plotPoints.holds.at(minPlotIdx) * visuAreaHeight;
204 float startPeakY = visuAreaOrigY - plotPoints.peaks.at(minPlotIdx) * visuAreaHeight;
205
206 holdPath.startNewSubPath(juce::Point<float>(startX, startHoldY));
207 peakPath.startNewSubPath(juce::Point<float>(startX, startPeakY));
208
209 // Add remaining bands
210 for (int i = minPlotIdx + 1; i <= maxPlotIdx; ++i)
211 {
212 float bandFreq = getBandFrequency(i);
213 float pointX = frequencyToScreenX(bandFreq);
214
215 float holdY = visuAreaOrigY - plotPoints.holds.at(i) * visuAreaHeight;
216 holdPath.lineTo(juce::Point<float>(pointX, holdY));
217
218 float peakY = visuAreaOrigY - plotPoints.peaks.at(i) * visuAreaHeight;
219 peakPath.lineTo(juce::Point<float>(pointX, peakY));
220 }
221
222 g.setColour(holdColour);
223 g.strokePath(holdPath, juce::PathStrokeType(1));
224 g.setColour(peakColour);
225 g.strokePath(peakPath, juce::PathStrokeType(3));
226 }
227 }
228 }
229
230 // Draw frequency legend at the bottom
231 auto markerLegendValues = std::map<float, std::string>{ {20.f, "20"}, {100.f, "100"}, {1000.f, "1k"}, {10000.f, "10k"}, {20000.f, "20k"} };
232 auto legendValueWidth = 40.0f;
233 auto legendY = height - outerMargin - outerMargin;
234
235 for (auto const& [freq, label] : markerLegendValues)
236 {
237 auto skewedProportionX = (log10(freq) - logScaleMin) / logScaleRange;
238 auto posX = float(outerMargin) + (static_cast<float>(totalWidth) * skewedProportionX);
239
240 g.setColour(legendColour);
241 g.drawText(label,
242 juce::Rectangle<float>(posX - 0.5f * legendValueWidth, float(legendY), legendValueWidth, float(outerMargin)),
243 juce::Justification::centred, true);
244 }
245}
246
248{
249 auto bounds = getLocalBounds();
250 auto visuArea = bounds;
251 auto legendArea = visuArea.removeFromRight(m_legendWidth);
252 ignoreUnused(legendArea);
253
254 if (m_chNumSelButton)
255 m_chNumSelButton->setBounds(bounds.removeFromTop(22).removeFromRight(22));
256
258}
259
261{
262 auto chNumSelButtonDrawable = juce::Drawable::createFromSVG(*juce::XmlDocument::parse(BinaryData::waves24px_svg).get());
263 chNumSelButtonDrawable->replaceColour(juce::Colours::black, getLookAndFeel().findColour(juce::TextButton::ColourIds::textColourOnId));
264 m_chNumSelButton->setImages(chNumSelButtonDrawable.get());
265}
266
268{
269 if (!data)
270 return;
271
272 switch (data->GetDataType())
273 {
275 {
276 ProcessorSpectrumData* sd = static_cast<ProcessorSpectrumData*>(data);
277 if (nullptr != sd && sd->GetChannelCount() > 0)
278 {
279 if (m_numAvailableChannels != int(sd->GetChannelCount()))
280 {
281 auto init = (0 == m_numAvailableChannels) && (1 == m_numVisibleChannels);
282 m_numAvailableChannels = int(sd->GetChannelCount());
283 if (init)
284 setNumVisibleChannels(m_numAvailableChannels);
285 }
286
287 for (auto i = 0; i < getNumVisibleChannels(); i++)
288 {
289 auto spectrum = sd->GetSpectrum(i);
290 if (spectrum.freqRes <= 0)
291 continue;
292
293 if (m_plotPoints[i].peaks.size() != spectrum.count)
294 m_plotPoints[i].peaks.resize(spectrum.count);
295 memcpy(&m_plotPoints[i].peaks[0], &spectrum.bandsPeak[0], spectrum.count * sizeof(float));
296
297 if (m_plotPoints[i].holds.size() != spectrum.count)
298 m_plotPoints[i].holds.resize(spectrum.count);
299 memcpy(&m_plotPoints[i].holds[0], &spectrum.bandsHold[0], spectrum.count * sizeof(float));
300
301 m_plotPoints[i].minFreq = spectrum.minFreq;
302 m_plotPoints[i].maxFreq = spectrum.maxFreq;
303 m_plotPoints[i].freqRes = spectrum.freqRes;
304 }
305 }
306 else
307 break;
308
310 break;
311 }
315 default:
316 break;
317 }
318}
319
321{
322 m_numVisibleChannels = numChannels;
323 m_plotPoints.resize(numChannels);
324}
325
327{
328 return m_numVisibleChannels;
329}
330
331
332}
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 setRefreshFrequency(int frequency)
Sets the display refresh rate in Hz.
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.
unsigned long GetChannelCount() override
Returns the number of audio channels this data object covers.
const SpectrumBands & GetSpectrum(unsigned long channel)
void setNumVisibleChannels(int numChannels)
void processingDataChanged(AbstractProcessorData *data) override
void paint(juce::Graphics &) override
Definition Mema.cpp:27