1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00
JUCE/examples/Demo/Source/Demos/AudioLatencyDemo.cpp
2015-07-22 15:59:34 +01:00

370 lines
13 KiB
C++

/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2015 - ROLI Ltd.
Permission is granted to use this software under the terms of either:
a) the GPL v2 (or any later version)
b) the Affero GPL v3
Details of these licenses can be found at: www.gnu.org/licenses
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
------------------------------------------------------------------------------
To release a closed-source product which uses JUCE, commercial licenses are
available: visit www.juce.com for more information.
==============================================================================
*/
#include "../JuceDemoHeader.h"
#include "AudioLiveScrollingDisplay.h"
class LatencyTester : public AudioIODeviceCallback,
private Timer
{
public:
LatencyTester (TextEditor& resultsBox_)
: playingSampleNum (0),
recordedSampleNum (-1),
sampleRate (0),
testIsRunning (false),
resultsBox (resultsBox_)
{
MainAppWindow::getSharedAudioDeviceManager().addAudioCallback (this);
}
~LatencyTester()
{
MainAppWindow::getSharedAudioDeviceManager().removeAudioCallback (this);
}
//==============================================================================
void beginTest()
{
resultsBox.moveCaretToEnd();
resultsBox.insertTextAtCaret (newLine + newLine + "Starting test..." + newLine);
resultsBox.moveCaretToEnd();
startTimer (50);
const ScopedLock sl (lock);
createTestSound();
recordedSound.clear();
playingSampleNum = recordedSampleNum = 0;
testIsRunning = true;
}
void timerCallback()
{
if (testIsRunning && recordedSampleNum >= recordedSound.getNumSamples())
{
testIsRunning = false;
stopTimer();
// Test has finished, so calculate the result..
const int latencySamples = calculateLatencySamples();
resultsBox.moveCaretToEnd();
resultsBox.insertTextAtCaret (getMessageDescribingResult (latencySamples));
resultsBox.moveCaretToEnd();
}
}
String getMessageDescribingResult (int latencySamples)
{
String message;
if (latencySamples >= 0)
{
message << newLine
<< "Results:" << newLine
<< latencySamples << " samples (" << String (latencySamples * 1000.0 / sampleRate, 1)
<< " milliseconds)" << newLine
<< "The audio device reports an input latency of "
<< deviceInputLatency << " samples, output latency of "
<< deviceOutputLatency << " samples." << newLine
<< "So the corrected latency = "
<< (latencySamples - deviceInputLatency - deviceOutputLatency)
<< " samples (" << String ((latencySamples - deviceInputLatency - deviceOutputLatency) * 1000.0 / sampleRate, 2)
<< " milliseconds)";
}
else
{
message << newLine
<< "Couldn't detect the test signal!!" << newLine
<< "Make sure there's no background noise that might be confusing it..";
}
return message;
}
//==============================================================================
void audioDeviceAboutToStart (AudioIODevice* device)
{
testIsRunning = false;
playingSampleNum = recordedSampleNum = 0;
sampleRate = device->getCurrentSampleRate();
deviceInputLatency = device->getInputLatencyInSamples();
deviceOutputLatency = device->getOutputLatencyInSamples();
recordedSound.setSize (1, (int) (0.9 * sampleRate));
recordedSound.clear();
}
void audioDeviceStopped()
{
// (nothing to do here)
}
void audioDeviceIOCallback (const float** inputChannelData,
int numInputChannels,
float** outputChannelData,
int numOutputChannels,
int numSamples)
{
const ScopedLock sl (lock);
if (testIsRunning)
{
float* const recordingBuffer = recordedSound.getWritePointer (0);
const float* const playBuffer = testSound.getReadPointer (0);
for (int i = 0; i < numSamples; ++i)
{
if (recordedSampleNum < recordedSound.getNumSamples())
{
float inputSamp = 0;
for (int j = numInputChannels; --j >= 0;)
if (inputChannelData[j] != 0)
inputSamp += inputChannelData[j][i];
recordingBuffer [recordedSampleNum] = inputSamp;
}
++recordedSampleNum;
float outputSamp = (playingSampleNum < testSound.getNumSamples()) ? playBuffer [playingSampleNum] : 0;
for (int j = numOutputChannels; --j >= 0;)
if (outputChannelData[j] != 0)
outputChannelData[j][i] = outputSamp;
++playingSampleNum;
}
}
else
{
// We need to clear the output buffers, in case they're full of junk..
for (int i = 0; i < numOutputChannels; ++i)
if (outputChannelData[i] != 0)
zeromem (outputChannelData[i], sizeof (float) * (size_t) numSamples);
}
}
private:
AudioSampleBuffer testSound, recordedSound;
Array<int> spikePositions;
int playingSampleNum, recordedSampleNum;
CriticalSection lock;
double sampleRate;
bool testIsRunning;
TextEditor& resultsBox;
int deviceInputLatency, deviceOutputLatency;
// create a test sound which consists of a series of randomly-spaced audio spikes..
void createTestSound()
{
const int length = ((int) sampleRate) / 4;
testSound.setSize (1, length);
testSound.clear();
Random rand;
for (int i = 0; i < length; ++i)
testSound.setSample (0, i, (rand.nextFloat() - rand.nextFloat() + rand.nextFloat() - rand.nextFloat()) * 0.06f);
spikePositions.clear();
int spikePos = 0;
int spikeDelta = 50;
while (spikePos < length - 1)
{
spikePositions.add (spikePos);
testSound.setSample (0, spikePos, 0.99f);
testSound.setSample (0, spikePos + 1, -0.99f);
spikePos += spikeDelta;
spikeDelta += spikeDelta / 6 + rand.nextInt (5);
}
}
// Searches a buffer for a set of spikes that matches those in the test sound
int findOffsetOfSpikes (const AudioSampleBuffer& buffer) const
{
const float minSpikeLevel = 5.0f;
const double smooth = 0.975;
const float* s = buffer.getReadPointer (0);
const int spikeDriftAllowed = 5;
Array<int> spikesFound;
spikesFound.ensureStorageAllocated (100);
double runningAverage = 0;
int lastSpike = 0;
for (int i = 0; i < buffer.getNumSamples() - 10; ++i)
{
const float samp = std::abs (s[i]);
if (samp > runningAverage * minSpikeLevel && i > lastSpike + 20)
{
lastSpike = i;
spikesFound.add (i);
}
runningAverage = runningAverage * smooth + (1.0 - smooth) * samp;
}
int bestMatch = -1;
int bestNumMatches = spikePositions.size() / 3; // the minimum number of matches required
if (spikesFound.size() < bestNumMatches)
return -1;
for (int offsetToTest = 0; offsetToTest < buffer.getNumSamples() - 2048; ++offsetToTest)
{
int numMatchesHere = 0;
int foundIndex = 0;
for (int refIndex = 0; refIndex < spikePositions.size(); ++refIndex)
{
const int referenceSpike = spikePositions.getUnchecked (refIndex) + offsetToTest;
int spike = 0;
while ((spike = spikesFound.getUnchecked (foundIndex)) < referenceSpike - spikeDriftAllowed
&& foundIndex < spikesFound.size() - 1)
++foundIndex;
if (spike >= referenceSpike - spikeDriftAllowed && spike <= referenceSpike + spikeDriftAllowed)
++numMatchesHere;
}
if (numMatchesHere > bestNumMatches)
{
bestNumMatches = numMatchesHere;
bestMatch = offsetToTest;
if (numMatchesHere == spikePositions.size())
break;
}
}
return bestMatch;
}
int calculateLatencySamples() const
{
// Detect the sound in both our test sound and the recording of it, and measure the difference
// in their start times..
const int referenceStart = findOffsetOfSpikes (testSound);
jassert (referenceStart >= 0);
const int recordedStart = findOffsetOfSpikes (recordedSound);
return (recordedStart < 0) ? -1
: (recordedStart - referenceStart);
}
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LatencyTester);
};
//==============================================================================
class AudioLatencyDemo : public Component,
private Button::Listener
{
public:
AudioLatencyDemo()
{
setOpaque (true);
addAndMakeVisible (liveAudioScroller = new LiveScrollingAudioDisplay());
addAndMakeVisible (resultsBox);
resultsBox.setMultiLine (true);
resultsBox.setReturnKeyStartsNewLine (true);
resultsBox.setReadOnly (true);
resultsBox.setScrollbarsShown (true);
resultsBox.setCaretVisible (false);
resultsBox.setPopupMenuEnabled (true);
resultsBox.setColour (TextEditor::backgroundColourId, Colour (0x32ffffff));
resultsBox.setColour (TextEditor::outlineColourId, Colour (0x1c000000));
resultsBox.setColour (TextEditor::shadowColourId, Colour (0x16000000));
resultsBox.setText ("Running this test measures the round-trip latency between the audio output and input "
"devices you\'ve got selected.\n\n"
"It\'ll play a sound, then try to measure the time at which the sound arrives "
"back at the audio input. Obviously for this to work you need to have your "
"microphone somewhere near your speakers...");
addAndMakeVisible (startTestButton);
startTestButton.addListener (this);
startTestButton.setButtonText ("Test Latency");
MainAppWindow::getSharedAudioDeviceManager().addAudioCallback (liveAudioScroller);
}
~AudioLatencyDemo()
{
MainAppWindow::getSharedAudioDeviceManager().removeAudioCallback (liveAudioScroller);
startTestButton.removeListener (this);
latencyTester = nullptr;
liveAudioScroller = nullptr;
}
void startTest()
{
if (latencyTester == nullptr)
latencyTester = new LatencyTester (resultsBox);
latencyTester->beginTest();
}
void paint (Graphics& g) override
{
fillTiledBackground (g);
}
void resized() override
{
liveAudioScroller->setBounds (8, 8, getWidth() - 16, 64);
startTestButton.setBounds (8, getHeight() - 41, 168, 32);
resultsBox.setBounds (8, 88, getWidth() - 16, getHeight() - 137);
}
private:
ScopedPointer<LatencyTester> latencyTester;
ScopedPointer<LiveScrollingAudioDisplay> liveAudioScroller;
TextButton startTestButton;
TextEditor resultsBox;
void buttonClicked (Button* buttonThatWasClicked) override
{
if (buttonThatWasClicked == &startTestButton)
startTest();
}
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioLatencyDemo)
};
// This static object will register this demo type in a global list of demos..
static JuceDemoType<AudioLatencyDemo> demo ("31 Audio: Latency Detector");