404 lines
14 KiB
C
404 lines
14 KiB
C
|
/*
|
||
|
==============================================================================
|
||
|
|
||
|
This file is part of the JUCE examples.
|
||
|
Copyright (c) 2020 - Raw Material Software Limited
|
||
|
|
||
|
The code included in this file is provided under the terms of the ISC license
|
||
|
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
|
||
|
To use, copy, modify, and/or distribute this software for any purpose with or
|
||
|
without fee is hereby granted provided that the above copyright notice and
|
||
|
this permission notice appear in all copies.
|
||
|
|
||
|
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
|
||
|
WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
|
||
|
PURPOSE, ARE DISCLAIMED.
|
||
|
|
||
|
==============================================================================
|
||
|
*/
|
||
|
|
||
|
/*******************************************************************************
|
||
|
The block below describes the properties of this PIP. A PIP is a short snippet
|
||
|
of code that can be read by the Projucer and used to generate a JUCE project.
|
||
|
|
||
|
BEGIN_JUCE_PIP_METADATA
|
||
|
|
||
|
name: AudioLatencyDemo
|
||
|
version: 1.0.0
|
||
|
vendor: JUCE
|
||
|
website: http://juce.com
|
||
|
description: Tests the audio latency of a device.
|
||
|
|
||
|
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
|
||
|
juce_audio_processors, juce_audio_utils, juce_core,
|
||
|
juce_data_structures, juce_events, juce_graphics,
|
||
|
juce_gui_basics, juce_gui_extra
|
||
|
exporters: xcode_mac, vs2019, linux_make, androidstudio, xcode_iphone
|
||
|
|
||
|
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
||
|
|
||
|
type: Component
|
||
|
mainClass: AudioLatencyDemo
|
||
|
|
||
|
useLocalCopy: 1
|
||
|
|
||
|
END_JUCE_PIP_METADATA
|
||
|
|
||
|
*******************************************************************************/
|
||
|
|
||
|
#pragma once
|
||
|
|
||
|
#include "../Assets/DemoUtilities.h"
|
||
|
#include "../Assets/AudioLiveScrollingDisplay.h"
|
||
|
|
||
|
//==============================================================================
|
||
|
class LatencyTester : public AudioIODeviceCallback,
|
||
|
private Timer
|
||
|
{
|
||
|
public:
|
||
|
LatencyTester (TextEditor& editorBox)
|
||
|
: resultsBox (editorBox)
|
||
|
{}
|
||
|
|
||
|
//==============================================================================
|
||
|
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() override
|
||
|
{
|
||
|
if (testIsRunning && recordedSampleNum >= recordedSound.getNumSamples())
|
||
|
{
|
||
|
testIsRunning = false;
|
||
|
stopTimer();
|
||
|
|
||
|
// Test has finished, so calculate the result..
|
||
|
auto 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) override
|
||
|
{
|
||
|
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() override {}
|
||
|
|
||
|
void audioDeviceIOCallback (const float** inputChannelData, int numInputChannels,
|
||
|
float** outputChannelData, int numOutputChannels, int numSamples) override
|
||
|
{
|
||
|
const ScopedLock sl (lock);
|
||
|
|
||
|
if (testIsRunning)
|
||
|
{
|
||
|
auto* recordingBuffer = recordedSound.getWritePointer (0);
|
||
|
auto* playBuffer = testSound.getReadPointer (0);
|
||
|
|
||
|
for (int i = 0; i < numSamples; ++i)
|
||
|
{
|
||
|
if (recordedSampleNum < recordedSound.getNumSamples())
|
||
|
{
|
||
|
auto inputSamp = 0.0f;
|
||
|
|
||
|
for (auto j = numInputChannels; --j >= 0;)
|
||
|
if (inputChannelData[j] != nullptr)
|
||
|
inputSamp += inputChannelData[j][i];
|
||
|
|
||
|
recordingBuffer[recordedSampleNum] = inputSamp;
|
||
|
}
|
||
|
|
||
|
++recordedSampleNum;
|
||
|
|
||
|
auto outputSamp = (playingSampleNum < testSound.getNumSamples()) ? playBuffer[playingSampleNum] : 0.0f;
|
||
|
|
||
|
for (auto j = numOutputChannels; --j >= 0;)
|
||
|
if (outputChannelData[j] != nullptr)
|
||
|
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] != nullptr)
|
||
|
zeromem (outputChannelData[i], (size_t) numSamples * sizeof (float));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private:
|
||
|
TextEditor& resultsBox;
|
||
|
AudioBuffer<float> testSound, recordedSound;
|
||
|
Array<int> spikePositions;
|
||
|
CriticalSection lock;
|
||
|
|
||
|
int playingSampleNum = 0;
|
||
|
int recordedSampleNum = -1;
|
||
|
double sampleRate = 0.0;
|
||
|
bool testIsRunning = false;
|
||
|
int deviceInputLatency, deviceOutputLatency;
|
||
|
|
||
|
//==============================================================================
|
||
|
// create a test sound which consists of a series of randomly-spaced audio spikes..
|
||
|
void createTestSound()
|
||
|
{
|
||
|
auto 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 AudioBuffer<float>& buffer) const
|
||
|
{
|
||
|
auto minSpikeLevel = 5.0f;
|
||
|
auto smooth = 0.975;
|
||
|
auto* s = buffer.getReadPointer (0);
|
||
|
int spikeDriftAllowed = 5;
|
||
|
|
||
|
Array<int> spikesFound;
|
||
|
spikesFound.ensureStorageAllocated (100);
|
||
|
auto runningAverage = 0.0;
|
||
|
int lastSpike = 0;
|
||
|
|
||
|
for (int i = 0; i < buffer.getNumSamples() - 10; ++i)
|
||
|
{
|
||
|
auto 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;
|
||
|
auto 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)
|
||
|
{
|
||
|
auto 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..
|
||
|
auto referenceStart = findOffsetOfSpikes (testSound);
|
||
|
jassert (referenceStart >= 0);
|
||
|
|
||
|
auto recordedStart = findOffsetOfSpikes (recordedSound);
|
||
|
|
||
|
return (recordedStart < 0) ? -1
|
||
|
: (recordedStart - referenceStart);
|
||
|
}
|
||
|
|
||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LatencyTester)
|
||
|
};
|
||
|
|
||
|
//==============================================================================
|
||
|
class AudioLatencyDemo : public Component
|
||
|
{
|
||
|
public:
|
||
|
AudioLatencyDemo()
|
||
|
{
|
||
|
setOpaque (true);
|
||
|
|
||
|
liveAudioScroller.reset (new LiveScrollingAudioDisplay());
|
||
|
addAndMakeVisible (liveAudioScroller.get());
|
||
|
|
||
|
addAndMakeVisible (resultsBox);
|
||
|
resultsBox.setMultiLine (true);
|
||
|
resultsBox.setReturnKeyStartsNewLine (true);
|
||
|
resultsBox.setReadOnly (true);
|
||
|
resultsBox.setScrollbarsShown (true);
|
||
|
resultsBox.setCaretVisible (false);
|
||
|
resultsBox.setPopupMenuEnabled (true);
|
||
|
|
||
|
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.onClick = [this] { startTest(); };
|
||
|
|
||
|
#ifndef JUCE_DEMO_RUNNER
|
||
|
RuntimePermissions::request (RuntimePermissions::recordAudio,
|
||
|
[this] (bool granted)
|
||
|
{
|
||
|
int numInputChannels = granted ? 2 : 0;
|
||
|
audioDeviceManager.initialise (numInputChannels, 2, nullptr, true, {}, nullptr);
|
||
|
});
|
||
|
#endif
|
||
|
|
||
|
audioDeviceManager.addAudioCallback (liveAudioScroller.get());
|
||
|
|
||
|
setSize (500, 500);
|
||
|
}
|
||
|
|
||
|
~AudioLatencyDemo() override
|
||
|
{
|
||
|
audioDeviceManager.removeAudioCallback (liveAudioScroller.get());
|
||
|
audioDeviceManager.removeAudioCallback (latencyTester .get());
|
||
|
latencyTester .reset();
|
||
|
liveAudioScroller.reset();
|
||
|
}
|
||
|
|
||
|
void startTest()
|
||
|
{
|
||
|
if (latencyTester.get() == nullptr)
|
||
|
{
|
||
|
latencyTester.reset (new LatencyTester (resultsBox));
|
||
|
audioDeviceManager.addAudioCallback (latencyTester.get());
|
||
|
}
|
||
|
|
||
|
latencyTester->beginTest();
|
||
|
}
|
||
|
|
||
|
void paint (Graphics& g) override
|
||
|
{
|
||
|
g.fillAll (findColour (ResizableWindow::backgroundColourId));
|
||
|
}
|
||
|
|
||
|
void resized() override
|
||
|
{
|
||
|
auto b = getLocalBounds().reduced (5);
|
||
|
|
||
|
if (liveAudioScroller.get() != nullptr)
|
||
|
{
|
||
|
liveAudioScroller->setBounds (b.removeFromTop (b.getHeight() / 5));
|
||
|
b.removeFromTop (10);
|
||
|
}
|
||
|
|
||
|
startTestButton.setBounds (b.removeFromBottom (b.getHeight() / 10));
|
||
|
b.removeFromBottom (10);
|
||
|
|
||
|
resultsBox.setBounds (b);
|
||
|
}
|
||
|
|
||
|
private:
|
||
|
// if this PIP is running inside the demo runner, we'll use the shared device manager instead
|
||
|
#ifndef JUCE_DEMO_RUNNER
|
||
|
AudioDeviceManager audioDeviceManager;
|
||
|
#else
|
||
|
AudioDeviceManager& audioDeviceManager { getSharedAudioDeviceManager (1, 2) };
|
||
|
#endif
|
||
|
|
||
|
std::unique_ptr<LatencyTester> latencyTester;
|
||
|
std::unique_ptr<LiveScrollingAudioDisplay> liveAudioScroller;
|
||
|
|
||
|
TextButton startTestButton { "Test Latency" };
|
||
|
TextEditor resultsBox;
|
||
|
|
||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioLatencyDemo)
|
||
|
};
|