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.

377 lines
13KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2017 - ROLI Ltd.
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 5 End-User License
  8. Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
  9. 27th April 2017).
  10. End User License Agreement: www.juce.com/juce-5-licence
  11. Privacy Policy: www.juce.com/juce-5-privacy-policy
  12. Or: You may also use this code under the terms of the GPL v3 (see
  13. www.gnu.org/licenses).
  14. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  15. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  16. DISCLAIMED.
  17. ==============================================================================
  18. */
  19. #include "../JuceDemoHeader.h"
  20. #include "AudioLiveScrollingDisplay.h"
  21. class LatencyTester : public AudioIODeviceCallback,
  22. private Timer
  23. {
  24. public:
  25. LatencyTester (TextEditor& resultsBox_)
  26. : playingSampleNum (0),
  27. recordedSampleNum (-1),
  28. sampleRate (0),
  29. testIsRunning (false),
  30. resultsBox (resultsBox_)
  31. {
  32. MainAppWindow::getSharedAudioDeviceManager().addAudioCallback (this);
  33. }
  34. ~LatencyTester()
  35. {
  36. MainAppWindow::getSharedAudioDeviceManager().removeAudioCallback (this);
  37. }
  38. //==============================================================================
  39. void beginTest()
  40. {
  41. resultsBox.moveCaretToEnd();
  42. resultsBox.insertTextAtCaret (newLine + newLine + "Starting test..." + newLine);
  43. resultsBox.moveCaretToEnd();
  44. startTimer (50);
  45. const ScopedLock sl (lock);
  46. createTestSound();
  47. recordedSound.clear();
  48. playingSampleNum = recordedSampleNum = 0;
  49. testIsRunning = true;
  50. }
  51. void timerCallback()
  52. {
  53. if (testIsRunning && recordedSampleNum >= recordedSound.getNumSamples())
  54. {
  55. testIsRunning = false;
  56. stopTimer();
  57. // Test has finished, so calculate the result..
  58. const int latencySamples = calculateLatencySamples();
  59. resultsBox.moveCaretToEnd();
  60. resultsBox.insertTextAtCaret (getMessageDescribingResult (latencySamples));
  61. resultsBox.moveCaretToEnd();
  62. }
  63. }
  64. String getMessageDescribingResult (int latencySamples)
  65. {
  66. String message;
  67. if (latencySamples >= 0)
  68. {
  69. message << newLine
  70. << "Results:" << newLine
  71. << latencySamples << " samples (" << String (latencySamples * 1000.0 / sampleRate, 1)
  72. << " milliseconds)" << newLine
  73. << "The audio device reports an input latency of "
  74. << deviceInputLatency << " samples, output latency of "
  75. << deviceOutputLatency << " samples." << newLine
  76. << "So the corrected latency = "
  77. << (latencySamples - deviceInputLatency - deviceOutputLatency)
  78. << " samples (" << String ((latencySamples - deviceInputLatency - deviceOutputLatency) * 1000.0 / sampleRate, 2)
  79. << " milliseconds)";
  80. }
  81. else
  82. {
  83. message << newLine
  84. << "Couldn't detect the test signal!!" << newLine
  85. << "Make sure there's no background noise that might be confusing it..";
  86. }
  87. return message;
  88. }
  89. //==============================================================================
  90. void audioDeviceAboutToStart (AudioIODevice* device)
  91. {
  92. testIsRunning = false;
  93. playingSampleNum = recordedSampleNum = 0;
  94. sampleRate = device->getCurrentSampleRate();
  95. deviceInputLatency = device->getInputLatencyInSamples();
  96. deviceOutputLatency = device->getOutputLatencyInSamples();
  97. recordedSound.setSize (1, (int) (0.9 * sampleRate));
  98. recordedSound.clear();
  99. }
  100. void audioDeviceStopped()
  101. {
  102. // (nothing to do here)
  103. }
  104. void audioDeviceIOCallback (const float** inputChannelData,
  105. int numInputChannels,
  106. float** outputChannelData,
  107. int numOutputChannels,
  108. int numSamples)
  109. {
  110. const ScopedLock sl (lock);
  111. if (testIsRunning)
  112. {
  113. float* const recordingBuffer = recordedSound.getWritePointer (0);
  114. const float* const playBuffer = testSound.getReadPointer (0);
  115. for (int i = 0; i < numSamples; ++i)
  116. {
  117. if (recordedSampleNum < recordedSound.getNumSamples())
  118. {
  119. float inputSamp = 0;
  120. for (int j = numInputChannels; --j >= 0;)
  121. if (inputChannelData[j] != 0)
  122. inputSamp += inputChannelData[j][i];
  123. recordingBuffer [recordedSampleNum] = inputSamp;
  124. }
  125. ++recordedSampleNum;
  126. float outputSamp = (playingSampleNum < testSound.getNumSamples()) ? playBuffer [playingSampleNum] : 0;
  127. for (int j = numOutputChannels; --j >= 0;)
  128. if (outputChannelData[j] != 0)
  129. outputChannelData[j][i] = outputSamp;
  130. ++playingSampleNum;
  131. }
  132. }
  133. else
  134. {
  135. // We need to clear the output buffers, in case they're full of junk..
  136. for (int i = 0; i < numOutputChannels; ++i)
  137. if (outputChannelData[i] != 0)
  138. zeromem (outputChannelData[i], sizeof (float) * (size_t) numSamples);
  139. }
  140. }
  141. private:
  142. AudioSampleBuffer testSound, recordedSound;
  143. Array<int> spikePositions;
  144. int playingSampleNum, recordedSampleNum;
  145. CriticalSection lock;
  146. double sampleRate;
  147. bool testIsRunning;
  148. TextEditor& resultsBox;
  149. int deviceInputLatency, deviceOutputLatency;
  150. // create a test sound which consists of a series of randomly-spaced audio spikes..
  151. void createTestSound()
  152. {
  153. const int length = ((int) sampleRate) / 4;
  154. testSound.setSize (1, length);
  155. testSound.clear();
  156. Random rand;
  157. for (int i = 0; i < length; ++i)
  158. testSound.setSample (0, i, (rand.nextFloat() - rand.nextFloat() + rand.nextFloat() - rand.nextFloat()) * 0.06f);
  159. spikePositions.clear();
  160. int spikePos = 0;
  161. int spikeDelta = 50;
  162. while (spikePos < length - 1)
  163. {
  164. spikePositions.add (spikePos);
  165. testSound.setSample (0, spikePos, 0.99f);
  166. testSound.setSample (0, spikePos + 1, -0.99f);
  167. spikePos += spikeDelta;
  168. spikeDelta += spikeDelta / 6 + rand.nextInt (5);
  169. }
  170. }
  171. // Searches a buffer for a set of spikes that matches those in the test sound
  172. int findOffsetOfSpikes (const AudioSampleBuffer& buffer) const
  173. {
  174. const float minSpikeLevel = 5.0f;
  175. const double smooth = 0.975;
  176. const float* s = buffer.getReadPointer (0);
  177. const int spikeDriftAllowed = 5;
  178. Array<int> spikesFound;
  179. spikesFound.ensureStorageAllocated (100);
  180. double runningAverage = 0;
  181. int lastSpike = 0;
  182. for (int i = 0; i < buffer.getNumSamples() - 10; ++i)
  183. {
  184. const float samp = std::abs (s[i]);
  185. if (samp > runningAverage * minSpikeLevel && i > lastSpike + 20)
  186. {
  187. lastSpike = i;
  188. spikesFound.add (i);
  189. }
  190. runningAverage = runningAverage * smooth + (1.0 - smooth) * samp;
  191. }
  192. int bestMatch = -1;
  193. int bestNumMatches = spikePositions.size() / 3; // the minimum number of matches required
  194. if (spikesFound.size() < bestNumMatches)
  195. return -1;
  196. for (int offsetToTest = 0; offsetToTest < buffer.getNumSamples() - 2048; ++offsetToTest)
  197. {
  198. int numMatchesHere = 0;
  199. int foundIndex = 0;
  200. for (int refIndex = 0; refIndex < spikePositions.size(); ++refIndex)
  201. {
  202. const int referenceSpike = spikePositions.getUnchecked (refIndex) + offsetToTest;
  203. int spike = 0;
  204. while ((spike = spikesFound.getUnchecked (foundIndex)) < referenceSpike - spikeDriftAllowed
  205. && foundIndex < spikesFound.size() - 1)
  206. ++foundIndex;
  207. if (spike >= referenceSpike - spikeDriftAllowed && spike <= referenceSpike + spikeDriftAllowed)
  208. ++numMatchesHere;
  209. }
  210. if (numMatchesHere > bestNumMatches)
  211. {
  212. bestNumMatches = numMatchesHere;
  213. bestMatch = offsetToTest;
  214. if (numMatchesHere == spikePositions.size())
  215. break;
  216. }
  217. }
  218. return bestMatch;
  219. }
  220. int calculateLatencySamples() const
  221. {
  222. // Detect the sound in both our test sound and the recording of it, and measure the difference
  223. // in their start times..
  224. const int referenceStart = findOffsetOfSpikes (testSound);
  225. jassert (referenceStart >= 0);
  226. const int recordedStart = findOffsetOfSpikes (recordedSound);
  227. return (recordedStart < 0) ? -1
  228. : (recordedStart - referenceStart);
  229. }
  230. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LatencyTester)
  231. };
  232. //==============================================================================
  233. class AudioLatencyDemo : public Component,
  234. private Button::Listener
  235. {
  236. public:
  237. AudioLatencyDemo()
  238. {
  239. setOpaque (true);
  240. addAndMakeVisible (liveAudioScroller = new LiveScrollingAudioDisplay());
  241. addAndMakeVisible (resultsBox);
  242. resultsBox.setMultiLine (true);
  243. resultsBox.setReturnKeyStartsNewLine (true);
  244. resultsBox.setReadOnly (true);
  245. resultsBox.setScrollbarsShown (true);
  246. resultsBox.setCaretVisible (false);
  247. resultsBox.setPopupMenuEnabled (true);
  248. resultsBox.setColour (TextEditor::backgroundColourId,
  249. getUIColourIfAvailable (LookAndFeel_V4::ColourScheme::UIColour::widgetBackground,
  250. 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. g.fillAll (getUIColourIfAvailable (LookAndFeel_V4::ColourScheme::UIColour::windowBackground));
  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");