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.

374 lines
13KB

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