The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

371 lines
13KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2015 - ROLI Ltd.
  5. Permission is granted to use this software under the terms of either:
  6. a) the GPL v2 (or any later version)
  7. b) the Affero GPL v3
  8. Details of these licenses can be found at: www.gnu.org/licenses
  9. JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
  10. WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  11. A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  12. ------------------------------------------------------------------------------
  13. To release a closed-source product which uses JUCE, commercial licenses are
  14. available: visit www.juce.com for more information.
  15. ==============================================================================
  16. */
  17. #include "../JuceDemoHeader.h"
  18. #include "AudioLiveScrollingDisplay.h"
  19. class LatencyTester : public AudioIODeviceCallback,
  20. private Timer
  21. {
  22. public:
  23. LatencyTester (TextEditor& resultsBox_)
  24. : playingSampleNum (0),
  25. recordedSampleNum (-1),
  26. sampleRate (0),
  27. testIsRunning (false),
  28. resultsBox (resultsBox_)
  29. {
  30. MainAppWindow::getSharedAudioDeviceManager().addAudioCallback (this);
  31. }
  32. ~LatencyTester()
  33. {
  34. MainAppWindow::getSharedAudioDeviceManager().removeAudioCallback (this);
  35. }
  36. //==============================================================================
  37. void beginTest()
  38. {
  39. resultsBox.moveCaretToEnd();
  40. resultsBox.insertTextAtCaret (newLine + newLine + "Starting test..." + newLine);
  41. resultsBox.moveCaretToEnd();
  42. startTimer (50);
  43. const ScopedLock sl (lock);
  44. createTestSound();
  45. recordedSound.clear();
  46. playingSampleNum = recordedSampleNum = 0;
  47. testIsRunning = true;
  48. }
  49. void timerCallback()
  50. {
  51. if (testIsRunning && recordedSampleNum >= recordedSound.getNumSamples())
  52. {
  53. testIsRunning = false;
  54. stopTimer();
  55. // Test has finished, so calculate the result..
  56. const int latencySamples = calculateLatencySamples();
  57. resultsBox.moveCaretToEnd();
  58. resultsBox.insertTextAtCaret (getMessageDescribingResult (latencySamples));
  59. resultsBox.moveCaretToEnd();
  60. }
  61. }
  62. String getMessageDescribingResult (int latencySamples)
  63. {
  64. String message;
  65. if (latencySamples >= 0)
  66. {
  67. message << newLine
  68. << "Results:" << newLine
  69. << latencySamples << " samples (" << String (latencySamples * 1000.0 / sampleRate, 1)
  70. << " milliseconds)" << newLine
  71. << "The audio device reports an input latency of "
  72. << deviceInputLatency << " samples, output latency of "
  73. << deviceOutputLatency << " samples." << newLine
  74. << "So the corrected latency = "
  75. << (latencySamples - deviceInputLatency - deviceOutputLatency)
  76. << " samples (" << String ((latencySamples - deviceInputLatency - deviceOutputLatency) * 1000.0 / sampleRate, 2)
  77. << " milliseconds)";
  78. }
  79. else
  80. {
  81. message << newLine
  82. << "Couldn't detect the test signal!!" << newLine
  83. << "Make sure there's no background noise that might be confusing it..";
  84. }
  85. return message;
  86. }
  87. //==============================================================================
  88. void audioDeviceAboutToStart (AudioIODevice* device)
  89. {
  90. testIsRunning = false;
  91. playingSampleNum = recordedSampleNum = 0;
  92. sampleRate = device->getCurrentSampleRate();
  93. deviceInputLatency = device->getInputLatencyInSamples();
  94. deviceOutputLatency = device->getOutputLatencyInSamples();
  95. recordedSound.setSize (1, (int) (0.9 * sampleRate));
  96. recordedSound.clear();
  97. }
  98. void audioDeviceStopped()
  99. {
  100. // (nothing to do here)
  101. }
  102. void audioDeviceIOCallback (const float** inputChannelData,
  103. int numInputChannels,
  104. float** outputChannelData,
  105. int numOutputChannels,
  106. int numSamples)
  107. {
  108. const ScopedLock sl (lock);
  109. if (testIsRunning)
  110. {
  111. float* const recordingBuffer = recordedSound.getWritePointer (0);
  112. const float* const playBuffer = testSound.getReadPointer (0);
  113. for (int i = 0; i < numSamples; ++i)
  114. {
  115. if (recordedSampleNum < recordedSound.getNumSamples())
  116. {
  117. float inputSamp = 0;
  118. for (int j = numInputChannels; --j >= 0;)
  119. if (inputChannelData[j] != 0)
  120. inputSamp += inputChannelData[j][i];
  121. recordingBuffer [recordedSampleNum] = inputSamp;
  122. }
  123. ++recordedSampleNum;
  124. float outputSamp = (playingSampleNum < testSound.getNumSamples()) ? playBuffer [playingSampleNum] : 0;
  125. for (int j = numOutputChannels; --j >= 0;)
  126. if (outputChannelData[j] != 0)
  127. outputChannelData[j][i] = outputSamp;
  128. ++playingSampleNum;
  129. }
  130. }
  131. else
  132. {
  133. // We need to clear the output buffers, in case they're full of junk..
  134. for (int i = 0; i < numOutputChannels; ++i)
  135. if (outputChannelData[i] != 0)
  136. zeromem (outputChannelData[i], sizeof (float) * (size_t) numSamples);
  137. }
  138. }
  139. private:
  140. AudioSampleBuffer testSound, recordedSound;
  141. Array<int> spikePositions;
  142. int playingSampleNum, recordedSampleNum;
  143. CriticalSection lock;
  144. double sampleRate;
  145. bool testIsRunning;
  146. TextEditor& resultsBox;
  147. int deviceInputLatency, deviceOutputLatency;
  148. // create a test sound which consists of a series of randomly-spaced audio spikes..
  149. void createTestSound()
  150. {
  151. const int length = ((int) sampleRate) / 4;
  152. testSound.setSize (1, length);
  153. testSound.clear();
  154. Random rand;
  155. for (int i = 0; i < length; ++i)
  156. testSound.setSample (0, i, (rand.nextFloat() - rand.nextFloat() + rand.nextFloat() - rand.nextFloat()) * 0.06f);
  157. spikePositions.clear();
  158. int spikePos = 0;
  159. int spikeDelta = 50;
  160. while (spikePos < length - 1)
  161. {
  162. spikePositions.add (spikePos);
  163. testSound.setSample (0, spikePos, 0.99f);
  164. testSound.setSample (0, spikePos + 1, -0.99f);
  165. spikePos += spikeDelta;
  166. spikeDelta += spikeDelta / 6 + rand.nextInt (5);
  167. }
  168. }
  169. // Searches a buffer for a set of spikes that matches those in the test sound
  170. int findOffsetOfSpikes (const AudioSampleBuffer& buffer) const
  171. {
  172. const float minSpikeLevel = 5.0f;
  173. const double smooth = 0.975;
  174. const float* s = buffer.getReadPointer (0);
  175. const int spikeDriftAllowed = 5;
  176. Array<int> spikesFound;
  177. spikesFound.ensureStorageAllocated (100);
  178. double runningAverage = 0;
  179. int lastSpike = 0;
  180. for (int i = 0; i < buffer.getNumSamples() - 10; ++i)
  181. {
  182. const float samp = std::abs (s[i]);
  183. if (samp > runningAverage * minSpikeLevel && i > lastSpike + 20)
  184. {
  185. lastSpike = i;
  186. spikesFound.add (i);
  187. }
  188. runningAverage = runningAverage * smooth + (1.0 - smooth) * samp;
  189. }
  190. int bestMatch = -1;
  191. int bestNumMatches = spikePositions.size() / 3; // the minimum number of matches required
  192. if (spikesFound.size() < bestNumMatches)
  193. return -1;
  194. for (int offsetToTest = 0; offsetToTest < buffer.getNumSamples() - 2048; ++offsetToTest)
  195. {
  196. int numMatchesHere = 0;
  197. int foundIndex = 0;
  198. for (int refIndex = 0; refIndex < spikePositions.size(); ++refIndex)
  199. {
  200. const int referenceSpike = spikePositions.getUnchecked (refIndex) + offsetToTest;
  201. int spike = 0;
  202. while ((spike = spikesFound.getUnchecked (foundIndex)) < referenceSpike - spikeDriftAllowed
  203. && foundIndex < spikesFound.size() - 1)
  204. ++foundIndex;
  205. if (spike >= referenceSpike - spikeDriftAllowed && spike <= referenceSpike + spikeDriftAllowed)
  206. ++numMatchesHere;
  207. }
  208. if (numMatchesHere > bestNumMatches)
  209. {
  210. bestNumMatches = numMatchesHere;
  211. bestMatch = offsetToTest;
  212. if (numMatchesHere == spikePositions.size())
  213. break;
  214. }
  215. }
  216. return bestMatch;
  217. }
  218. int calculateLatencySamples() const
  219. {
  220. // Detect the sound in both our test sound and the recording of it, and measure the difference
  221. // in their start times..
  222. const int referenceStart = findOffsetOfSpikes (testSound);
  223. jassert (referenceStart >= 0);
  224. const int recordedStart = findOffsetOfSpikes (recordedSound);
  225. return (recordedStart < 0) ? -1
  226. : (recordedStart - referenceStart);
  227. }
  228. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LatencyTester)
  229. };
  230. //==============================================================================
  231. class AudioLatencyDemo : public Component,
  232. private Button::Listener
  233. {
  234. public:
  235. AudioLatencyDemo()
  236. {
  237. setOpaque (true);
  238. addAndMakeVisible (liveAudioScroller = new LiveScrollingAudioDisplay());
  239. addAndMakeVisible (resultsBox);
  240. resultsBox.setMultiLine (true);
  241. resultsBox.setReturnKeyStartsNewLine (true);
  242. resultsBox.setReadOnly (true);
  243. resultsBox.setScrollbarsShown (true);
  244. resultsBox.setCaretVisible (false);
  245. resultsBox.setPopupMenuEnabled (true);
  246. resultsBox.setColour (TextEditor::backgroundColourId, Colour (0x32ffffff));
  247. resultsBox.setColour (TextEditor::outlineColourId, Colour (0x1c000000));
  248. resultsBox.setColour (TextEditor::shadowColourId, Colour (0x16000000));
  249. resultsBox.setText ("Running this test measures the round-trip latency between the audio output and input "
  250. "devices you\'ve got selected.\n\n"
  251. "It\'ll play a sound, then try to measure the time at which the sound arrives "
  252. "back at the audio input. Obviously for this to work you need to have your "
  253. "microphone somewhere near your speakers...");
  254. addAndMakeVisible (startTestButton);
  255. startTestButton.addListener (this);
  256. startTestButton.setButtonText ("Test Latency");
  257. MainAppWindow::getSharedAudioDeviceManager().addAudioCallback (liveAudioScroller);
  258. }
  259. ~AudioLatencyDemo()
  260. {
  261. MainAppWindow::getSharedAudioDeviceManager().removeAudioCallback (liveAudioScroller);
  262. startTestButton.removeListener (this);
  263. latencyTester = nullptr;
  264. liveAudioScroller = nullptr;
  265. }
  266. void startTest()
  267. {
  268. if (latencyTester == nullptr)
  269. latencyTester = new LatencyTester (resultsBox);
  270. latencyTester->beginTest();
  271. }
  272. void paint (Graphics& g) override
  273. {
  274. fillStandardDemoBackground (g);
  275. }
  276. void resized() override
  277. {
  278. liveAudioScroller->setBounds (8, 8, getWidth() - 16, 64);
  279. startTestButton.setBounds (8, getHeight() - 41, 168, 32);
  280. resultsBox.setBounds (8, 88, getWidth() - 16, getHeight() - 137);
  281. }
  282. private:
  283. ScopedPointer<LatencyTester> latencyTester;
  284. ScopedPointer<LiveScrollingAudioDisplay> liveAudioScroller;
  285. TextButton startTestButton;
  286. TextEditor resultsBox;
  287. void buttonClicked (Button* buttonThatWasClicked) override
  288. {
  289. if (buttonThatWasClicked == &startTestButton)
  290. startTest();
  291. }
  292. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioLatencyDemo)
  293. };
  294. // This static object will register this demo type in a global list of demos..
  295. static JuceDemoType<AudioLatencyDemo> demo ("31 Audio: Latency Detector");