- clean working copy for leaving SVN
This commit is contained in:
@@ -0,0 +1,371 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library - "Jules' Utility Class Extensions"
|
||||
Copyright 2004-12 by Raw Material Software Ltd.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
JUCE can be redistributed and/or modified under the terms of the GNU General
|
||||
Public License (Version 2), as published by the Free Software Foundation.
|
||||
A copy of the license is included in the JUCE distribution, or can be found
|
||||
online 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.rawmaterialsoftware.com/juce 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");
|
||||
Reference in New Issue
Block a user