Hello everyone. I have a task: stop the player if the audio file’s volume is lower than, say, -18 dB. How can I measure the volume (without using the volume slider)?
Thanks in advance for your help.
To measure the volume of some audio, you choose your ‘ballistics’. JUCE has ‘peak’ and RMS built in, but there are other weightings/standards you can use if you like.
I took the Audio Player tutorial from JUCE to demo how you can do this.
A quick-and-dirty RMS per block is simple. However, the measurement is ‘reset’ every block so it’s not really the right way to do it.
void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) override
{
if (readerSource.get() == nullptr)
{
bufferToFill.clearActiveBufferRegion();
return;
}
transportSource.getNextAudioBlock (bufferToFill);
if (transportSource.isPlaying())
{
stopTransportBelowThresholdRms(bufferToFill.buffer);
}
}
void stopTransportBelowThresholdRms(const AudioBuffer<float>* buf, float thresholdDb = -36.f)
{
// Average the RMS levels across the channels (can also do min/max if you prefer)
float totalRmsLevel{};
for ( auto channel{0}; channel < buf->getNumChannels(); ++channel )
{
totalRmsLevel += buf->getRMSLevel(channel, 0, buf->getNumSamples());
}
jassert(buf->getNumChannels() != 0);
const auto averageRmsLevelGain = totalRmsLevel / buf->getNumChannels();
const auto averageRmsLevelDb = Decibels::gainToDecibels(averageRmsLevelGain);
// Assuming many files start with silence, we need to give the audio a chance
// to start before we stop it.
// audioStarted is a member bool used just to store state for this function.
if (audioStarted)
{
if (averageRmsLevelDb < thresholdDb)
{
// Not shown here - this should be queued onto the message thread
// instead of blocking the audio thread here
changeState(Stopping);
// You might also want to implement a short fade-out to avoid a click
audioStarted = false;
}
}
else
{
if (averageRmsLevelDb > thresholdDb)
{
audioStarted = true;
}
}
}
For peak calculations, we can use the juce_dsp module. This is more complicated since we need to maintain the state of an envelope follower but it will give us a proper running envelope over the whole audio file. You can set it to peak or RMS too.
dependencies: juce_dsp
#include <juce_dsp/juce_dsp.h>
void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override
{
transportSource.prepareToPlay (samplesPerBlockExpected, sampleRate);
// envelope is a member dsp::BallisticsFilter<float>
envelope.setAttackTime(40.f);
envelope.setReleaseTime(40.f);
envelope.setLevelCalculationType(dsp::BallisticsFilterLevelCalculationType::peak);
// numChannelsInAudioFile is a member uint32
// I think you'll need to populate numChannelsInAudioFile when you open the file
envelope.prepare(dsp::ProcessSpec{sampleRate, static_cast<uint32>(samplesPerBlockExpected), numChannelsInAudioFile});
// envelopeBuffer is a member AudioBuffer<float> envelopeBuffer
// It will be used to store the running level of the audio
envelopeBuffer.setSize(numChannelsInAudioFile, samplesPerBlockExpected);
}
void getNextAudioBlock (const juce::AudioSourceChannelInfo& bufferToFill) override
{
if (readerSource.get() == nullptr)
{
bufferToFill.clearActiveBufferRegion();
return;
}
transportSource.getNextAudioBlock (bufferToFill);
if (transportSource.isPlaying())
{
stopTransportBelowThresholdEnvelope(bufferToFill.buffer);
}
}
void stopTransportBelowThresholdEnvelope(const AudioBuffer<float>* buf, float thresholdDb = -18.f)
{
// Create AudioBlocks to view the audio buffer and the envelope buffer
dsp::AudioBlock<const float> blockBuf{ *buf };
dsp::AudioBlock<float> blockEnv{ envelopeBuffer };
// Define a processing context for the envelope processor
dsp::ProcessContextNonReplacing<float> processContext{blockBuf, blockEnv};
// The envelope processes the audio buffer and writes the results into the envelope buffer
envelope.process(processContext);
// Average the peak levels across the channels
float totalPeakLevel{};
for ( auto channel{0}; channel < buf->getNumChannels(); ++channel )
{
const auto maxLevelPeakDb = blockEnv.findMinAndMax().getEnd();
totalPeakLevel += maxLevelPeakDb;
}
jassert(buf->getNumChannels() != 0);
const auto averagePeakLevelGain = totalPeakLevel / buf->getNumChannels();
// Convert peak level to dB
const auto averagePeakLevelDb = Decibels::gainToDecibels(averagePeakLevelGain);
// Assuming many files start with silence, we need to give the audio a chance
// to start before we stop it.
// audioStarted is a member variable used just to store state for this function.
if (audioStarted)
{
if (averagePeakLevelDb < thresholdDb)
{
// Not shown here - this should be queued onto the message thread
// instead of blocking the audio thread here
changeState(Stopping);
// You might also want to implement a short fade-out to avoid a click
audioStarted = false;
}
}
else
{
if (averagePeakLevelDb > thresholdDb)
{
audioStarted = true;
}
}
}
Check the docs/tutorials for more on how to use the DSP module.
Hope that helps!
Could you maybe clarify:
What player? Are you developing a player, is this an existing player or is this part of the OS?
Do you want to stop when the measured volume is below -18 dB or when the selected volume is below a certain threshold?
What audio is played back? Is this material you control or does this come from arbitrary local or streamed sources?
It sounds to me like you want to create either a player that saves bandwidth when nobody is listening, or that the user cannot skip adverts when the sound is muted…? Just guessing…
Thanks, Adam. I’ll try.
Hi.
I’m developing a standard player for playing audio files from a disk. I want to implement a crossfade, but I don’t yet know how to analyze the file. Ideally, the player needs to receive information that the audio ends, say, at 3:02, followed by silence, and the crossfade should start at 2:58, while the next file should be playing. If the file ends with a fade-out, then when the volume drops, say by -18 dB, the next file should be played.
