@@ -127,6 +127,92 @@ class AudioFileWaveform : public yup::AudioViewComponent
127127
128128// ==============================================================================
129129
130+ /* *
131+ Wraps an AudioSource and taps the audio stream to send to a KMeterState.
132+ */
133+ class MeteringAudioSource : public yup ::PositionableAudioSource
134+ {
135+ public:
136+ MeteringAudioSource (yup::PositionableAudioSource* sourceToWrap, yup::KMeterState& meterState)
137+ : source (sourceToWrap)
138+ , meter (meterState)
139+ {
140+ }
141+
142+ void prepareToPlay (int samplesPerBlockExpected, double newSampleRate) override
143+ {
144+ if (source != nullptr )
145+ source->prepareToPlay (samplesPerBlockExpected, newSampleRate);
146+
147+ meter.prepare (newSampleRate, 2 );
148+ }
149+
150+ void releaseResources () override
151+ {
152+ if (source != nullptr )
153+ source->releaseResources ();
154+ }
155+
156+ void getNextAudioBlock (const yup::AudioSourceChannelInfo& bufferToFill) override
157+ {
158+ if (source != nullptr )
159+ {
160+ source->getNextAudioBlock (bufferToFill);
161+
162+ // Tap the audio and push to meter
163+ const int numChannels = yup::jmin (bufferToFill.buffer ->getNumChannels (), 2 );
164+ if (numChannels > 0 && bufferToFill.numSamples > 0 )
165+ {
166+ const float * channels[2 ] = { nullptr , nullptr };
167+ for (int i = 0 ; i < numChannels; ++i)
168+ channels[i] = bufferToFill.buffer ->getReadPointer (i, bufferToFill.startSample );
169+
170+ meter.pushSamples (channels, numChannels, bufferToFill.numSamples );
171+ meter.processPendingAudio ();
172+ }
173+ }
174+ else
175+ {
176+ bufferToFill.clearActiveBufferRegion ();
177+ }
178+ }
179+
180+ void setNextReadPosition (yup::int64 newPosition) override
181+ {
182+ if (source != nullptr )
183+ source->setNextReadPosition (newPosition);
184+ }
185+
186+ yup::int64 getNextReadPosition () const override
187+ {
188+ return source != nullptr ? source->getNextReadPosition () : 0 ;
189+ }
190+
191+ yup::int64 getTotalLength () const override
192+ {
193+ return source != nullptr ? source->getTotalLength () : 0 ;
194+ }
195+
196+ bool isLooping () const override
197+ {
198+ return source != nullptr ? source->isLooping () : false ;
199+ }
200+
201+ void setLooping (bool shouldLoop) override
202+ {
203+ if (source != nullptr )
204+ source->setLooping (shouldLoop);
205+ }
206+
207+ private:
208+ yup::PositionableAudioSource* source;
209+ yup::KMeterState& meter;
210+
211+ YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MeteringAudioSource)
212+ };
213+
214+ // ==============================================================================
215+
130216/* *
131217 Demonstrates loading, visualizing, playing, and exporting audio files.
132218*/
@@ -143,13 +229,22 @@ class AudioFileDemo : public yup::Component
143229 , loopButton (" Loop" )
144230 , labelsButton (" Labels" )
145231 , waveformDisplay (waveformCache)
232+ , meterState (48000.0 , 2 )
233+ , leftMeter (meterState, 0 )
234+ , rightMeter (meterState, 1 )
235+ , meteringSource (&transportSource, meterState)
146236 {
147237 formatManager.registerDefaultFormats (
148238 yup::AudioFormatType::all & ~yup::AudioFormatType::coreAudio);
149239
150240 deviceManager.initialiseWithDefaultDevices (0 , 2 );
151241 deviceManager.addAudioCallback (&sourcePlayer);
152- sourcePlayer.setSource (&transportSource);
242+ sourcePlayer.setSource (&meteringSource);
243+
244+ // Configure K-Meters
245+ meterState.setScale (yup::KMeterState::Scale::k20);
246+ leftMeter.setShowPeakHold (true );
247+ rightMeter.setShowPeakHold (true );
153248
154249 // Configure the waveform cache
155250 waveformCache->setThreadPool (&waveformThreadPool);
@@ -161,6 +256,7 @@ class AudioFileDemo : public yup::Component
161256 {
162257 stopPlayback ();
163258 transportSource.setSource (nullptr );
259+ meteringSource.setLooping (false );
164260 sourcePlayer.setSource (nullptr );
165261 deviceManager.removeAudioCallback (&sourcePlayer);
166262 deviceManager.closeAudioDevice ();
@@ -169,12 +265,15 @@ class AudioFileDemo : public yup::Component
169265 void resized () override
170266 {
171267 auto bounds = getLocalBounds ().reduced (8 );
172- auto header = bounds.removeFromTop (38 );
268+ auto header = bounds.removeFromTop (122 );
173269
174270 const int buttonHeight = 28 ;
175271 const int buttonWidth = 100 ;
272+ const int smallButtonWidth = 60 ;
273+ const int mediumButtonWidth = 85 ;
176274 const int buttonMargin = 6 ;
177275
276+ // First row of buttons
178277 auto buttonRow = header.removeFromTop (buttonHeight);
179278 loadButton.setBounds (buttonRow.removeFromLeft (buttonWidth));
180279 buttonRow.removeFromLeft (buttonMargin);
@@ -188,11 +287,48 @@ class AudioFileDemo : public yup::Component
188287 buttonRow.removeFromLeft (buttonMargin);
189288 labelsButton.setBounds (buttonRow.removeFromLeft (buttonWidth));
190289
290+ // Second row for K-scale buttons
291+ header.removeFromTop (4 );
292+ auto scaleRow = header.removeFromTop (buttonHeight);
293+ k20Button.setBounds (scaleRow.removeFromLeft (smallButtonWidth));
294+ scaleRow.removeFromLeft (buttonMargin);
295+ k14Button.setBounds (scaleRow.removeFromLeft (smallButtonWidth));
296+ scaleRow.removeFromLeft (buttonMargin);
297+ k12Button.setBounds (scaleRow.removeFromLeft (smallButtonWidth));
298+
299+ // Third row for metering standard buttons
300+ header.removeFromTop (4 );
301+ auto standardRow = header.removeFromTop (buttonHeight);
302+ rmsButton.setBounds (standardRow.removeFromLeft (mediumButtonWidth));
303+ standardRow.removeFromLeft (buttonMargin);
304+ ituButton.setBounds (standardRow.removeFromLeft (mediumButtonWidth));
305+ standardRow.removeFromLeft (buttonMargin);
306+ ebuButton.setBounds (standardRow.removeFromLeft (mediumButtonWidth));
307+
308+ // Fourth row for over counter mode buttons
309+ header.removeFromTop (4 );
310+ auto modeRow = header.removeFromTop (buttonHeight);
311+ contiguousButton.setBounds (modeRow.removeFromLeft (mediumButtonWidth));
312+ modeRow.removeFromLeft (buttonMargin);
313+ totalButton.setBounds (modeRow.removeFromLeft (mediumButtonWidth));
314+
191315 bounds.removeFromTop (6 );
192316 infoLabel.setBounds (bounds.removeFromTop (22 ));
193317 statusLabel.setBounds (bounds.removeFromTop (22 ));
194318 bounds.removeFromTop (6 );
195319
320+ // Reserve space for K-Meters on the right
321+ const int meterWidth = 60 ;
322+ const int meterGap = 8 ;
323+ const int meterSectionWidth = (meterWidth * 2 ) + (meterGap * 3 );
324+
325+ auto meterArea = bounds.removeFromRight (meterSectionWidth);
326+ leftMeter.setBounds (meterArea.removeFromLeft (meterWidth));
327+ meterArea.removeFromLeft (meterGap);
328+ rightMeter.setBounds (meterArea.removeFromLeft (meterWidth));
329+
330+ // Rest is for waveform
331+ bounds.removeFromRight (meterGap);
196332 waveformDisplay.setBounds (bounds);
197333 }
198334
@@ -218,6 +354,9 @@ class AudioFileDemo : public yup::Component
218354private:
219355 void setupUi ()
220356 {
357+ addAndMakeVisible (leftMeter);
358+ addAndMakeVisible (rightMeter);
359+
221360 addAndMakeVisible (loadButton);
222361 loadButton.onClick = [this ]
223362 {
@@ -267,6 +406,107 @@ class AudioFileDemo : public yup::Component
267406 waveformDisplay.setChannelLabelsVisible (labelsButton.getToggleState ());
268407 };
269408
409+ // K-Scale selection buttons (manual radio button behavior)
410+ addAndMakeVisible (k20Button);
411+ k20Button.setButtonText (" K-20" );
412+ k20Button.setToggleState (true , yup::NotificationType::dontSendNotification);
413+ k20Button.onClick = [this ]
414+ {
415+ k20Button.setToggleState (true , yup::NotificationType::dontSendNotification);
416+ k14Button.setToggleState (false , yup::NotificationType::dontSendNotification);
417+ k12Button.setToggleState (false , yup::NotificationType::dontSendNotification);
418+ meterState.setScale (yup::KMeterState::Scale::k20);
419+ leftMeter.repaint ();
420+ rightMeter.repaint ();
421+ };
422+
423+ addAndMakeVisible (k14Button);
424+ k14Button.setButtonText (" K-14" );
425+ k14Button.setToggleState (false , yup::NotificationType::dontSendNotification);
426+ k14Button.onClick = [this ]
427+ {
428+ k20Button.setToggleState (false , yup::NotificationType::dontSendNotification);
429+ k14Button.setToggleState (true , yup::NotificationType::dontSendNotification);
430+ k12Button.setToggleState (false , yup::NotificationType::dontSendNotification);
431+ meterState.setScale (yup::KMeterState::Scale::k14);
432+ leftMeter.repaint ();
433+ rightMeter.repaint ();
434+ };
435+
436+ addAndMakeVisible (k12Button);
437+ k12Button.setButtonText (" K-12" );
438+ k12Button.setToggleState (false , yup::NotificationType::dontSendNotification);
439+ k12Button.onClick = [this ]
440+ {
441+ k20Button.setToggleState (false , yup::NotificationType::dontSendNotification);
442+ k14Button.setToggleState (false , yup::NotificationType::dontSendNotification);
443+ k12Button.setToggleState (true , yup::NotificationType::dontSendNotification);
444+ meterState.setScale (yup::KMeterState::Scale::k12);
445+ leftMeter.repaint ();
446+ rightMeter.repaint ();
447+ };
448+
449+ // Metering standard selection buttons
450+ addAndMakeVisible (rmsButton);
451+ rmsButton.setButtonText (" RMS Flat" );
452+ rmsButton.setToggleState (true , yup::NotificationType::dontSendNotification);
453+ rmsButton.onClick = [this ]
454+ {
455+ rmsButton.setToggleState (true , yup::NotificationType::dontSendNotification);
456+ ituButton.setToggleState (false , yup::NotificationType::dontSendNotification);
457+ ebuButton.setToggleState (false , yup::NotificationType::dontSendNotification);
458+ meterState.setMeteringStandard (yup::KMeterState::MeteringStandard::rmsFlat);
459+ leftMeter.repaint ();
460+ rightMeter.repaint ();
461+ };
462+
463+ addAndMakeVisible (ituButton);
464+ ituButton.setButtonText (" ITU BS.1770-4" );
465+ ituButton.setToggleState (false , yup::NotificationType::dontSendNotification);
466+ ituButton.onClick = [this ]
467+ {
468+ rmsButton.setToggleState (false , yup::NotificationType::dontSendNotification);
469+ ituButton.setToggleState (true , yup::NotificationType::dontSendNotification);
470+ ebuButton.setToggleState (false , yup::NotificationType::dontSendNotification);
471+ meterState.setMeteringStandard (yup::KMeterState::MeteringStandard::ituBS1770_4);
472+ leftMeter.repaint ();
473+ rightMeter.repaint ();
474+ };
475+
476+ addAndMakeVisible (ebuButton);
477+ ebuButton.setButtonText (" EBU R128" );
478+ ebuButton.setToggleState (false , yup::NotificationType::dontSendNotification);
479+ ebuButton.onClick = [this ]
480+ {
481+ rmsButton.setToggleState (false , yup::NotificationType::dontSendNotification);
482+ ituButton.setToggleState (false , yup::NotificationType::dontSendNotification);
483+ ebuButton.setToggleState (true , yup::NotificationType::dontSendNotification);
484+ meterState.setMeteringStandard (yup::KMeterState::MeteringStandard::ebuR128);
485+ leftMeter.repaint ();
486+ rightMeter.repaint ();
487+ };
488+
489+ // Over counter mode selection buttons
490+ addAndMakeVisible (contiguousButton);
491+ contiguousButton.setButtonText (" Contiguous" );
492+ contiguousButton.setToggleState (true , yup::NotificationType::dontSendNotification);
493+ contiguousButton.onClick = [this ]
494+ {
495+ contiguousButton.setToggleState (true , yup::NotificationType::dontSendNotification);
496+ totalButton.setToggleState (false , yup::NotificationType::dontSendNotification);
497+ meterState.setOverCounterMode (yup::KMeterState::OverCounterMode::contiguous);
498+ };
499+
500+ addAndMakeVisible (totalButton);
501+ totalButton.setButtonText (" Total" );
502+ totalButton.setToggleState (false , yup::NotificationType::dontSendNotification);
503+ totalButton.onClick = [this ]
504+ {
505+ contiguousButton.setToggleState (false , yup::NotificationType::dontSendNotification);
506+ totalButton.setToggleState (true , yup::NotificationType::dontSendNotification);
507+ meterState.setOverCounterMode (yup::KMeterState::OverCounterMode::total);
508+ };
509+
270510 addAndMakeVisible (infoLabel);
271511 infoLabel.setText (" No audio loaded." , yup::NotificationType::dontSendNotification);
272512 infoLabel.setColor (yup::Label::Style::textFillColorId, yup::Colors::white);
@@ -454,11 +694,25 @@ class AudioFileDemo : public yup::Component
454694 yup::TextButton saveButton;
455695 yup::ToggleButton loopButton;
456696 yup::ToggleButton labelsButton;
697+ yup::ToggleButton k20Button;
698+ yup::ToggleButton k14Button;
699+ yup::ToggleButton k12Button;
700+ yup::ToggleButton rmsButton;
701+ yup::ToggleButton ituButton;
702+ yup::ToggleButton ebuButton;
703+ yup::ToggleButton contiguousButton;
704+ yup::ToggleButton totalButton;
457705
458706 yup::Label infoLabel;
459707 yup::Label statusLabel;
460708 AudioFileWaveform waveformDisplay;
461709
710+ // K-Meter components
711+ yup::KMeterState meterState;
712+ yup::KMeterComponent leftMeter;
713+ yup::KMeterComponent rightMeter;
714+ MeteringAudioSource meteringSource;
715+
462716 yup::String currentFileName { " No audio loaded" };
463717 double loadedSampleRate = 0.0 ;
464718 double audioLengthSeconds = 0.0 ;
0 commit comments