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.

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