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.

375 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.getSampleData (0, 0);
  115. const float* const playBuffer = testSound.getSampleData (0, 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. float* s = testSound.getSampleData (0, 0);
  158. Random rand;
  159. for (int i = 0; i < length; ++i)
  160. s[i] = (rand.nextFloat() - rand.nextFloat() + rand.nextFloat() - rand.nextFloat()) * 0.06f;
  161. spikePositions.clear();
  162. int spikePos = 0;
  163. int spikeDelta = 50;
  164. while (spikePos < length - 1)
  165. {
  166. spikePositions.add (spikePos);
  167. s[spikePos] = 0.99f;
  168. s[spikePos + 1] = -0.99f;
  169. spikePos += spikeDelta;
  170. spikeDelta += spikeDelta / 6 + rand.nextInt (5);
  171. }
  172. }
  173. // Searches a buffer for a set of spikes that matches those in the test sound
  174. int findOffsetOfSpikes (const AudioSampleBuffer& buffer) const
  175. {
  176. const float minSpikeLevel = 5.0f;
  177. const double smooth = 0.975;
  178. const float* s = buffer.getSampleData (0, 0);
  179. const int spikeDriftAllowed = 5;
  180. Array<int> spikesFound;
  181. spikesFound.ensureStorageAllocated (100);
  182. double runningAverage = 0;
  183. int lastSpike = 0;
  184. for (int i = 0; i < buffer.getNumSamples() - 10; ++i)
  185. {
  186. const float samp = std::abs (s[i]);
  187. if (samp > runningAverage * minSpikeLevel && i > lastSpike + 20)
  188. {
  189. lastSpike = i;
  190. spikesFound.add (i);
  191. }
  192. runningAverage = runningAverage * smooth + (1.0 - smooth) * samp;
  193. }
  194. int bestMatch = -1;
  195. int bestNumMatches = spikePositions.size() / 3; // the minimum number of matches required
  196. if (spikesFound.size() < bestNumMatches)
  197. return -1;
  198. for (int offsetToTest = 0; offsetToTest < buffer.getNumSamples() - 2048; ++offsetToTest)
  199. {
  200. int numMatchesHere = 0;
  201. int foundIndex = 0;
  202. for (int refIndex = 0; refIndex < spikePositions.size(); ++refIndex)
  203. {
  204. const int referenceSpike = spikePositions.getUnchecked (refIndex) + offsetToTest;
  205. int spike = 0;
  206. while ((spike = spikesFound.getUnchecked (foundIndex)) < referenceSpike - spikeDriftAllowed
  207. && foundIndex < spikesFound.size() - 1)
  208. ++foundIndex;
  209. if (spike >= referenceSpike - spikeDriftAllowed && spike <= referenceSpike + spikeDriftAllowed)
  210. ++numMatchesHere;
  211. }
  212. if (numMatchesHere > bestNumMatches)
  213. {
  214. bestNumMatches = numMatchesHere;
  215. bestMatch = offsetToTest;
  216. if (numMatchesHere == spikePositions.size())
  217. break;
  218. }
  219. }
  220. return bestMatch;
  221. }
  222. int calculateLatencySamples() const
  223. {
  224. // Detect the sound in both our test sound and the recording of it, and measure the difference
  225. // in their start times..
  226. const int referenceStart = findOffsetOfSpikes (testSound);
  227. jassert (referenceStart >= 0);
  228. const int recordedStart = findOffsetOfSpikes (recordedSound);
  229. return (recordedStart < 0) ? -1
  230. : (recordedStart - referenceStart);
  231. }
  232. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LatencyTester);
  233. };
  234. //==============================================================================
  235. class AudioLatencyDemo : public Component,
  236. private Button::Listener
  237. {
  238. public:
  239. AudioLatencyDemo()
  240. {
  241. setOpaque (true);
  242. addAndMakeVisible (liveAudioScroller = new LiveScrollingAudioDisplay());
  243. addAndMakeVisible (resultsBox);
  244. resultsBox.setMultiLine (true);
  245. resultsBox.setReturnKeyStartsNewLine (true);
  246. resultsBox.setReadOnly (true);
  247. resultsBox.setScrollbarsShown (true);
  248. resultsBox.setCaretVisible (false);
  249. resultsBox.setPopupMenuEnabled (true);
  250. resultsBox.setColour (TextEditor::backgroundColourId, Colour (0x32ffffff));
  251. resultsBox.setColour (TextEditor::outlineColourId, Colour (0x1c000000));
  252. resultsBox.setColour (TextEditor::shadowColourId, Colour (0x16000000));
  253. resultsBox.setText ("Running this test measures the round-trip latency between the audio output and input "
  254. "devices you\'ve got selected.\n\n"
  255. "It\'ll play a sound, then try to measure the time at which the sound arrives "
  256. "back at the audio input. Obviously for this to work you need to have your "
  257. "microphone somewhere near your speakers...");
  258. addAndMakeVisible (startTestButton);
  259. startTestButton.addListener (this);
  260. startTestButton.setButtonText ("Test Latency");
  261. MainAppWindow::getSharedAudioDeviceManager().addAudioCallback (liveAudioScroller);
  262. }
  263. ~AudioLatencyDemo()
  264. {
  265. MainAppWindow::getSharedAudioDeviceManager().removeAudioCallback (liveAudioScroller);
  266. startTestButton.removeListener (this);
  267. latencyTester = nullptr;
  268. liveAudioScroller = nullptr;
  269. }
  270. void startTest()
  271. {
  272. if (latencyTester == nullptr)
  273. latencyTester = new LatencyTester (resultsBox);
  274. latencyTester->beginTest();
  275. }
  276. void paint (Graphics& g) override
  277. {
  278. fillBrushedAluminiumBackground (g);
  279. }
  280. void resized() override
  281. {
  282. liveAudioScroller->setBounds (8, 8, getWidth() - 16, 64);
  283. startTestButton.setBounds (8, getHeight() - 41, 168, 32);
  284. resultsBox.setBounds (8, 88, getWidth() - 16, getHeight() - 137);
  285. }
  286. private:
  287. ScopedPointer<LatencyTester> latencyTester;
  288. ScopedPointer<LiveScrollingAudioDisplay> liveAudioScroller;
  289. TextButton startTestButton;
  290. TextEditor resultsBox;
  291. void buttonClicked (Button* buttonThatWasClicked) override
  292. {
  293. if (buttonThatWasClicked == &startTestButton)
  294. startTest();
  295. }
  296. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioLatencyDemo)
  297. };
  298. // This static object will register this demo type in a global list of demos..
  299. static JuceDemoType<AudioLatencyDemo> demo ("31 Audio: Latency Detector");