hydrogen 1.2.6
AudioEngineTests.cpp
Go to the documentation of this file.
1/*
2 * Hydrogen
3 * Copyright(c) 2002-2008 by Alex >Comix< Cominu [comix@users.sourceforge.net]
4 * Copyright(c) 2008-2025 The hydrogen development team [hydrogen-devel@lists.sourceforge.net]
5 *
6 * http://www.hydrogen-music.org
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY, without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program. If not, see https://www.gnu.org/licenses
20 *
21 */
22#include <random>
23#include <stdexcept>
24
25#include <QTest>
26
30
33#include <core/Basics/Pattern.h>
35#include <core/Basics/Note.h>
36#include <core/Basics/Sample.h>
37#include <core/Basics/Song.h>
39#include <core/Hydrogen.h>
42#include <core/config.h>
43
44namespace H2Core
45{
46
47#ifdef H2CORE_HAVE_JACK
48JackAudioDriver::Timebase AudioEngineTests::m_referenceTimebase =
50#endif
51
53 auto pHydrogen = Hydrogen::get_instance();
54 auto pCoreActionController = pHydrogen->getCoreActionController();
55 auto pAE = pHydrogen->getAudioEngine();
56
57 pCoreActionController->activateTimeline( true );
58 pCoreActionController->addTempoMarker( 0, 120 );
59 pCoreActionController->addTempoMarker( 3, 100 );
60 pCoreActionController->addTempoMarker( 5, 40 );
61 pCoreActionController->addTempoMarker( 7, 200 );
62
63 auto checkFrame = []( long long nFrame, double fTolerance ) {
64 const double fTick = TransportPosition::computeTickFromFrame( nFrame );
65
66 double fTickMismatch;
67 const long long nFrameCheck =
68 TransportPosition::computeFrameFromTick( fTick, &fTickMismatch );
69
70 if ( nFrameCheck != nFrame || std::abs( fTickMismatch ) > fTolerance ) {
72 QString( "[testFrameToTickConversion::checkFrame] nFrame: %1, fTick: %2, nFrameComputed: %3, fTickMismatch: %4, frame diff: %5, fTolerance: %6" )
73 .arg( nFrame ).arg( fTick, 0, 'E', -1 ).arg( nFrameCheck )
74 .arg( fTickMismatch, 0, 'E', -1 ).arg( nFrameCheck - nFrame )
75 .arg( fTolerance, 0, 'E', -1 ) );
76 }
77 };
78 checkFrame( 342732, 1e-10 );
79 checkFrame( 1037223, 1e-10 );
80 checkFrame( 453610333722, 1e-6 );
81
82 auto checkTick = []( double fTick, double fTolerance ) {
83 double fTickMismatch;
84 const long long nFrame =
85 TransportPosition::computeFrameFromTick( fTick, &fTickMismatch );
86
87 const double fTickCheck =
88 TransportPosition::computeTickFromFrame( nFrame ) + fTickMismatch;
89
90 if ( abs( fTickCheck - fTick ) > fTolerance ) {
92 QString( "[testFrameToTickConversion::checkTick] nFrame: %1, fTick: %2, fTickComputed: %3, fTickMismatch: %4, tick diff: %5, fTolerance: %6" )
93 .arg( nFrame ).arg( fTick, 0, 'E', -1 ).arg( fTickCheck, 0, 'E', -1 )
94 .arg( fTickMismatch, 0, 'E', -1 ).arg( fTickCheck - fTick, 0, 'E', -1 )
95 .arg( fTolerance, 0, 'E', -1 ));
96 }
97 };
98 checkTick( 552, 1e-9 );
99 checkTick( 1939, 1e-9 );
100 checkTick( 534623409, 1e-6 );
101 checkTick( pAE->m_fSongSizeInTicks * 3, 1e-9 );
102}
103
105 auto pHydrogen = Hydrogen::get_instance();
106 auto pSong = pHydrogen->getSong();
107 auto pPref = Preferences::get_instance();
108 auto pCoreActionController = pHydrogen->getCoreActionController();
109 auto pAE = pHydrogen->getAudioEngine();
110 auto pTransportPos = pAE->getTransportPosition();
111 auto pQueuingPos = pAE->m_pQueuingPosition;
112
113 pCoreActionController->activateTimeline( false );
114 pCoreActionController->activateLoopMode( true );
115
116 pAE->lock( RIGHT_HERE );
117 pAE->setState( AudioEngine::State::Testing );
118
119 std::random_device randomSeed;
120 std::default_random_engine randomEngine( randomSeed() );
121 std::uniform_int_distribution<int> frameDist( 1, pPref->m_nBufferSize );
122 std::uniform_real_distribution<float> tempoDist( MIN_BPM, MAX_BPM );
123
124 // For this call the AudioEngine still needs to be in state
125 // Playing or Ready.
126 pAE->reset( false );
127
128 // Check consistency of updated frames, ticks, and queuing
129 // position while using a random buffer size (e.g. like PulseAudio
130 // does).
131 uint32_t nFrames;
132 double fCheckTick, fLastTickIntervalEnd;
133 long long nCheckFrame, nLastTransportFrame, nTotalFrames, nLastLookahead;
134 long nLastQueuingTick;
135 int nn;
136
137 auto resetVariables = [&]() {
138 nLastTransportFrame = 0;
139 nLastQueuingTick = 0;
140 fLastTickIntervalEnd = 0;
141 nTotalFrames = 0;
142 nLastLookahead = 0;
143 nn = 0;
144 };
145 resetVariables();
146
147 const int nMaxCycles =
148 std::max( std::ceil( static_cast<double>(pAE->m_fSongSizeInTicks) /
149 static_cast<double>(pPref->m_nBufferSize) *
150 static_cast<double>(pTransportPos->getTickSize()) * 4.0 ),
151 static_cast<double>(pAE->m_fSongSizeInTicks) );
152
153 while ( pTransportPos->getDoubleTick() <
154 pAE->getSongSizeInTicks() ) {
155 nFrames = frameDist( randomEngine );
157 "testTransportProcessing : song mode : constant tempo", nFrames,
158 &nLastLookahead, &nLastTransportFrame, &nTotalFrames,
159 &nLastQueuingTick, &fLastTickIntervalEnd, true );
160
161 nn++;
162 if ( nn > nMaxCycles ) {
164 QString( "[testTransportProcessing] [song mode : constant tempo] end of the song wasn't reached in time. pTransportPos->getFrame(): %1, pTransportPos->getDoubleTick(): %2, pTransportPos->getTickSize(): %3, pAE->getSongSizeInTicks(): %4, nMaxCycles: %5" )
165 .arg( pTransportPos->getFrame() )
166 .arg( pTransportPos->getDoubleTick(), 0, 'f' )
167 .arg( pTransportPos->getTickSize(), 0, 'f' )
168 .arg( pAE->getSongSizeInTicks(), 0, 'f' )
169 .arg( nMaxCycles ) );
170 }
171 }
172
173 pAE->reset( false );
174
175 pAE->setState( AudioEngine::State::Ready );
176 pAE->unlock();
177
178 // Check whether all frames are covered when running playback in song mode
179 // without looping.
180 pCoreActionController->activateLoopMode( false );
181
182 pAE->lock( RIGHT_HERE );
183 pAE->setState( AudioEngine::State::Testing );
184 resetVariables();
185 while ( nn <= nMaxCycles ) {
186 nFrames = frameDist( randomEngine );
187 pAE->incrementTransportPosition( nFrames );
188
189 if ( pAE->isEndOfSongReached( pAE->m_pTransportPosition ) ) {
190 // End of song reached
191 if ( pTransportPos->getTick() < pAE->getSongSizeInTicks() ) {
193 QString( "[testTransportProcessing] [song mode : no looping] final tick was not reached at song end. pTransportPos->getTick: [%1], pAE->getSongSizeInTicks: %2" )
194 .arg( pTransportPos->getTick() ).arg( pAE->getSongSizeInTicks() ) );
195 }
196 break;
197 }
198
199 nn++;
200 if ( nn > nMaxCycles ) {
202 QString( "[testTransportProcessing] [song mode : no looping] end of the song wasn't reached in time. pTransportPos->getFrame(): %1, pTransportPos->getDoubleTick(): %2, pTransportPos->getTickSize(): %3, pAE->getSongSizeInTicks(): %4, nMaxCycles: %5" )
203 .arg( pTransportPos->getFrame() )
204 .arg( pTransportPos->getDoubleTick(), 0, 'f' )
205 .arg( pTransportPos->getTickSize(), 0, 'f' )
206 .arg( pAE->getSongSizeInTicks(), 0, 'f' )
207 .arg( nMaxCycles ) );
208 }
209 }
210
211 pAE->reset( false );
212 pAE->setState( AudioEngine::State::Ready );
213 pAE->unlock();
214
215 // Check whether all frames are covered when running playback in song mode
216 // without looping.
217 pCoreActionController->activateLoopMode( true );
218
219 pAE->lock( RIGHT_HERE );
220 pAE->setState( AudioEngine::State::Testing );
221
222 resetVariables();
223
224 float fBpm;
225 float fLastBpm = pTransportPos->getBpm();
226
227 const int nCyclesPerTempo = 11;
228 while ( pTransportPos->getDoubleTick() <
229 pAE->getSongSizeInTicks() ) {
230
231 fBpm = tempoDist( randomEngine );
232 pAE->setNextBpm( fBpm );
233 pAE->updateBpmAndTickSize( pTransportPos );
234 pAE->updateBpmAndTickSize( pQueuingPos );
235
236 nLastLookahead = 0;
237
238 for ( int cc = 0; cc < nCyclesPerTempo; ++cc ) {
239 nFrames = frameDist( randomEngine );
241 QString( "testTransportProcessing : song mode : variable tempo %1->%2" )
242 .arg( fLastBpm, 0, 'f' ).arg( fBpm, 0, 'f' ), nFrames, &nLastLookahead,
243 &nLastTransportFrame, &nTotalFrames, &nLastQueuingTick,
244 &fLastTickIntervalEnd, true );
245 }
246
247 fLastBpm = fBpm;
248
249 nn++;
250 if ( nn > nMaxCycles ) {
252 "[testTransportProcessing] [song mode : variable tempo] end of the song wasn't reached in time." );
253 }
254 }
255
256 pAE->setState( AudioEngine::State::Ready );
257 pAE->unlock();
258
259 // Check consistency of playback in PatternMode
260 pCoreActionController->activateSongMode( false );
261
262 pAE->lock( RIGHT_HERE );
263 pAE->setState( AudioEngine::State::Testing );
264
265 resetVariables();
266
267 fLastBpm = pTransportPos->getBpm();
268
269 const int nDifferentTempos = 10;
270 for ( int tt = 0; tt < nDifferentTempos; ++tt ) {
271
272 fBpm = tempoDist( randomEngine );
273
274 pAE->setNextBpm( fBpm );
275 pAE->updateBpmAndTickSize( pTransportPos );
276 pAE->updateBpmAndTickSize( pQueuingPos );
277
278 nLastLookahead = 0;
279
280 for ( int cc = 0; cc < nCyclesPerTempo; ++cc ) {
281 nFrames = frameDist( randomEngine );
283 QString( "testTransportProcessing : pattern mode : variable tempo %1->%2" )
284 .arg( fLastBpm ).arg( fBpm ), nFrames, &nLastLookahead,
285 &nLastTransportFrame, &nTotalFrames, &nLastQueuingTick,
286 &fLastTickIntervalEnd, true );
287 }
288
289 fLastBpm = fBpm;
290 }
291
292 pAE->setState( AudioEngine::State::Ready );
293 pAE->unlock();
294 pCoreActionController->activateSongMode( true );
295}
296
298 auto pHydrogen = Hydrogen::get_instance();
299 auto pSong = pHydrogen->getSong();
300 auto pTimeline = pHydrogen->getTimeline();
301 auto pPref = Preferences::get_instance();
302 auto pCoreActionController = pHydrogen->getCoreActionController();
303 auto pAE = pHydrogen->getAudioEngine();
304 auto pTransportPos = pAE->getTransportPosition();
305 auto pQueuingPos = pAE->m_pQueuingPosition;
306
307 pCoreActionController->activateLoopMode( true );
308
309 pAE->lock( RIGHT_HERE );
310 pAE->setState( AudioEngine::State::Testing );
311
312 // Activating the Timeline without requiring the AudioEngine to be locked.
313 auto activateTimeline = [&]( bool bEnabled ) {
314 pPref->setUseTimelineBpm( bEnabled );
315 pSong->setIsTimelineActivated( bEnabled );
316
317 if ( bEnabled ) {
318 pTimeline->activate();
319 } else {
320 pTimeline->deactivate();
321 }
322
323 pAE->handleTimelineChange();
324 };
325 activateTimeline( true );
326
327 std::random_device randomSeed;
328 std::default_random_engine randomEngine( randomSeed() );
329 std::uniform_int_distribution<int> frameDist( 1, pPref->m_nBufferSize );
330 std::uniform_real_distribution<float> tempoDist( MIN_BPM, MAX_BPM );
331
332 // For this call the AudioEngine still needs to be in state
333 // Playing or Ready.
334 pAE->reset( false );
335
336 // Check consistency of updated frames, ticks, and queuing
337 // position while using a random buffer size (e.g. like PulseAudio
338 // does).
339 uint32_t nFrames;
340 double fCheckTick, fLastTickIntervalEnd;
341 long long nCheckFrame, nLastTransportFrame, nTotalFrames, nLastLookahead;
342 long nLastQueuingTick;
343 int nn;
344
345 auto resetVariables = [&]() {
346 nLastTransportFrame = 0;
347 nLastQueuingTick = 0;
348 fLastTickIntervalEnd = 0;
349 nTotalFrames = 0;
350 nLastLookahead = 0;
351 nn = 0;
352 };
353 resetVariables();
354
355 const int nMaxCycles =
356 std::max( std::ceil( static_cast<double>(pAE->m_fSongSizeInTicks) /
357 static_cast<double>(pPref->m_nBufferSize) *
358 static_cast<double>(pTransportPos->getTickSize()) * 4.0 ),
359 static_cast<double>(pAE->m_fSongSizeInTicks) );
360
361 while ( pTransportPos->getDoubleTick() <
362 pAE->getSongSizeInTicks() ) {
363 nFrames = frameDist( randomEngine );
365 QString( "[testTransportProcessingTimeline : song mode : all timeline]" ),
366 nFrames, &nLastLookahead, &nLastTransportFrame, &nTotalFrames,
367 &nLastQueuingTick, &fLastTickIntervalEnd, false );
368
369 nn++;
370 if ( nn > nMaxCycles ) {
372 QString( "[testTransportProcessingTimeline] [all timeline] end of the song wasn't reached in time. pTransportPos->getFrame(): %1, pTransportPos->getDoubleTick(): %2, pTransportPos->getTickSize(): %3, pAE->getSongSizeInTicks(): %4, nMaxCycles: %5" )
373 .arg( pTransportPos->getFrame() )
374 .arg( pTransportPos->getDoubleTick(), 0, 'f' )
375 .arg( pTransportPos->getTickSize(), 0, 'f' )
376 .arg( pAE->getSongSizeInTicks(), 0, 'f' )
377 .arg( nMaxCycles ) );
378 }
379 }
380
381 // Alternate Timeline usage and timeline deactivation with
382 // "classical" bpm change".
383
384 pAE->reset( false );
385 resetVariables();
386
387 float fBpm;
388 float fLastBpm = pTransportPos->getBpm();
389
390 const int nCyclesPerTempo = 11;
391 while ( pTransportPos->getDoubleTick() <
392 pAE->getSongSizeInTicks() ) {
393
394 QString sContext;
395 if ( nn % 2 == 0 ){
396 activateTimeline( false );
397 fBpm = tempoDist( randomEngine );
398 pAE->setNextBpm( fBpm );
399 pAE->updateBpmAndTickSize( pTransportPos );
400 pAE->updateBpmAndTickSize( pQueuingPos );
401
402 sContext = "no timeline";
403 }
404 else {
405 activateTimeline( true );
406 fBpm = AudioEngine::getBpmAtColumn( pTransportPos->getColumn() );
407
408 sContext = "timeline";
409 }
410
411 for ( int cc = 0; cc < nCyclesPerTempo; ++cc ) {
412 nFrames = frameDist( randomEngine );
414 QString( "testTransportProcessing : alternating timeline : bpm %1->%2 : %3" )
415 .arg( fLastBpm ).arg( fBpm ).arg( sContext ),
416 nFrames, &nLastLookahead, &nLastTransportFrame, &nTotalFrames,
417 &nLastQueuingTick, &fLastTickIntervalEnd, false );
418 }
419
420 fLastBpm = fBpm;
421
422 nn++;
423 if ( nn > nMaxCycles ) {
425 "[testTransportProcessingTimeline] [alternating timeline] end of the song wasn't reached in time." );
426 }
427 }
428
429 pAE->setState( AudioEngine::State::Ready );
430 pAE->unlock();
431}
432
434 auto pHydrogen = Hydrogen::get_instance();
435 auto pSong = pHydrogen->getSong();
436 auto pPref = Preferences::get_instance();
437 auto pCoreActionController = pHydrogen->getCoreActionController();
438 auto pAE = pHydrogen->getAudioEngine();
439 auto pTransportPos = pAE->getTransportPosition();
440
441 pCoreActionController->activateLoopMode( true );
442 pCoreActionController->activateSongMode( true );
443
444 pAE->lock( RIGHT_HERE );
445 pAE->setState( AudioEngine::State::Testing );
446
447 // For this call the AudioEngine still needs to be in state
448 // Playing or Ready.
449 pAE->reset( false );
450
451 // Check consistency of updated frames, ticks, and queuing
452 // position while using a random buffer size (e.g. like PulseAudio
453 // does).
454 double fLastTickIntervalEnd;
455 long long nLastTransportFrame, nTotalFrames, nLastLookahead;
456 long nLastQueuingTick;
457 int nn;
458
459 auto resetVariables = [&]() {
460 nLastTransportFrame = 0;
461 nLastQueuingTick = 0;
462 fLastTickIntervalEnd = 0;
463 nTotalFrames = 0;
464 nLastLookahead = 0;
465 nn = 0;
466 };
467 resetVariables();
468
469 const int nLoops = 3;
470 const double fSongSizeInTicks = pAE->m_fSongSizeInTicks;
471
472 const int nMaxCycles =
473 std::max( std::ceil( fSongSizeInTicks /
474 static_cast<double>(pPref->m_nBufferSize) *
475 static_cast<double>(pTransportPos->getTickSize()) * 4.0 ),
476 fSongSizeInTicks ) *
477 nLoops;
478
479 // Run nLoops cycles in total. nLoops - 1 with loop mode enabled
480 // and disabling it in the nLoops'th run. This should cause the
481 // transport stop when reaching the end of the song again. The
482 // condition of the while loop will be true for some longer just
483 // to be sure.
484 bool bLoopEnabled = true;
485 int nRet = 0;
486 while ( pTransportPos->getDoubleTick() <
487 fSongSizeInTicks * ( nLoops + 2 ) ) {
488 nRet = processTransport(
489 QString( "[testTransportProcessingTimeline : song mode : all timeline]" ),
490 pPref->m_nBufferSize, &nLastLookahead, &nLastTransportFrame,
491 &nTotalFrames, &nLastQueuingTick, &fLastTickIntervalEnd, false );
492
493 if ( nRet == -1 ) {
494 break;
495 }
496
497
498 // Transport did run for nLoops - 1 rounds, let's deactivate
499 // loop mode.
500 if ( bLoopEnabled && pTransportPos->getDoubleTick() >
501 fSongSizeInTicks * ( nLoops - 1 ) ) {
502 pAE->setState( AudioEngine::State::Ready );
503 pAE->unlock();
504 pCoreActionController->activateLoopMode( false );
505 pAE->lock( RIGHT_HERE );
506 pAE->setState( AudioEngine::State::Testing );
507 }
508
509 nn++;
510 if ( nn > nMaxCycles ||
511 pTransportPos->getDoubleTick() > fSongSizeInTicks * nLoops ) {
513 QString( "[testLoopMode] transport is rolling for too long. pTransportPos: %1,\n\tfSongSizeInTicks(): %2, nLoops: %3, pPref->m_nBufferSize: %4, nMaxCycles: %5" )
514 .arg( pTransportPos->toQString() )
515 .arg( fSongSizeInTicks, 0, 'f' ).arg( nLoops )
516 .arg( pPref->m_nBufferSize ).arg( nMaxCycles ) );
517 }
518 }
519
520 // Ensure transport did run the requested number of loops.
521 if ( pAE->m_pQueuingPosition->getDoubleTick() < fSongSizeInTicks * nLoops ) {
523 QString( "[testLoopMode] transport ended prematurely. pAE->m_pQueuingPosition: %1,\n\tfSongSizeInTicks(): %2, nLoops: %3, pPref->m_nBufferSize: %4" )
524 .arg( pAE->m_pQueuingPosition->toQString() )
525 .arg( fSongSizeInTicks, 0, 'f' ).arg( nLoops )
526 .arg( pPref->m_nBufferSize ) );
527 }
528
529
530 pAE->setState( AudioEngine::State::Ready );
531 pAE->unlock();
532}
533
534int AudioEngineTests::processTransport( const QString& sContext,
535 int nFrames,
536 long long* nLastLookahead,
537 long long* nLastTransportFrame,
538 long long* nTotalFrames,
539 long* nLastQueuingTick,
540 double* fLastTickIntervalEnd,
541 bool bCheckLookahead ) {
542 auto pSong = Hydrogen::get_instance()->getSong();
544 auto pTransportPos = pAE->getTransportPosition();
545 auto pQueuingPos = pAE->m_pQueuingPosition;
546
547 double fTickStart, fTickEnd;
548 const long long nLeadLag =
549 pAE->computeTickInterval( &fTickStart, &fTickEnd, nFrames );
550 fTickStart = pAE->coarseGrainTick( fTickStart );
551 fTickEnd = pAE->coarseGrainTick( fTickEnd );
552
553 if ( bCheckLookahead ) {
554 // If this is the first call after a tempo change, the last
555 // lookahead will be set to 0
556 if ( *nLastLookahead != 0 &&
557 *nLastLookahead != nLeadLag + AudioEngine::nMaxTimeHumanize + 1 ) {
559 QString( "[processTransport : lookahead] [%1] with one and the same BPM/tick size the lookahead must be consistent! [ %2 -> %3 ]" )
560 .arg( sContext ).arg( *nLastLookahead )
561 .arg( nLeadLag + AudioEngine::nMaxTimeHumanize + 1 ) );
562 }
563 *nLastLookahead = nLeadLag + AudioEngine::nMaxTimeHumanize + 1;
564 }
565
566 pAE->updateNoteQueue( nFrames );
567 pAE->incrementTransportPosition( nFrames );
568
569 if ( pAE->isEndOfSongReached( pAE->m_pTransportPosition ) ) {
570 // Don't check consistency at the end of the song as just the
571 // remaining frames are covered.
572 return -1;
573 }
574
576 pTransportPos, "[processTransport] " + sContext );
577
579 pQueuingPos, "[processTransport] " + sContext );
580
581 if ( pTransportPos->getFrame() - nFrames -
582 pTransportPos->getFrameOffsetTempo() != *nLastTransportFrame ) {
584 QString( "[processTransport : transport] [%1] inconsistent frame update. pTransportPos->getFrame(): %2, nFrames: %3, nLastTransportFrame: %4, pTransportPos->getFrameOffsetTempo(): %5, pAE->m_fSongSizeInTicks: %6, pAE->m_nLoopsDone: %7" )
585 .arg( sContext ).arg( pTransportPos->getFrame() ).arg( nFrames )
586 .arg( *nLastTransportFrame ).arg( pTransportPos->getFrameOffsetTempo() )
587 .arg( pAE->m_fSongSizeInTicks ).arg( pAE->m_nLoopsDone ) );
588 }
589 *nLastTransportFrame = pTransportPos->getFrame() -
590 pTransportPos->getFrameOffsetTempo();
591
592 const int nNoteQueueUpdate =
593 static_cast<int>( fTickEnd ) - static_cast<int>( fTickStart );
594 // We will only compare the queuing position in case interval
595 // in updateNoteQueue covers at least one tick and, thus,
596 // an update has actually taken place.
597 if ( *nLastQueuingTick > 0 && nNoteQueueUpdate > 0 ) {
598 if ( pQueuingPos->getTick() - nNoteQueueUpdate !=
599 *nLastQueuingTick &&
600 ! pAE->isEndOfSongReached( pQueuingPos ) ) {
602 QString( "[processTransport : queuing pos] [%1] inconsistent tick update. pQueuingPos->getTick(): %2, nNoteQueueUpdate: %3, nLastQueuingTick: %4, fTickStart: %5, fTickEnd: %6, nFrames = %7, pTransportPos: %8, pQueuingPos: %9, pAE->m_fSongSizeInTicks: %10, pAE->m_nLoopsDone: %11" )
603 .arg( sContext ).arg( pQueuingPos->getTick() )
604 .arg( nNoteQueueUpdate ).arg( *nLastQueuingTick )
605 .arg( fTickStart, 0, 'f' ).arg( fTickEnd, 0, 'f' )
606 .arg( nFrames ).arg( pTransportPos->toQString() )
607 .arg( pQueuingPos->toQString() )
608 .arg( pAE->m_fSongSizeInTicks ).arg( pAE->m_nLoopsDone ));
609 }
610 }
611 *nLastQueuingTick = pQueuingPos->getTick();
612
613 // Check whether the tick interval covered in updateNoteQueue
614 // is consistent and does not include holes or overlaps.
615 // In combination with testNoteEnqueuing this should
616 // guarantuee that all note will be queued properly.
617 if ( std::abs( fTickStart - *fLastTickIntervalEnd ) > 1E-4 ||
618 fTickStart > fTickEnd ) {
620 QString( "[processTransport : tick interval] [%1] inconsistent update. old: [ ... : %2 ], new: [ %3, %4 ], pTransportPos->getTickOffsetQueuing(): %5, diff: %6" )
621 .arg( sContext ).arg( *fLastTickIntervalEnd ).arg( fTickStart )
622 .arg( fTickEnd ).arg( pTransportPos->getTickOffsetQueuing() )
623 .arg( std::abs( fTickStart - *fLastTickIntervalEnd ), 0, 'E', -1 ) );
624 }
625 *fLastTickIntervalEnd = fTickEnd;
626
627 // Using the offset Hydrogen can keep track of the actual
628 // number of frames passed since the playback was started
629 // even in case a tempo change was issued by the user.
630 *nTotalFrames += nFrames;
631 if ( pTransportPos->getFrame() - pTransportPos->getFrameOffsetTempo() !=
632 *nTotalFrames ) {
634 QString( "[processTransport : total] [%1] total frames incorrect. pTransportPos->getFrame(): %2, pTransportPos->getFrameOffsetTempo(): %3, nTotalFrames: %4" )
635 .arg( sContext ).arg( pTransportPos->getFrame() )
636 .arg( pTransportPos->getFrameOffsetTempo() ).arg( *nTotalFrames ) );
637 }
638
639 return 0;
640}
641
643 auto pHydrogen = Hydrogen::get_instance();
644 auto pSong = pHydrogen->getSong();
645 auto pCoreActionController = pHydrogen->getCoreActionController();
646 auto pPref = Preferences::get_instance();
647 auto pAE = pHydrogen->getAudioEngine();
648 auto pTransportPos = pAE->getTransportPosition();
649
650 pAE->lock( RIGHT_HERE );
651 pAE->setState( AudioEngine::State::Testing );
652
653 std::random_device randomSeed;
654 std::default_random_engine randomEngine( randomSeed() );
655 std::uniform_real_distribution<double> tickDist( 0, pAE->m_fSongSizeInTicks );
656 std::uniform_int_distribution<long long> frameDist( 0, pPref->m_nBufferSize );
657
658 // For this call the AudioEngine still needs to be in state
659 // Playing or Ready.
660 pAE->reset( false );
661
662 // Check consistency of updated frames and ticks while relocating
663 // transport.
664 double fNewTick;
665 long long nNewFrame;
666
667 const int nProcessCycles = 100;
668 for ( int nn = 0; nn < nProcessCycles; ++nn ) {
669
670 if ( nn < nProcessCycles - 2 ) {
671 fNewTick = tickDist( randomEngine );
672 }
673 else if ( nn < nProcessCycles - 1 ) {
674 // Resulted in an unfortunate rounding error due to the
675 // song end.
676 fNewTick = pSong->lengthInTicks() - 1 + 0.928009209;
677 }
678 else {
679 // There was a rounding error at this particular tick.
680 fNewTick = std::fmin( 960, pSong->lengthInTicks() );
681 }
682
683 pAE->locate( fNewTick, false );
684
686 pTransportPos, "[testTransportRelocation] mismatch tick-based" );
687
688 // Frame-based relocation
689 nNewFrame = frameDist( randomEngine );
690 pAE->locateToFrame( nNewFrame );
691
693 pTransportPos, "[testTransportRelocation] mismatch frame-based" );
694
695 }
696
697 pAE->reset( false );
698 pAE->setState( AudioEngine::State::Ready );
699 pAE->unlock();
700}
701
703 auto pHydrogen = Hydrogen::get_instance();
704 auto pCoreActionController = pHydrogen->getCoreActionController();
705 auto pSong = pHydrogen->getSong();
706 auto pAE = pHydrogen->getAudioEngine();
707
708 const int nTestColumn = 4;
709
710 pAE->lock( RIGHT_HERE );
711 pAE->setState( AudioEngine::State::Testing );
712 pAE->reset( false );
713 pAE->setState( AudioEngine::State::Ready );
714 pAE->unlock();
715
716 pCoreActionController->activateLoopMode( true );
717 pCoreActionController->locateToColumn( nTestColumn );
718
719 pAE->lock( RIGHT_HERE );
720 pAE->setState( AudioEngine::State::Testing );
721
722 AudioEngineTests::toggleAndCheckConsistency( 1, 1, "[testSongSizeChange] prior" );
723
724 // Toggle a grid cell after to the current transport position
725 AudioEngineTests::toggleAndCheckConsistency( 6, 6, "[testSongSizeChange] after" );
726
727 // Now we head to the "same" position inside the song but with the
728 // transport being looped once.
729 long nNextTick = pHydrogen->getTickForColumn( nTestColumn );
730 if ( nNextTick == -1 ) {
732 QString( "[testSongSizeChange] Bad test design: there is no column [%1]" )
733 .arg( nTestColumn ) );
734 }
735
736 nNextTick += pSong->lengthInTicks();
737
738 pAE->locate( nNextTick );
739
740 AudioEngineTests::toggleAndCheckConsistency( 1, 1, "[testSongSizeChange] looped:prior" );
741
742 // Toggle a grid cell after to the current transport position
743 AudioEngineTests::toggleAndCheckConsistency( 13, 6, "[testSongSizeChange] looped:after" );
744
745 pAE->setState( AudioEngine::State::Ready );
746 pAE->unlock();
747 pCoreActionController->activateLoopMode( false );
748}
749
751 auto pHydrogen = Hydrogen::get_instance();
752 auto pSong = pHydrogen->getSong();
753 auto pCoreActionController = pHydrogen->getCoreActionController();
754 auto pPref = Preferences::get_instance();
755 auto pAE = pHydrogen->getAudioEngine();
756 auto pTransportPos = pAE->getTransportPosition();
757
758 pCoreActionController->activateTimeline( false );
759 pCoreActionController->activateLoopMode( true );
760
761 pAE->lock( RIGHT_HERE );
762 pAE->setState( AudioEngine::State::Testing );
763
764 const int nColumns = pSong->getPatternGroupVector()->size();
765
766 std::random_device randomSeed;
767 std::default_random_engine randomEngine( randomSeed() );
768 std::uniform_real_distribution<double> tickDist( 1, pPref->m_nBufferSize );
769 std::uniform_int_distribution<int> columnDist( nColumns, nColumns + 100 );
770
771 // For this call the AudioEngine still needs to be in state
772 // Playing or Ready.
773 pAE->reset( false );
774
775 const uint32_t nFrames = 500;
776 const double fInitialSongSize = pAE->m_fSongSizeInTicks;
777 int nNewColumn;
778 double fTick;
779
780 auto checkState = [&]( const QString& sContext, bool bSongSizeShouldChange ){
782 pTransportPos,
783 QString( "[testSongSizeChangeInLoopMode::checkState] [%1] before increment" )
784 .arg( sContext ) );
785
786 if ( bSongSizeShouldChange &&
787 fInitialSongSize == pAE->m_fSongSizeInTicks ) {
789 QString( "[testSongSizeChangeInLoopMode] [%1] song size stayed the same [%2->%3]")
790 .arg( sContext ).arg( fInitialSongSize ).arg( pAE->m_fSongSizeInTicks ) );
791 }
792 else if ( ! bSongSizeShouldChange &&
793 fInitialSongSize != pAE->m_fSongSizeInTicks ) {
795 QString( "[testSongSizeChangeInLoopMode] [%1] unexpected song enlargement [%2->%3]")
796 .arg( sContext ).arg( fInitialSongSize ).arg( pAE->m_fSongSizeInTicks ) );
797 }
798
799 pAE->incrementTransportPosition( nFrames );
800
802 pTransportPos,
803 QString( "[testSongSizeChangeInLoopMode::checkState] [%1] after increment" )
804 .arg( sContext ) );
805 };
806
807 const int nNumberOfTogglings = 5;
808 for ( int nn = 0; nn < nNumberOfTogglings; ++nn ) {
809
810 fTick = tickDist( randomEngine );
811 pAE->locate( fInitialSongSize + fTick );
812
813 checkState( QString( "relocation to [%1]" ).arg( fTick ), false );
814
815 nNewColumn = columnDist( randomEngine );
816
817 pAE->setState( AudioEngine::State::Ready );
818 pAE->unlock();
819 pCoreActionController->toggleGridCell( nNewColumn, 0 );
820 pAE->lock( RIGHT_HERE );
821 pAE->setState( AudioEngine::State::Testing );
822
823 checkState( QString( "toggling column [%1]" ).arg( nNewColumn ), true );
824
825 pAE->setState( AudioEngine::State::Ready );
826 pAE->unlock();
827 pCoreActionController->toggleGridCell( nNewColumn, 0 );
828 pAE->lock( RIGHT_HERE );
829 pAE->setState( AudioEngine::State::Testing );
830
831 checkState( QString( "again toggling column [%1]" ).arg( nNewColumn ), false );
832 }
833
834 pAE->setState( AudioEngine::State::Ready );
835 pAE->unlock();
836}
837
839 auto pHydrogen = Hydrogen::get_instance();
840 auto pSong = pHydrogen->getSong();
841 auto pCoreActionController = pHydrogen->getCoreActionController();
842 auto pPref = Preferences::get_instance();
843 auto pAE = pHydrogen->getAudioEngine();
844 auto pSampler = pAE->getSampler();
845 auto pTransportPos = pAE->getTransportPosition();
846 auto pQueuingPos = pAE->m_pQueuingPosition;
847
848 pCoreActionController->activateTimeline( false );
849 pCoreActionController->activateLoopMode( false );
850 pCoreActionController->activateSongMode( true );
851 pAE->lock( RIGHT_HERE );
852 pAE->setState( AudioEngine::State::Testing );
853
854 std::random_device randomSeed;
855 std::default_random_engine randomEngine( randomSeed() );
856 std::uniform_int_distribution<int> frameDist( pPref->m_nBufferSize / 2,
857 pPref->m_nBufferSize );
858
859 // For this call the AudioEngine still needs to be in state
860 // Playing or Ready.
861 pAE->reset( false );
862
863 // Check consistency of updated frames and ticks while using a
864 // random buffer size (e.g. like PulseAudio does).
865
866 uint32_t nFrames;
867 int nn = 0;
868
869 int nMaxCycles =
870 std::max( std::ceil( static_cast<double>(pAE->m_fSongSizeInTicks) /
871 static_cast<double>(pPref->m_nBufferSize) *
872 static_cast<double>(pTransportPos->getTickSize()) * 4.0 ),
873 static_cast<double>(pAE->m_fSongSizeInTicks) );
874
875
876 // Ensure the sampler is clean.
877 AudioEngineTests::resetSampler( "testNoteEnqueuing : song mode" );
878
879 auto notesInSong = pSong->getAllNotes();
880 std::vector<std::shared_ptr<Note>> notesInSongQueue;
881 std::vector<std::shared_ptr<Note>> notesInSamplerQueue;
882
883 auto retrieveNotes = [&]( const QString& sContext ) {
884 // Add freshly enqueued notes.
885 AudioEngineTests::mergeQueues( &notesInSongQueue,
887 pAE->processAudio( nFrames );
888
889 AudioEngineTests::mergeQueues( &notesInSamplerQueue,
890 pSampler->getPlayingNotesQueue() );
891
892 pAE->incrementTransportPosition( nFrames );
893
894 ++nn;
895 if ( nn > nMaxCycles ) {
897 QString( "[testNoteEnqueuing::retrieveNotes] [%1] end of the song wasn't reached in time. pTransportPos->getFrame(): %2, pTransportPos->getDoubleTick(): %3, getTickSize(): %4, pAE->m_fSongSizeInTicks: %5, nMaxCycles: %6" )
898 .arg( sContext ).arg( pTransportPos->getFrame() )
899 .arg( pTransportPos->getDoubleTick(), 0, 'f' )
900 .arg( pTransportPos->getTickSize(), 0, 'f' )
901 .arg( pAE->m_fSongSizeInTicks, 0, 'f' ).arg( nMaxCycles ) );
902 }
903 };
904
905 nn = 0;
906 int nRes;
907 while ( pQueuingPos->getDoubleTick() < pAE->m_fSongSizeInTicks ) {
908
909 nFrames = frameDist( randomEngine );
910 pAE->updateNoteQueue( nFrames );
911 retrieveNotes( "song mode" );
912 }
913
914 auto checkQueueConsistency = [&]( const QString& sContext ) {
915 if ( notesInSongQueue.size() !=
916 notesInSong.size() ) {
917 QString sMsg = QString( "[testNoteEnqueuing::checkQueueConsistency] [%1] Mismatch between notes count in Song [%2] and NoteQueue [%3]. Song:\n" )
918 .arg( sContext ).arg( notesInSong.size() )
919 .arg( notesInSongQueue.size() );
920 for ( int ii = 0; ii < notesInSong.size(); ++ii ) {
921 auto note = notesInSong[ ii ];
922 sMsg.append( QString( "\t[%1] instr: %2, position: %3, noteStart: %4, velocity: %5\n")
923 .arg( ii )
924 .arg( note->get_instrument()->get_name() )
925 .arg( note->get_position() )
926 .arg( note->getNoteStart() )
927 .arg( note->get_velocity() ) );
928 }
929 sMsg.append( "NoteQueue:\n" );
930 for ( int ii = 0; ii < notesInSongQueue.size(); ++ii ) {
931 auto note = notesInSongQueue[ ii ];
932 sMsg.append( QString( "\t[%1] instr: %2, position: %3, noteStart: %4, velocity: %5\n")
933 .arg( ii )
934 .arg( note->get_instrument()->get_name() )
935 .arg( note->get_position() )
936 .arg( note->getNoteStart() )
937 .arg( note->get_velocity() ) );
938 }
939
941 }
942
943 // We have to relax the test for larger buffer sizes. Else, the
944 // notes will be already fully processed in and flush from the
945 // Sampler before we had the chance to grab and compare them.
946 if ( notesInSamplerQueue.size() !=
947 notesInSong.size() &&
948 pPref->m_nBufferSize < 1024 ) {
949 QString sMsg = QString( "[testNoteEnqueuing::checkQueueConsistency] [%1] Mismatch between notes count in Song [%2] and Sampler [%3]. Song:\n" )
950 .arg( sContext ).arg( notesInSong.size() )
951 .arg( notesInSamplerQueue.size() );
952 for ( int ii = 0; ii < notesInSong.size(); ++ii ) {
953 auto note = notesInSong[ ii ];
954 sMsg.append( QString( "\t[%1] instr: %2, position: %3, noteStart: %4, velocity: %5\n")
955 .arg( ii )
956 .arg( note->get_instrument()->get_name() )
957 .arg( note->get_position() )
958 .arg( note->getNoteStart() )
959 .arg( note->get_velocity() ) );
960 }
961 sMsg.append( "SamplerQueue:\n" );
962 for ( int ii = 0; ii < notesInSamplerQueue.size(); ++ii ) {
963 auto note = notesInSamplerQueue[ ii ];
964 sMsg.append( QString( "\t[%1] instr: %2, position: %3, noteStart: %4, velocity: %5\n")
965 .arg( ii )
966 .arg( note->get_instrument()->get_name() )
967 .arg( note->get_position() )
968 .arg( note->getNoteStart() )
969 .arg( note->get_velocity() ) );
970 }
971
973 }
974 };
975 checkQueueConsistency( "song mode" );
976
977 pAE->setState( AudioEngine::State::Ready );
978 pAE->unlock();
979
981 // Perform the test in pattern mode
983
984 pCoreActionController->activateSongMode( false );
985 pHydrogen->setPatternMode( Song::PatternMode::Selected );
986 pHydrogen->setSelectedPatternNumber( 4 );
987
988 pAE->lock( RIGHT_HERE );
989 pAE->setState( AudioEngine::State::Testing );
990
991 AudioEngineTests::resetSampler( "testNoteEnqueuing : pattern mode" );
992
993 auto pPattern =
994 pSong->getPatternList()->get( pHydrogen->getSelectedPatternNumber() );
995 if ( pPattern == nullptr ) {
997 QString( "[testNoteEnqueuing] null pattern selected [%1]" )
998 .arg( pHydrogen->getSelectedPatternNumber() ) );
999 }
1000
1001 int nLoops = 5;
1002 notesInSong.clear();
1003 for ( int ii = 0; ii < nLoops; ++ii ) {
1004 FOREACH_NOTE_CST_IT_BEGIN_LENGTH( pPattern->get_notes(), it, pPattern ) {
1005 if ( it->second != nullptr ) {
1006 auto note = std::make_shared<Note>( it->second );
1007 note->set_position( note->get_position() +
1008 ii * pPattern->get_length() );
1009 notesInSong.push_back( note );
1010 }
1011 }
1012 }
1013
1014 notesInSongQueue.clear();
1015 notesInSamplerQueue.clear();
1016
1017 nMaxCycles =
1018 static_cast<int>(std::max( static_cast<float>(pPattern->get_length()) *
1019 static_cast<float>(nLoops) *
1020 pTransportPos->getTickSize() * 4 /
1021 static_cast<float>(pPref->m_nBufferSize),
1022 static_cast<float>(MAX_NOTES) *
1023 static_cast<float>(nLoops) ));
1024 nn = 0;
1025 while ( pQueuingPos->getDoubleTick() < pPattern->get_length() * nLoops ) {
1026
1027 nFrames = frameDist( randomEngine );
1028 pAE->updateNoteQueue( nFrames );
1029 retrieveNotes( "pattern mode" );
1030 }
1031
1032 // Transport in pattern mode is always looped. We have to pop the
1033 // notes added during the second run due to the lookahead.
1034 auto popSurplusNotes = [&]( std::vector<std::shared_ptr<Note>>* queue ) {
1035 const int nNoteNumber = queue->size();
1036 for ( int ii = 0; ii < nNoteNumber; ++ii ) {
1037 auto pNote = queue->at( nNoteNumber - 1 - ii );
1038 if ( pNote != nullptr &&
1039 pNote->get_position() >= pPattern->get_length() * nLoops ) {
1040 queue->pop_back();
1041 } else {
1042 break;
1043 }
1044 }
1045 };
1046 popSurplusNotes( &notesInSongQueue );
1047 popSurplusNotes( &notesInSamplerQueue );
1048
1049 checkQueueConsistency( "pattern mode" );
1050
1051 pAE->setState( AudioEngine::State::Ready );
1052 pAE->unlock();
1053
1055 // Perform the test in looped song mode
1057 // In case the transport is looped the first note was lost the
1058 // first time transport was wrapped to the beginning again. This
1059 // occurred just in song mode.
1060
1061 pCoreActionController->activateLoopMode( true );
1062 pCoreActionController->activateSongMode( true );
1063
1064 pAE->lock( RIGHT_HERE );
1065 pAE->setState( AudioEngine::State::Testing );
1066 pAE->reset( false );
1067
1068 nLoops = 1;
1069
1070 nMaxCycles =
1071 std::max( std::ceil( static_cast<double>(pAE->m_fSongSizeInTicks) /
1072 static_cast<double>(pPref->m_nBufferSize) *
1073 static_cast<double>(pTransportPos->getTickSize()) * 4.0 ),
1074 static_cast<double>(pAE->m_fSongSizeInTicks) ) *
1075 ( nLoops + 1 );
1076
1077 AudioEngineTests::resetSampler( "testNoteEnqueuing : loop mode" );
1078
1079 notesInSong.clear();
1080 for ( int ii = 0; ii <= nLoops; ++ii ) {
1081 auto notesVec = pSong->getAllNotes();
1082 for ( auto nnote : notesVec ) {
1083 nnote->set_position( nnote->get_position() +
1084 ii * pAE->m_fSongSizeInTicks );
1085 }
1086 notesInSong.insert( notesInSong.end(), notesVec.begin(), notesVec.end() );
1087 }
1088
1089 notesInSongQueue.clear();
1090 notesInSamplerQueue.clear();
1091
1092 nn = 0;
1093 while ( pQueuingPos->getDoubleTick() <
1094 pAE->m_fSongSizeInTicks * ( nLoops + 1 ) ) {
1095
1096 nFrames = frameDist( randomEngine );
1097
1098 // Turn off loop mode once we entered the last loop cycle.
1099 if ( ( pTransportPos->getDoubleTick() >
1100 pAE->m_fSongSizeInTicks * nLoops + 100 ) &&
1101 pSong->getLoopMode() == Song::LoopMode::Enabled ) {
1102 pAE->setState( AudioEngine::State::Ready );
1103 pAE->unlock();
1104 pCoreActionController->activateLoopMode( false );
1105 pAE->lock( RIGHT_HERE );
1106 pAE->setState( AudioEngine::State::Testing );
1107 }
1108
1109 pAE->updateNoteQueue( nFrames );
1110 retrieveNotes( "looped song mode" );
1111 }
1112
1113 checkQueueConsistency( "looped song mode" );
1114
1115 pAE->setState( AudioEngine::State::Ready );
1116 pAE->unlock();
1117}
1118
1120 auto pHydrogen = Hydrogen::get_instance();
1121 auto pSong = pHydrogen->getSong();
1122 auto pAE = pHydrogen->getAudioEngine();
1123 auto pSampler = pAE->getSampler();
1124 auto pTransportPos = pAE->getTransportPosition();
1125 auto pPref = Preferences::get_instance();
1126
1127 pAE->lock( RIGHT_HERE );
1128 pAE->setState( AudioEngine::State::Testing );
1129
1130 std::random_device randomSeed;
1131 std::default_random_engine randomEngine( randomSeed() );
1132 std::uniform_int_distribution<int> frameDist( pPref->m_nBufferSize / 2,
1133 pPref->m_nBufferSize );
1134
1135 // For reset() the AudioEngine still needs to be in state
1136 // Playing or Ready.
1137 pAE->reset( false );
1138 AudioEngineTests::resetSampler( __PRETTY_FUNCTION__ );
1139
1140 uint32_t nFrames;
1141
1142 const int nMaxCycles =
1143 std::max( std::ceil( static_cast<double>(pAE->m_fSongSizeInTicks) /
1144 static_cast<double>(pPref->m_nBufferSize) *
1145 static_cast<double>(pTransportPos->getTickSize()) * 4.0 ),
1146 static_cast<double>(pAE->m_fSongSizeInTicks) );
1147
1148 auto notesInSong = pSong->getAllNotes();
1149 std::vector<std::shared_ptr<Note>> notesInSongQueue;
1150 std::vector<std::shared_ptr<Note>> notesInSamplerQueue;
1151
1152 int nn = 0;
1153 while ( pTransportPos->getDoubleTick() <
1154 pAE->m_fSongSizeInTicks ) {
1155
1156 nFrames = frameDist( randomEngine );
1157
1158 pAE->updateNoteQueue( nFrames );
1159
1160 // Add freshly enqueued notes.
1161 AudioEngineTests::mergeQueues( &notesInSongQueue,
1163
1164 pAE->processAudio( nFrames );
1165
1166 AudioEngineTests::mergeQueues( &notesInSamplerQueue,
1167 pSampler->getPlayingNotesQueue() );
1168
1169 pAE->incrementTransportPosition( nFrames );
1170
1171 ++nn;
1172 if ( nn > nMaxCycles ) {
1174 QString( "[testNoteEnqueuingTimeline] end of the song wasn't reached in time. pTransportPos->getFrame(): %1, pTransportPos->getDoubleTick(): %2, getTickSize(): %3, pAE->m_fSongSizeInTicks: %4, nMaxCycles: %5" )
1175 .arg( pTransportPos->getFrame() )
1176 .arg( pTransportPos->getDoubleTick(), 0, 'f' )
1177 .arg( pTransportPos->getTickSize(), 0, 'f' )
1178 .arg( pAE->m_fSongSizeInTicks, 0, 'f' )
1179 .arg( nMaxCycles ) );
1180 }
1181 }
1182
1183 if ( notesInSongQueue.size() != notesInSong.size() ) {
1185 QString( "Mismatching number of notes in song [%1] and note queue [%2]" )
1186 .arg( notesInSong.size() )
1187 .arg( notesInSongQueue.size() ) );
1188 }
1189
1190 if ( notesInSamplerQueue.size() != notesInSong.size() ) {
1192 QString( "Mismatching number of notes in song [%1] and sampler queue [%2]" )
1193 .arg( notesInSong.size() )
1194 .arg( notesInSamplerQueue.size() ) );
1195 }
1196
1197 // Ensure the ordering of the notes is identical
1198 for ( int ii = 0; ii < notesInSong.size(); ++ii ) {
1199 if ( ! notesInSong[ ii ]->match( notesInSongQueue[ ii ] ) ) {
1201 QString( "Mismatch at note [%1] between song [%2] and song queue [%3]" )
1202 .arg( ii )
1203 .arg( notesInSong[ ii ]->toQString() )
1204 .arg( notesInSongQueue[ ii ]->toQString() ) );
1205 }
1206 if ( ! notesInSong[ ii ]->match( notesInSamplerQueue[ ii ] ) ) {
1208 QString( "Mismatch at note [%1] between song [%2] and sampler queue [%3]" )
1209 .arg( ii )
1210 .arg( notesInSong[ ii ]->toQString() )
1211 .arg( notesInSamplerQueue[ ii ]->toQString() ) );
1212 }
1213 }
1214
1215 pAE->setState( AudioEngine::State::Ready );
1216 pAE->unlock();
1217}
1218
1220 auto pHydrogen = Hydrogen::get_instance();
1221 auto pSong = pHydrogen->getSong();
1222 auto pAE = pHydrogen->getAudioEngine();
1223 auto pSampler = pAE->getSampler();
1224 auto pTransportPos = pAE->getTransportPosition();
1225 auto pPref = Preferences::get_instance();
1226 auto pCoreActionController = pHydrogen->getCoreActionController();
1227
1228 pCoreActionController->activateLoopMode( false );
1229 pCoreActionController->activateSongMode( true );
1230
1231 pAE->lock( RIGHT_HERE );
1232 pAE->setState( AudioEngine::State::Testing );
1233
1234 // For reset() the AudioEngine still needs to be in state
1235 // Playing or Ready.
1236 pAE->reset( false );
1237
1238 // Rolls playback from beginning to the end of the song and
1239 // captures all notes added to the Sampler.
1240 auto getNotes = [&]( std::vector<std::shared_ptr<Note>> *notes ) {
1241
1242 AudioEngineTests::resetSampler( "testHumanization::getNotes" );
1243
1244 // Factor by which the number of frames processed when
1245 // retrieving notes will be smaller than the buffer size. This
1246 // vital because when using a large number of frames below the
1247 // notes might already be processed and flushed from the
1248 // Sampler before we had the chance to retrieve them.
1249 const double fStep = 10.0;
1250 const int nMaxCycles =
1251 std::max( std::ceil( static_cast<double>(pAE->m_fSongSizeInTicks) /
1252 static_cast<double>(pPref->m_nBufferSize) * fStep *
1253 static_cast<double>(pTransportPos->getTickSize()) * 4.0 ),
1254 static_cast<double>(pAE->m_fSongSizeInTicks) );
1255 const uint32_t nFrames = static_cast<uint32_t>(
1256 std::round( static_cast<double>(pPref->m_nBufferSize) / fStep ) );
1257
1258 pAE->locate( 0 );
1259
1260 QString sPlayingPatterns;
1261 for ( const auto& pattern : *pTransportPos->getPlayingPatterns() ) {
1262 sPlayingPatterns += " " + pattern->get_name();
1263 }
1264
1265 int nn = 0;
1266 while ( pTransportPos->getDoubleTick() < pAE->m_fSongSizeInTicks ||
1267 pAE->getEnqueuedNotesNumber() > 0 ) {
1268
1269 pAE->updateNoteQueue( nFrames );
1270
1271 pAE->processAudio( nFrames );
1272
1274 pSampler->getPlayingNotesQueue() );
1275
1276 pAE->incrementTransportPosition( nFrames );
1277
1278 ++nn;
1279 if ( nn > nMaxCycles ) {
1281 QString( "[testHumanization::getNotes] end of the song wasn't reached in time. pTransportPos->getFrame(): %1, pTransportPos->getDoubleTick(): %2, getTickSize(): %3, pAE->m_fSongSizeInTicks: %4, nMaxCycles: %5" )
1282 .arg( pTransportPos->getFrame() )
1283 .arg( pTransportPos->getDoubleTick(), 0, 'f' )
1284 .arg( pTransportPos->getTickSize(), 0, 'f' )
1285 .arg( pAE->m_fSongSizeInTicks, 0, 'f' )
1286 .arg( nMaxCycles ) );
1287 }
1288 }
1289 };
1290
1291 // Sets the rotaries of velocity and timinig humanization as well
1292 // as of the pitch randomization of the Kick Instrument (used for
1293 // the notes in the test song) to a particular value between 0 and
1294 // 1.
1295 auto setHumanization = [&]( double fValue ) {
1296 fValue = std::clamp( fValue, 0.0, 1.0 );
1297
1298 pSong->setHumanizeTimeValue( fValue );
1299 pSong->setHumanizeVelocityValue( fValue );
1300
1301 pSong->getInstrumentList()->get( 0 )->set_random_pitch_factor( fValue );
1302 };
1303
1304 auto setSwing = [&]( double fValue ) {
1305 fValue = std::clamp( fValue, 0.0, 1.0 );
1306
1307 pSong->setSwingFactor( fValue );
1308 };
1309
1310 // Reference notes with no humanization and property
1311 // customization.
1312 auto notesInSong = pSong->getAllNotes();
1313
1314 // First pattern is activated per default.
1315 setHumanization( 0 );
1316 setSwing( 0 );
1317
1318 std::vector<std::shared_ptr<Note>> notesReference;
1319 getNotes( &notesReference );
1320
1321 if ( notesReference.size() != notesInSong.size() ) {
1323 QString( "[testHumanization] [references] Bad test setup. Mismatching number of notes [%1 : %2]" )
1324 .arg( notesReference.size() )
1325 .arg( notesInSong.size() ) );
1326 }
1327
1329 // Pattern 2 contains notes of the same instrument at the same
1330 // positions but velocity, pan, leag&lag, and note key as well as
1331 // note octave are all customized. Check whether these
1332 // customizations reach the Sampler.
1333 pAE->setState( AudioEngine::State::Ready );
1334 pAE->unlock();
1335 pCoreActionController->toggleGridCell( 0, 0 );
1336 pCoreActionController->toggleGridCell( 0, 1 );
1337 pAE->lock( RIGHT_HERE );
1338 pAE->setState( AudioEngine::State::Testing );
1339
1340 std::vector<std::shared_ptr<Note>> notesCustomized;
1341 getNotes( &notesCustomized );
1342
1343 if ( notesReference.size() != notesCustomized.size() ) {
1345 QString( "[testHumanization] [customization] Mismatching number of notes [%1 : %2]" )
1346 .arg( notesReference.size() )
1347 .arg( notesCustomized.size() ) );
1348 }
1349
1350 for ( int ii = 0; ii < notesReference.size(); ++ii ) {
1351 auto pNoteReference = notesReference[ ii ];
1352 auto pNoteCustomized = notesCustomized[ ii ];
1353
1354 if ( pNoteReference != nullptr && pNoteCustomized != nullptr ) {
1355 if ( pNoteReference->get_velocity() ==
1356 pNoteCustomized->get_velocity() ) {
1358 QString( "[testHumanization] [customization] Velocity of note [%1] was not altered" )
1359 .arg( ii ) );
1360 } else if ( pNoteReference->get_lead_lag() ==
1361 pNoteCustomized->get_lead_lag() ) {
1363 QString( "[testHumanization] [customization] Lead Lag of note [%1] was not altered" )
1364 .arg( ii ) );
1365 } else if ( pNoteReference->getNoteStart() ==
1366 pNoteCustomized->getNoteStart() ) {
1367 // The note start incorporates the lead & lag
1368 // information and is the property used by the
1369 // Sampler.
1371 QString( "[testHumanization] [customization] Note start of note [%1] was not altered" )
1372 .arg( ii ) );
1373 } else if ( pNoteReference->getPan() ==
1374 pNoteCustomized->getPan() ) {
1376 QString( "[testHumanization] [customization] Pan of note [%1] was not altered" )
1377 .arg( ii ) );
1378 } else if ( pNoteReference->get_total_pitch() ==
1379 pNoteCustomized->get_total_pitch() ) {
1381 QString( "[testHumanization] [customization] Total Pitch of note [%1] was not altered" )
1382 .arg( ii ) );
1383 }
1384 } else {
1386 QString( "[testHumanization] [customization] Unable to access note [%1]" )
1387 .arg( ii ) );
1388 }
1389
1390 }
1391
1393 // Check whether deviations of the humanized/randomized properties
1394 // are indeed distributed as a Gaussian with mean zero and an
1395 // expected standard deviation.
1396 //
1397 // Switch back to pattern 1
1398 pAE->setState( AudioEngine::State::Ready );
1399 pAE->unlock();
1400 pCoreActionController->toggleGridCell( 0, 1 );
1401 pCoreActionController->toggleGridCell( 0, 0 );
1402 pAE->lock( RIGHT_HERE );
1403 pAE->setState( AudioEngine::State::Testing );
1404
1405 auto checkHumanization = [&]( double fValue, std::vector<std::shared_ptr<Note>>* pNotes ) {
1406
1407 if ( notesReference.size() != pNotes->size() ) {
1409 QString( "[testHumanization] [humanization] Mismatching number of notes [%1 : %2]" )
1410 .arg( notesReference.size() )
1411 .arg( pNotes->size() ) );
1412 }
1413
1414 auto checkDeviation = []( std::vector<float>* pDeviations, float fTargetSD, const QString& sContext ) {
1415
1416 float fMean = std::accumulate( pDeviations->begin(), pDeviations->end(),
1417 0.0, std::plus<float>() ) /
1418 static_cast<float>( pDeviations->size() );
1419
1420 // Standard deviation
1421 auto compVariance = [&]( float fValue, float fElement ) {
1422 return fValue + ( fElement - fMean ) * ( fElement - fMean );
1423 };
1424 float fSD = std::sqrt( std::accumulate( pDeviations->begin(),
1425 pDeviations->end(),
1426 0.0, compVariance ) /
1427 static_cast<float>( pDeviations->size() ) );
1428
1429 // As we look at random numbers, the observed mean and
1430 // standard deviation won't match. But there should be no
1431 // more than 50% difference or something when wrong.
1432 if ( std::abs( fMean ) > std::abs( fSD ) * 0.5 ) {
1434 QString( "[testHumanization] [%1] Mismatching mean [%2] != [0] with std. deviation [%3]" )
1435 .arg( sContext ).arg( fMean, 0, 'E', -1 )
1436 .arg( fSD, 0, 'E', -1 ) );
1437 }
1438 if ( std::abs( fSD - fTargetSD ) > fTargetSD * 0.5 ) {
1440 QString( "[testHumanization] [%1] Mismatching standard deviation [%2] != [%3], diff [%4]" )
1441 .arg( sContext ).arg( fSD, 0, 'E', -1 )
1442 .arg( fTargetSD, 0, 'E', -1 )
1443 .arg( fSD - fTargetSD, 0, 'E', -1 ) );
1444 }
1445
1446 };
1447
1448 std::vector<float> deviationsPitch( notesReference.size() );
1449 std::vector<float> deviationsVelocity( notesReference.size() );
1450 std::vector<float> deviationsTiming( notesReference.size() );
1451
1452 for ( int ii = 0; ii < pNotes->size(); ++ii ) {
1453 auto pNoteReference = notesReference[ ii ];
1454 auto pNoteHumanized = pNotes->at( ii );
1455
1456 if ( pNoteReference != nullptr && pNoteHumanized != nullptr ) {
1457 deviationsVelocity[ ii ] =
1458 pNoteReference->get_velocity() - pNoteHumanized->get_velocity();
1459 deviationsPitch[ ii ] =
1460 pNoteReference->get_pitch() - pNoteHumanized->get_pitch();
1461 deviationsTiming[ ii ] =
1462 pNoteReference->getNoteStart() - pNoteHumanized->getNoteStart();
1463 } else {
1465 QString( "[testHumanization] [swing] Unable to access note [%1]" )
1466 .arg( ii ) );
1467 }
1468 }
1469
1470 // Within the audio engine every property has its own factor
1471 // multiplied with the humanization value set via the
1472 // GUI. With the latter ranging from 0 to 1 the factor
1473 // represent the maximum standard deviation available.
1474 checkDeviation( &deviationsVelocity,
1475 AudioEngine::fHumanizeVelocitySD * fValue, "velocity" );
1476 checkDeviation( &deviationsTiming,
1478 AudioEngine::nMaxTimeHumanize * fValue, "timing" );
1479 checkDeviation( &deviationsPitch,
1480 AudioEngine::fHumanizePitchSD * fValue, "pitch" );
1481 };
1482
1483 setHumanization( 0.2 );
1484 std::vector<std::shared_ptr<Note>> notesHumanizedWeak;
1485 getNotes( &notesHumanizedWeak );
1486 checkHumanization( 0.2, &notesHumanizedWeak );
1487
1488 setHumanization( 0.8 );
1489 std::vector<std::shared_ptr<Note>> notesHumanizedStrong;
1490 getNotes( &notesHumanizedStrong );
1491 checkHumanization( 0.8, &notesHumanizedStrong );
1492
1494 // Check whether swing works.
1495 //
1496 // There is still discussion about HOW the swing should work and
1497 // whether the current implementation is valid. Therefore, this
1498 // test will only check whether setting this option alters at
1499 // least one note position
1500
1501 setHumanization( 0 );
1502 setSwing( 0.5 );
1503 std::vector<std::shared_ptr<Note>> notesSwing;
1504 getNotes( &notesSwing );
1505
1506 if ( notesReference.size() != notesSwing.size() ) {
1508 QString( "[testHumanization] [swing] Mismatching number of notes [%1 : %2]" )
1509 .arg( notesReference.size() )
1510 .arg( notesSwing.size() ) );
1511 }
1512
1513 bool bNoteAltered = false;
1514 for ( int ii = 0; ii < notesReference.size(); ++ii ) {
1515 auto pNoteReference = notesReference[ ii ];
1516 auto pNoteSwing = notesSwing[ ii ];
1517
1518 if ( pNoteReference != nullptr && pNoteSwing != nullptr ) {
1519 if ( pNoteReference->getNoteStart() !=
1520 pNoteSwing->getNoteStart() ) {
1521 bNoteAltered = true;
1522 }
1523 } else {
1525 QString( "[testHumanization] [swing] Unable to access note [%1]" )
1526 .arg( ii ) );
1527 }
1528 }
1529 if ( ! bNoteAltered ) {
1530 AudioEngineTests::throwException( "[testHumanization] [swing] No notes affected." );
1531 }
1532
1534
1535 pAE->setState( AudioEngine::State::Ready );
1536 pAE->unlock();
1537}
1538
1539void AudioEngineTests::mergeQueues( std::vector<std::shared_ptr<Note>>* noteList, std::vector<std::shared_ptr<Note>> newNotes ) {
1540 bool bNoteFound;
1541 for ( const auto& newNote : newNotes ) {
1542 bNoteFound = false;
1543 // Check whether the notes is already present.
1544 for ( const auto& presentNote : *noteList ) {
1545 if ( newNote != nullptr && presentNote != nullptr ) {
1546 if ( newNote->match( presentNote.get() ) &&
1547 newNote->get_position() == presentNote->get_position() &&
1548 newNote->get_velocity() == presentNote->get_velocity() ) {
1549 bNoteFound = true;
1550 }
1551 }
1552 }
1553
1554 if ( ! bNoteFound ) {
1555 noteList->push_back( std::make_shared<Note>(newNote.get()) );
1556 }
1557 }
1558}
1559
1560// Used for the Sampler note queue
1561void AudioEngineTests::mergeQueues( std::vector<std::shared_ptr<Note>>* noteList, std::vector<Note*> newNotes ) {
1562 bool bNoteFound;
1563 for ( const auto& newNote : newNotes ) {
1564 bNoteFound = false;
1565 // Check whether the notes is already present.
1566 for ( const auto& presentNote : *noteList ) {
1567 if ( newNote != nullptr && presentNote != nullptr ) {
1568 if ( newNote->match( presentNote.get() ) &&
1569 newNote->get_position() == presentNote->get_position() &&
1570 newNote->get_velocity() == presentNote->get_velocity() ) {
1571 bNoteFound = true;
1572 }
1573 }
1574 }
1575
1576 if ( ! bNoteFound ) {
1577 noteList->push_back( std::make_shared<Note>(newNote) );
1578 }
1579 }
1580}
1581
1582void AudioEngineTests::checkTransportPosition( std::shared_ptr<TransportPosition> pPos, const QString& sContext ) {
1583
1584 auto pHydrogen = Hydrogen::get_instance();
1585 auto pSong = pHydrogen->getSong();
1586 auto pAE = pHydrogen->getAudioEngine();
1587
1588 double fCheckTickMismatch;
1589 const long long nCheckFrame =
1591 pPos->getDoubleTick(), &fCheckTickMismatch );
1592 const double fCheckTick =
1593 TransportPosition::computeTickFromFrame( pPos->getFrame() );
1594
1595 if ( abs( fCheckTick + fCheckTickMismatch - pPos->getDoubleTick() ) > 1e-9 ||
1596 abs( fCheckTickMismatch - pPos->m_fTickMismatch ) > 1e-9 ) {
1598 QString( "[checkTransportPosition] [%8] [tick mismatch]. original position: [%1],\nnCheckFrame: %2, fCheckTick: %3, fCheckTickMismatch: %4, fCheckTick + fCheckTickMismatch - pPos->getDoubleTick(): %5, fCheckTickMismatch - pPos->m_fTickMismatch: %6, nCheckFrame - pPos->getFrame(): %7" )
1599 .arg( pPos->toQString( "", true ) ).arg( nCheckFrame )
1600 .arg( fCheckTick, 0 , 'f', 9 ).arg( fCheckTickMismatch, 0 , 'f', 9 )
1601 .arg( fCheckTick + fCheckTickMismatch - pPos->getDoubleTick(), 0, 'E' )
1602 .arg( fCheckTickMismatch - pPos->m_fTickMismatch, 0, 'E' )
1603 .arg( nCheckFrame - pPos->getFrame() ).arg( sContext ) );
1604 }
1605
1606 if ( nCheckFrame != pPos->getFrame() ) {
1608 QString( "[checkTransportPosition] [%8] [frame mismatch]. original position: [%1],\nnCheckFrame: %2, fCheckTick: %3, fCheckTickMismatch: %4, fCheckTick + fCheckTickMismatch - pPos->getDoubleTick(): %5, fCheckTickMismatch - pPos->m_fTickMismatch: %6, nCheckFrame - pPos->getFrame(): %7" )
1609 .arg( pPos->toQString( "", true ) ).arg( nCheckFrame )
1610 .arg( fCheckTick, 0 , 'f', 9 ).arg( fCheckTickMismatch, 0 , 'f', 9 )
1611 .arg( fCheckTick + fCheckTickMismatch - pPos->getDoubleTick(), 0, 'E' )
1612 .arg( fCheckTickMismatch - pPos->m_fTickMismatch, 0, 'E' )
1613 .arg( nCheckFrame - pPos->getFrame() ).arg( sContext ) );
1614 }
1615
1616 long nCheckPatternStartTick;
1617 const int nCheckColumn = pHydrogen->getColumnForTick(
1618 pPos->getTick(), pSong->isLoopEnabled(), &nCheckPatternStartTick );
1619 const long nTicksSinceSongStart = static_cast<long>(std::floor(
1620 std::fmod( pPos->getDoubleTick(), pAE->m_fSongSizeInTicks ) ));
1621
1622 if ( pHydrogen->getMode() == Song::Mode::Song && pPos->getColumn() != -1 &&
1623 ( nCheckColumn != pPos->getColumn() ) ) {
1625 QString( "[checkTransportPosition] [%7] [column mismatch]. current position: [%1],\nnCheckColumn: %2, nCheckPatternStartTick: %3, nCheckPatternTickPosition: %4, nTicksSinceSongStart: %5, pAE->m_fSongSizeInTicks: %6" )
1626 .arg( pPos->toQString( "", true ) ).arg( nCheckColumn )
1627 .arg( nCheckPatternStartTick )
1628 .arg( nTicksSinceSongStart - nCheckPatternStartTick )
1629 .arg( nTicksSinceSongStart ).arg( pAE->m_fSongSizeInTicks, 0, 'f' )
1630 .arg( sContext ) );
1631 }
1632
1633 if ( pHydrogen->getMode() == Song::Mode::Song && pPos->getColumn() != -1 &&
1634 ( ( nCheckPatternStartTick != pPos->getPatternStartTick() ) ||
1635 ( nTicksSinceSongStart - nCheckPatternStartTick !=
1636 pPos->getPatternTickPosition() ) ) ) {
1638 QString( "[checkTransportPosition] [%7] [pattern tick mismatch]. current position: [%1],\nnCheckColumn: %2, nCheckPatternStartTick: %3, nCheckPatternTickPosition: %4, nTicksSinceSongStart: %5, pAE->m_fSongSizeInTicks: %6" )
1639 .arg( pPos->toQString( "", true ) ).arg( nCheckColumn )
1640 .arg( nCheckPatternStartTick )
1641 .arg( nTicksSinceSongStart - nCheckPatternStartTick )
1642 .arg( nTicksSinceSongStart ).arg( pAE->m_fSongSizeInTicks, 0, 'f' )
1643 .arg( sContext ) );
1644 }
1645}
1646
1647void AudioEngineTests::checkAudioConsistency( const std::vector<std::shared_ptr<Note>> oldNotes,
1648 const std::vector<std::shared_ptr<Note>> newNotes,
1649 const QString& sContext,
1650 int nPassedFrames, bool bTestAudio,
1651 float fPassedTicks ) {
1652 auto pHydrogen = Hydrogen::get_instance();
1653 auto pSong = pHydrogen->getSong();
1654 auto pAE = pHydrogen->getAudioEngine();
1655 auto pTransportPos = pAE->getTransportPosition();
1656
1657 double fPassedFrames = static_cast<double>(nPassedFrames);
1658 const int nSampleRate = pHydrogen->getAudioOutput()->getSampleRate();
1659
1660 int nNotesFound = 0;
1661 for ( const auto& ppNewNote : newNotes ) {
1662 for ( const auto& ppOldNote : oldNotes ) {
1663 if ( ppNewNote->match( ppOldNote.get() ) &&
1664 ppNewNote->get_humanize_delay() ==
1665 ppOldNote->get_humanize_delay() &&
1666 ppNewNote->get_velocity() == ppOldNote->get_velocity() ) {
1667 ++nNotesFound;
1668
1669 if ( bTestAudio ) {
1670 // Check for consistency in the Sample position
1671 // advanced by the Sampler upon rendering.
1672 for ( int nn = 0; nn < ppNewNote->get_instrument()->get_components()->size(); nn++ ) {
1673 auto pSelectedLayer = ppOldNote->get_layer_selected( nn );
1674
1675 // The frames passed during the audio
1676 // processing depends on the sample rate of
1677 // the driver and sample and has to be
1678 // adjusted in here. This is equivalent to the
1679 // question whether Sampler::renderNote() or
1680 // Sampler::renderNoteResample() was used.
1681 if ( ppOldNote->getSample( nn )->get_sample_rate() !=
1682 nSampleRate ||
1683 ppOldNote->get_total_pitch() != 0.0 ) {
1684 // In here we assume the layer pitch is zero.
1685 fPassedFrames = static_cast<double>(nPassedFrames) *
1686 Note::pitchToFrequency( ppOldNote->get_total_pitch() ) *
1687 static_cast<float>(ppOldNote->getSample( nn )->get_sample_rate()) /
1688 static_cast<float>(nSampleRate);
1689 }
1690
1691 const int nSampleFrames =
1692 ppNewNote->get_instrument()->get_component( nn )
1693 ->get_layer( pSelectedLayer->nSelectedLayer )
1694 ->get_sample()->get_frames();
1695 const double fExpectedFrames =
1696 std::min( static_cast<double>(pSelectedLayer->fSamplePosition) +
1697 fPassedFrames,
1698 static_cast<double>(nSampleFrames) );
1699 if ( std::abs( ppNewNote->get_layer_selected( nn )->fSamplePosition -
1700 fExpectedFrames ) > 1 ) {
1702 QString( "[checkAudioConsistency] [%4] glitch in audio render. Diff: %9\nPre: %1\nPost: %2\nwith passed frames: %3, nSampleFrames: %5, fExpectedFrames: %6, sample sampleRate: %7, driver sampleRate: %8\n" )
1703 .arg( ppOldNote->toQString( "", true ) )
1704 .arg( ppNewNote->toQString( "", true ) )
1705 .arg( fPassedFrames, 0, 'f' ).arg( sContext )
1706 .arg( nSampleFrames ).arg( fExpectedFrames, 0, 'f' )
1707 .arg( ppOldNote->getSample( nn )->get_sample_rate() )
1708 .arg( nSampleRate )
1709 .arg( ppNewNote->get_layer_selected( nn )->fSamplePosition -
1710 fExpectedFrames, 0, 'g', 30 ) );
1711 }
1712 }
1713 }
1714 else { // if ( bTestAudio )
1715 // Check whether changes in note start position
1716 // were properly applied in the note queue of the
1717 // audio engine.
1718 if ( ppNewNote->get_position() - fPassedTicks !=
1719 ppOldNote->get_position() ) {
1721 QString( "[checkAudioConsistency] [%5] glitch in note queue.\n\tPre: %1\n\tPost: %2\n\tfPassedTicks: %3, diff (new - passed - old): %4" )
1722 .arg( ppOldNote->toQString( "", true ) )
1723 .arg( ppNewNote->toQString( "", true ) )
1724 .arg( fPassedTicks )
1725 .arg( ppNewNote->get_position() - fPassedTicks -
1726 ppOldNote->get_position() ).arg( sContext ) );
1727 }
1728 }
1729 }
1730 }
1731 }
1732
1733 // If one of the note vectors is empty - especially the new notes
1734 // - we can not test anything. But such things might happen as we
1735 // try various sample sizes and all notes might be already played
1736 // back and flushed.
1737 if ( nNotesFound == 0 &&
1738 oldNotes.size() > 0 &&
1739 newNotes.size() > 0 ) {
1740 QString sMsg = QString( "[checkAudioConsistency] [%1] bad test design. No notes played back." )
1741 .arg( sContext );
1742 sMsg.append( "\nold notes:" );
1743 for ( auto const& nnote : oldNotes ) {
1744 sMsg.append( "\n" + nnote->toQString( " ", true ) );
1745 }
1746 sMsg.append( "\nnew notes:" );
1747 for ( auto const& nnote : newNotes ) {
1748 sMsg.append( "\n" + nnote->toQString( " ", true ) );
1749 }
1750 sMsg.append( QString( "\n\npTransportPos->getDoubleTick(): %1, pTransportPos->getFrame(): %2, nPassedFrames: %3, fPassedTicks: %4, pTransportPos->getTickSize(): %5" )
1751 .arg( pTransportPos->getDoubleTick(), 0, 'f' )
1752 .arg( pTransportPos->getFrame() )
1753 .arg( nPassedFrames )
1754 .arg( fPassedTicks, 0, 'f' )
1755 .arg( pTransportPos->getTickSize(), 0, 'f' ) );
1756 sMsg.append( "\n\n notes in song:" );
1757 for ( auto const& nnote : pSong->getAllNotes() ) {
1758 sMsg.append( "\n" + nnote->toQString( " ", true ) );
1759 }
1761 }
1762}
1763
1764std::vector<std::shared_ptr<Note>> AudioEngineTests::copySongNoteQueue() {
1765 auto pAE = Hydrogen::get_instance()->getAudioEngine();
1766 std::vector<Note*> rawNotes;
1767 std::vector<std::shared_ptr<Note>> notes;
1768 for ( ; ! pAE->m_songNoteQueue.empty(); pAE->m_songNoteQueue.pop() ) {
1769 rawNotes.push_back( pAE->m_songNoteQueue.top() );
1770 notes.push_back( std::make_shared<Note>( pAE->m_songNoteQueue.top() ) );
1771 }
1772
1773 for ( auto nnote : rawNotes ) {
1774 pAE->m_songNoteQueue.push( nnote );
1775 }
1776
1777 return notes;
1778}
1779
1780void AudioEngineTests::toggleAndCheckConsistency( int nToggleColumn, int nToggleRow, const QString& sContext ) {
1781 auto pHydrogen = Hydrogen::get_instance();
1782 auto pCoreActionController = pHydrogen->getCoreActionController();
1783 auto pSong = pHydrogen->getSong();
1784 auto pAE = pHydrogen->getAudioEngine();
1785 auto pSampler = pAE->getSampler();
1786 auto pTransportPos = pAE->getTransportPosition();
1787
1788 const unsigned long nBufferSize = pHydrogen->getAudioOutput()->getBufferSize();
1789
1790 pAE->updateNoteQueue( nBufferSize );
1791 pAE->processAudio( nBufferSize );
1792 pAE->incrementTransportPosition( nBufferSize );
1793
1794 // Cache some stuff in order to compare it later on.
1795 long nOldSongSize;
1796 int nOldColumn;
1797 float fPrevTempo, fPrevTickSize;
1798 double fPrevTickStart, fPrevTickEnd;
1799 long long nPrevLeadLag;
1800
1801 std::vector<std::shared_ptr<Note>> notesSamplerPreToggle,
1802 notesSamplerPostToggle, notesSamplerPostRolling;
1803
1804 auto notesSongQueuePreToggle = AudioEngineTests::copySongNoteQueue();
1805
1806 auto toggleAndCheck = [&]( const QString& sContext ) {
1807 notesSamplerPreToggle.clear();
1808 for ( const auto& ppNote : pSampler->getPlayingNotesQueue() ) {
1809 notesSamplerPreToggle.push_back( std::make_shared<Note>( ppNote ) );
1810 }
1811
1812 nPrevLeadLag = pAE->computeTickInterval( &fPrevTickStart, &fPrevTickEnd,
1813 nBufferSize );
1814 nOldSongSize = pSong->lengthInTicks();
1815 nOldColumn = pTransportPos->getColumn();
1816 fPrevTempo = pTransportPos->getBpm();
1817 fPrevTickSize = pTransportPos->getTickSize();
1818
1819 pAE->setState( AudioEngine::State::Ready );
1820 pAE->unlock();
1821 pCoreActionController->toggleGridCell( nToggleColumn, nToggleRow );
1822 pAE->lock( RIGHT_HERE );
1823 pAE->setState( AudioEngine::State::Testing );
1824
1825 const QString sNewContext =
1826 QString( "toggleAndCheckConsistency::toggleAndCheck : %1 : toggling (%2,%3)" )
1827 .arg( sContext ).arg( nToggleColumn ).arg( nToggleRow );
1828
1829 // Check whether there was a change in song size
1830 const long nNewSongSize = pSong->lengthInTicks();
1831 if ( nNewSongSize == nOldSongSize ) {
1833 QString( "[%1] no change in song size" ).arg( sNewContext ) );
1834 }
1835
1836 // Check whether current frame and tick information are still
1837 // consistent.
1838 AudioEngineTests::checkTransportPosition( pTransportPos, sNewContext );
1839
1840 // m_songNoteQueue have been updated properly.
1841 const auto notesSongQueuePostToggle = AudioEngineTests::copySongNoteQueue();
1843 notesSongQueuePreToggle, notesSongQueuePostToggle,
1844 sNewContext + " : song queue", 0, false,
1845 pTransportPos->getTickOffsetSongSize() );
1846
1847 // The post toggle state will become the pre toggle state during
1848 // the next toggling.
1849 notesSongQueuePreToggle = notesSongQueuePostToggle;
1850
1851 notesSamplerPostToggle.clear();
1852 for ( const auto& ppNote : pSampler->getPlayingNotesQueue() ) {
1853 notesSamplerPostToggle.push_back( std::make_shared<Note>( ppNote ) );
1854 }
1856 notesSamplerPreToggle, notesSamplerPostToggle,
1857 sNewContext + " : sampler queue", 0, true,
1858 pTransportPos->getTickOffsetSongSize() );
1859
1860 // Column must be consistent. Unless the song length shrunk due to
1861 // the toggling and the previous column was located beyond the new
1862 // end (in which case transport will be reset to 0).
1863 if ( nOldColumn < pSong->getPatternGroupVector()->size() ) {
1864 // Transport was not reset to 0 - happens in most cases.
1865
1866 if ( nOldColumn != pTransportPos->getColumn() &&
1867 nOldColumn < pSong->getPatternGroupVector()->size() ) {
1869 QString( "[%3] Column changed old: %1, new: %2" )
1870 .arg( nOldColumn ).arg( pTransportPos->getColumn() )
1871 .arg( sNewContext ) );
1872 }
1873
1874 double fTickEnd, fTickStart;
1875 const long long nLeadLag =
1876 pAE->computeTickInterval( &fTickStart, &fTickEnd, nBufferSize );
1877 if ( std::abs( nLeadLag - nPrevLeadLag ) > 1 ) {
1879 QString( "[%3] LeadLag should be constant since there should be change in tick size. old: %1, new: %2" )
1880 .arg( nPrevLeadLag ).arg( nLeadLag ).arg( sNewContext ) );
1881 }
1882 if ( std::abs( fTickStart - pTransportPos->getTickOffsetSongSize() - fPrevTickStart ) > 4e-3 ) {
1884 QString( "[%5] Mismatch in the start of the tick interval handled by updateNoteQueue new [%1] != [%2] old+offset, old: %3, offset: %4" )
1885 .arg( fTickStart, 0, 'f' )
1886 .arg( fPrevTickStart + pTransportPos->getTickOffsetSongSize(), 0, 'f' )
1887 .arg( fPrevTickStart, 0, 'f' )
1888 .arg( pTransportPos->getTickOffsetSongSize(), 0, 'f' )
1889 .arg( sNewContext ) );
1890 }
1891 if ( std::abs( fTickEnd - pTransportPos->getTickOffsetSongSize() - fPrevTickEnd ) > 4e-3 ) {
1893 QString( "[%5] Mismatch in the end of the tick interval handled by updateNoteQueue new [%1] != [%2] old+offset, old: %3, offset: %4" )
1894 .arg( fTickEnd, 0, 'f' )
1895 .arg( fPrevTickEnd + pTransportPos->getTickOffsetSongSize(), 0, 'f' )
1896 .arg( fPrevTickEnd, 0, 'f' )
1897 .arg( pTransportPos->getTickOffsetSongSize(), 0, 'f' )
1898 .arg( sNewContext ) );
1899 }
1900 }
1901 else if ( pTransportPos->getColumn() != 0 &&
1902 nOldColumn >= pSong->getPatternGroupVector()->size() ) {
1904 QString( "[%4] Column reset failed nOldColumn: %1, pTransportPos->getColumn() (new): %2, pSong->getPatternGroupVector()->size() (new): %3" )
1905 .arg( nOldColumn )
1906 .arg( pTransportPos->getColumn() )
1907 .arg( pSong->getPatternGroupVector()->size() )
1908 .arg( sNewContext ) );
1909 }
1910
1911 // Now we emulate that playback continues without any new notes
1912 // being added and expect the rendering of the notes currently
1913 // played back by the Sampler to start off precisely where we
1914 // stopped before the song size change. New notes might still be
1915 // added due to the lookahead, so, we just check for the
1916 // processing of notes we already encountered.
1917 pAE->incrementTransportPosition( nBufferSize );
1918 pAE->processAudio( nBufferSize );
1919
1920 // Update the end of the tick interval usually handled by
1921 // updateNoteQueue().
1922 double fTickEndRolling, fTickStartUnused;
1923 pAE->computeTickInterval( &fTickStartUnused, &fTickEndRolling, nBufferSize );
1924
1925 pAE->incrementTransportPosition( nBufferSize );
1926 pAE->processAudio( nBufferSize );
1927
1928 pAE->m_fLastTickEnd = fTickEndRolling;
1929
1930 // Check whether tempo and tick size have not changed.
1931 if ( fPrevTempo != pTransportPos->getBpm() ||
1932 fPrevTickSize != pTransportPos->getTickSize() ) {
1934 QString( "[%1] tempo and ticksize are affected" )
1935 .arg( sNewContext ) );
1936 }
1937
1938 notesSamplerPostRolling.clear();
1939 for ( const auto& ppNote : pSampler->getPlayingNotesQueue() ) {
1940 notesSamplerPostRolling.push_back( std::make_shared<Note>( ppNote ) );
1941 }
1943 notesSamplerPostToggle, notesSamplerPostRolling,
1944 QString( "toggleAndCheckConsistency::toggleAndCheck : %1 : rolling after toggle (%2,%3)" )
1945 .arg( sContext ).arg( nToggleColumn ).arg( nToggleRow ),
1946 nBufferSize * 2, true );
1947 };
1948
1949 // Toggle the grid cell.
1950 toggleAndCheck( sContext + " : 1. toggle" );
1951
1952 // Toggle the same grid cell again.
1953 toggleAndCheck( sContext + " : 2. toggle" );
1954}
1955
1956void AudioEngineTests::resetSampler( const QString& sContext ) {
1957 auto pHydrogen = Hydrogen::get_instance();
1958 auto pSong = pHydrogen->getSong();
1959 auto pAE = pHydrogen->getAudioEngine();
1960 auto pSampler = pAE->getSampler();
1961 auto pPref = Preferences::get_instance();
1962
1963 // Larger number to account for both small buffer sizes and long
1964 // samples.
1965 const int nMaxCleaningCycles = 5000;
1966 int nn = 0;
1967
1968 // Ensure the sampler is clean.
1969 while ( pSampler->isRenderingNotes() ) {
1970 pAE->processAudio( pPref->m_nBufferSize );
1971 pAE->incrementTransportPosition( pPref->m_nBufferSize );
1972 ++nn;
1973
1974 // {//DEBUG
1975 // QString msg = QString( "[%1] nn: %2, note:" )
1976 // .arg( sContext ).arg( nn );
1977 // auto pNoteQueue = pSampler->getPlayingNotesQueue();
1978 // if ( pNoteQueue.size() > 0 ) {
1979 // auto pNote = pNoteQueue[0];
1980 // if ( pNote != nullptr ) {
1981 // msg.append( pNote->toQString("", true ) );
1982 // } else {
1983 // msg.append( " nullptr" );
1984 // }
1985 // DEBUGLOG( msg );
1986 // }
1987 // }
1988
1989 if ( nn > nMaxCleaningCycles ) {
1991 QString( "[%1] Sampler is in weird state" )
1992 .arg( sContext ) );
1993 }
1994 }
1995
1996 pAE->reset( false );
1997}
1998
2000 auto pHydrogen = Hydrogen::get_instance();
2001 auto pSong = pHydrogen->getSong();
2002 auto pCoreActionController = pHydrogen->getCoreActionController();
2003 auto pAE = pHydrogen->getAudioEngine();
2004
2005 pAE->lock( RIGHT_HERE );
2006 pAE->reset( true );
2007
2008 // Check whether the transport positions in the audio engine are untouched
2009 // by updateTransportPosition.
2010 pAE->locate( 42 );
2011 auto pTransportOld =
2012 std::make_shared<TransportPosition>( pAE->getTransportPosition() );
2013 auto pQueuingOld =
2014 std::make_shared<TransportPosition>( pAE->m_pQueuingPosition );
2015
2016 auto pTestPos = std::make_shared<TransportPosition>( "test" );
2017 const long long nFrame = 3521;
2018 const auto fTick = TransportPosition::computeTickFromFrame( nFrame );
2019 pAE->updateTransportPosition( fTick, nFrame, pTestPos );
2020
2021 if ( pAE->getTransportPosition() != pTransportOld ) {
2022 throwException( QString( "[testUpdateTransportPosition] Glitch in pAE->m_pTransportPosition:\nOld: %1\nNew: %2" )
2023 .arg( pTransportOld->toQString() )
2024 .arg( pAE->getTransportPosition()->toQString() ) );
2025 }
2026 if ( pAE->m_pQueuingPosition != pQueuingOld ) {
2027 throwException( QString( "[testUpdateTransportPosition] Glitch in pAE->m_pQueuingPosition:\nOld: %1\nNew: %2" )
2028 .arg( pQueuingOld->toQString() )
2029 .arg( pAE->m_pQueuingPosition->toQString() ) );
2030 }
2031
2032 if ( pTransportOld == pTestPos ) {
2033 throwException( "[testUpdateTransportPosition] Test position shouldn't coincide with pAE->m_pTransportPosition" );
2034 }
2035
2036 pAE->unlock();
2037
2038 // Verify that Hydrogen won't explode in case updateTransportPosition is
2039 // called with no song set (as it is used in
2040 // JackAudioEngine::JackTimebaseCallback which will be called as long as the
2041 // driver is running).
2042 pHydrogen->setSong( nullptr );
2043
2044 pAE->lock( RIGHT_HERE );
2045 auto pNullPos = std::make_shared<TransportPosition>( "null" );
2046 pAE->updateTransportPosition( fTick, nFrame, pNullPos );
2047 pAE->unlock();
2048
2049 pHydrogen->setSong( pSong );
2050}
2051
2052#ifdef H2CORE_HAVE_JACK
2053void AudioEngineTests::testTransportProcessingJack() {
2054 auto pHydrogen = Hydrogen::get_instance();
2055 auto pSong = pHydrogen->getSong();
2056 auto pCoreActionController = pHydrogen->getCoreActionController();
2057 auto pAE = pHydrogen->getAudioEngine();
2058
2059 // Check whether all frames are covered when running playback in song mode
2060 // without looping.
2061 pCoreActionController->activateLoopMode( false );
2062
2063 pAE->lock( RIGHT_HERE );
2064 pAE->reset( true );
2065 pAE->unlock();
2066
2067 auto pDriver = startJackAudioDriver();
2068 if ( pDriver == nullptr ) {
2069 throwException( "[testTransportProcessingJack] Unable to use JACK driver" );
2070 }
2071
2072 // In case the reference Hydrogen is JACK Timebase controller, Timeline of
2073 // this instance is deactivated and we are listening to tempo changes
2074 // broadcasted by the JACK server. We need to verify we actually receive
2075 // them.
2076 bool bTempoChangeEncountered;
2077 float fBpm;
2078
2079 pAE->lock( RIGHT_HERE );
2080 fBpm = pAE->getBpmAtColumn( 0 );
2081
2082 // The callback function registered to the JACK server will take care of
2083 // consistency checking. In here we just start playback and wait till it's
2084 // done.
2085 pAE->play();
2086 pAE->unlock();
2087
2088 // Transport is started via the JACK server. We have to give it some time to
2089 // communicate transport start back to us.
2090 QTest::qSleep( 400 );
2091
2092 const int nMaxMilliSeconds = 11500;
2093 int nMilliSeconds = 0;
2094 const int nIncrement = 100;
2095 while ( pAE->getState() == AudioEngine::State::Playing ||
2096 pAE->getNextState() == AudioEngine::State::Playing ) {
2097 if ( ! bTempoChangeEncountered && fBpm != pAE->getBpmAtColumn( 0 ) ) {
2098 bTempoChangeEncountered = true;
2099 }
2100
2101 if ( nMilliSeconds >= nMaxMilliSeconds ) {
2103 QString( "[testTransportProcessingJack] playback takes too long" ) );
2104 }
2105
2106 QTest::qSleep( nIncrement );
2107 nMilliSeconds += nIncrement;
2108 }
2109
2110 pAE->lock( RIGHT_HERE );
2111 pAE->stop();
2112 if ( pAE->getState() == AudioEngine::State::Playing ) {
2113 pAE->stopPlayback();
2114 }
2115 pAE->reset( true );
2116 pAE->unlock();
2117
2118 if ( pHydrogen->getJackTimebaseState() == JackAudioDriver::Timebase::Listener &&
2119 ! bTempoChangeEncountered ) {
2120 throwException( "[testTransportProcessingJack] no tempo changes received from JACK Timebase controller" );
2121 }
2122
2123 stopJackAudioDriver();
2124}
2125
2126void AudioEngineTests::testTransportProcessingOffsetsJack() {
2127 auto pHydrogen = Hydrogen::get_instance();
2128 auto pSong = pHydrogen->getSong();
2129 auto pCoreActionController = pHydrogen->getCoreActionController();
2130 auto pAE = pHydrogen->getAudioEngine();
2131 auto pTransportPos = pAE->getTransportPosition();
2132
2133 // When being JACK Timebase listener Hydrogen will adopt tempo setting
2134 // provided by an external application and discards internal changes. This
2135 // test would be identical to testTransportProcessingJack.
2136 if ( pHydrogen->getJackTimebaseState() ==
2138 return;
2139 }
2140
2141 // Check whether all frames are covered when running playback in song mode
2142 // without looping.
2143 pCoreActionController->activateLoopMode( false );
2144 pCoreActionController->activateTimeline( false );
2145
2146 std::random_device randomSeed;
2147 std::default_random_engine randomEngine( randomSeed() );
2148 std::uniform_real_distribution<float> tempoDist( MIN_BPM, MAX_BPM );
2149
2150 pAE->lock( RIGHT_HERE );
2151 pAE->reset( true );
2152 pAE->unlock();
2153
2154 auto pDriver = startJackAudioDriver();
2155 if ( pDriver == nullptr ) {
2156 throwException( "[testTransportProcessingOffsetsJack] Unable to use JACK driver" );
2157 }
2158
2159 float fBpm, fLastBpm;
2160 bool bTempoChanged = false;
2161 const int nToggleColumn = 4;
2162 const int nToggleRow = 4;
2163 const float fOriginalSongSize = pAE->m_fSongSizeInTicks;
2164
2165 pAE->lock( RIGHT_HERE );
2166 fLastBpm = pAE->getBpmAtColumn( 0 );
2167 // The callback function registered to the JACK server will take care of
2168 // consistency checking. In here we just start playback and wait till it's
2169 // done.
2170 pAE->play();
2171 pAE->unlock();
2172
2173 // Transport is started via the JACK server. We have to give it some time to
2174 // communicate transport start back to us.
2175 QTest::qSleep( 400 );
2176
2177 const int nMaxMilliSeconds = 11500;
2178 int nMilliSeconds = 0;
2179 const int nIncrement = 100;
2180 while ( pAE->getState() == AudioEngine::State::Playing ||
2181 pAE->getNextState() == AudioEngine::State::Playing ) {
2182 if ( ! bTempoChanged && fLastBpm != pAE->getBpmAtColumn( 0 ) ) {
2183 bTempoChanged = true;
2184 }
2185
2186 if ( nMilliSeconds >= nMaxMilliSeconds ) {
2188 QString( "[testTransportProcessingOffsetsJack] playback takes too long" ) );
2189 }
2190
2191 // Alter song size
2192 //
2193 // It's alright to not lock the AudioEngine when accessing the current
2194 // song size. It should never change during regular playback (which is
2195 // covered by a separate test).
2196 const auto nOldSongSize = pAE->m_fSongSizeInTicks;
2197 pCoreActionController->toggleGridCell( nToggleColumn, nToggleRow );
2198 if ( nOldSongSize == pAE->m_fSongSizeInTicks ) {
2199 throwException( "[testTransportProcessingOffsetsJack] song size did not change." );
2200 }
2201 INFOLOG( QString( "[testTransportProcessingOffsetsJack] update song size [%1] -> [%2]" )
2202 .arg( nOldSongSize ).arg( pAE->m_fSongSizeInTicks ) );
2203
2205 pTransportPos, "[testTransportProcessingOffsetsJack] mismatch after song size update" );
2206
2207 // The sleep helps us to keep song size and tempo-based exceptions
2208 // apart.
2209 QTest::qSleep( nIncrement );
2210 nMilliSeconds += nIncrement;
2211
2212 fBpm = tempoDist( randomEngine );
2213 pAE->lock( RIGHT_HERE );
2214 INFOLOG( QString( "[testTransportProcessingOffsetsJack] changing tempo [%1]->[%2]" )
2215 .arg( pAE->getBpmAtColumn( 0 ) ).arg( fBpm ) );
2216 pAE->setNextBpm( fBpm );
2217 pAE->unlock();
2218
2219 QTest::qSleep( nIncrement );
2220 nMilliSeconds += nIncrement;
2221 }
2222
2223 pAE->lock( RIGHT_HERE );
2224 pAE->stop();
2225 if ( pAE->getState() == AudioEngine::State::Playing ) {
2226 pAE->stopPlayback();
2227 }
2228 pAE->reset( true );
2229 pAE->unlock();
2230
2231 if ( ! bTempoChanged ) {
2232 throwException( "[testTransportProcessingOffsetsJack] tempo was not change. Decrease time increments!" );
2233 }
2234
2235 // Ensure the additional grid cell we activate/deactivate is set to its
2236 // original state.
2237 if ( pAE->m_fSongSizeInTicks != fOriginalSongSize ) {
2238 pCoreActionController->toggleGridCell( nToggleColumn, nToggleRow );
2239 }
2240
2241 stopJackAudioDriver();
2242}
2243
2244void AudioEngineTests::testTransportRelocationJack() {
2245 auto pHydrogen = Hydrogen::get_instance();
2246 auto pSong = pHydrogen->getSong();
2247 auto pPref = Preferences::get_instance();
2248 auto pAE = pHydrogen->getAudioEngine();
2249 auto pTransportPos = pAE->getTransportPosition();
2250
2251 pAE->lock( RIGHT_HERE );
2252 pAE->stop();
2253 if ( pAE->getState() == AudioEngine::State::Playing ) {
2254 pAE->stopPlayback();
2255 }
2256
2257 // For this call the AudioEngine still needs to be in state
2258 // Playing or Ready.
2259 pAE->reset( true );
2260 pAE->unlock();
2261
2262 auto pDriver = startJackAudioDriver();
2263 if ( pDriver == nullptr ) {
2264 throwException( "[testTransportRelocationJack] Unable to use JACK driver" );
2265 }
2266
2267 pAE->lock( RIGHT_HERE );
2268#ifdef HAVE_INTEGRATION_TESTS
2269 JackAudioDriver::m_nIntegrationLastRelocationFrame = -1;
2270 pDriver->m_bIntegrationCheckRelocationLoop = true;
2271#endif
2272 pAE->unlock();
2273
2274 std::random_device randomSeed;
2275 std::default_random_engine randomEngine( randomSeed() );
2276 std::uniform_real_distribution<double> tickDist( 0, pAE->m_fSongSizeInTicks );
2277
2278 // Check consistency of updated frames and ticks while relocating
2279 // transport.
2280 double fNewTick;
2281 long long nNewFrame;
2282
2283
2284 double fTickMismatch;
2285 const int nProcessCycles = 100;
2286 for ( int nn = 0; nn < nProcessCycles; ++nn ) {
2287
2288 // When being listener we have no way to properly check the resulting
2289 // tick as our ground truth is just the frame information provided by
2290 // the JACK server.
2291 if ( pHydrogen->getJackTimebaseState() !=
2293 if ( nn < nProcessCycles - 2 ) {
2294 fNewTick = tickDist( randomEngine );
2295 }
2296 else if ( nn < nProcessCycles - 1 ) {
2297 // Resulted in an unfortunate rounding error due to the
2298 // song end.
2299 fNewTick = pSong->lengthInTicks() - 1 + 0.928009209;
2300 }
2301 else {
2302 // There was a rounding error at this particular tick.
2303 fNewTick = std::fmin( 960, pSong->lengthInTicks() );
2304 }
2305
2306 pAE->lock( RIGHT_HERE );
2307
2308 // Ensure we relocate to a new position. Else the assertions in this
2309 // test will fail.
2310 while ( std::abs( fNewTick - pTransportPos->getDoubleTick() ) < 1 ) {
2311 fNewTick = tickDist( randomEngine );
2312 }
2313
2314 INFOLOG( QString( "relocate to tick [%1]->[%2]" )
2315 .arg( pTransportPos->getDoubleTick() ).arg( fNewTick ) );
2316 pAE->locate( fNewTick, true );
2317 pAE->unlock();
2318
2319 AudioEngineTests::waitForRelocation( pDriver, fNewTick, -1 );
2320 // We send the tick value to and receive it from the JACK server via
2321 // routines in the libjack2 library. We have to be relaxed about the
2322 // precision we can expect.
2323 if ( abs( pTransportPos->getDoubleTick() - fNewTick ) > 1e-1 ) {
2324 throwException( QString( "[testTransportRelocationJack::tick] failed to relocate to tick. [%1] != [%2]" )
2325 .arg( pTransportPos->getDoubleTick() ).arg( fNewTick ) );
2326 }
2327
2328#ifdef HAVE_INTEGRATION_TESTS
2329 // In case there is an issue with the BBT <-> transport position
2330 // conversion or the m_nTimebaseFrameOffset, the driver will detect
2331 // multiple relocations (maybe one in each cycle).
2332 if ( pDriver->m_bIntegrationRelocationLoop ) {
2333 throwException( "[testTransportRelocationJack::frame] relocation loop detected" );
2334 }
2335#endif
2336
2338 pTransportPos, "[testTransportRelocationJack::tick] mismatch tick-based" );
2339 }
2340
2341 // Frame-based relocation
2342 // We sample ticks and convert them since we are using tempo markers.
2343 if ( nn < nProcessCycles - 1 ) {
2345 tickDist( randomEngine ), &fTickMismatch );
2346 } else {
2347 // With this one there were rounding mishaps in v1.2.3
2348 nNewFrame = std::min( static_cast<long long>(2174246),
2350 pSong->lengthInTicks(), &fTickMismatch ) );
2351 }
2352
2353 pAE->lock( RIGHT_HERE );
2354
2355 while ( nNewFrame == pTransportPos->getFrame() ) {
2357 tickDist( randomEngine ), &fTickMismatch );
2358 }
2359
2360#ifdef HAVE_INTEGRATION_TESTS
2361 if ( pHydrogen->getJackTimebaseState() ==
2363 // We are listener
2364 //
2365 // Discard the previous offset or we do not end up at the frame we
2366 // provided to locateTransport and the comparison fails.
2367 pDriver->m_nTimebaseFrameOffset = 0;
2368 JackAudioDriver::m_nIntegrationLastRelocationFrame = -1;
2369 }
2370#endif
2371
2372 INFOLOG( QString( "relocate to frame [%1]->[%2]" )
2373 .arg( pTransportPos->getFrame() ).arg( nNewFrame ) );
2374 pDriver->locateTransport( nNewFrame );
2375 pAE->unlock();
2376
2377 AudioEngineTests::waitForRelocation( pDriver, -1, nNewFrame );
2378
2379 long long nCurrentFrame;
2380 if ( pHydrogen->getJackTimebaseState() ==
2382 nCurrentFrame = pDriver->m_JackTransportPos.frame;
2383 }
2384 else {
2385 nCurrentFrame = pTransportPos->getFrame();
2386 }
2387
2388 if ( nNewFrame != nCurrentFrame ) {
2389 throwException( QString( "[testTransportRelocationJack::frame] failed to relocate to frame. timebase state: [%1], nNewFrame [%2] != nCurrentFrame [%3], pPos->getFrame(): [%4]" )
2391 pDriver->getTimebaseState() ) )
2392 .arg( nNewFrame )
2393 .arg( nCurrentFrame )
2394 .arg( pTransportPos->getFrame() ) );
2395 }
2396
2397#ifdef HAVE_INTEGRATION_TESTS
2398 // In case there is an issue with the BBT <-> transport position
2399 // conversion or the m_nTimebaseFrameOffset, the driver will detect
2400 // multiple relocations (maybe one in each cycle).
2401 if ( pDriver->m_bIntegrationRelocationLoop ) {
2402 throwException( "[testTransportRelocationJack::frame] relocation loop detected" );
2403 }
2404#endif
2405
2407 pTransportPos, "[testTransportRelocationJack::frame] mismatch frame-based" );
2408 }
2409
2410 pAE->lock( RIGHT_HERE );
2411#ifdef HAVE_INTEGRATION_TESTS
2412 pDriver->m_bIntegrationCheckRelocationLoop = false;
2413 JackAudioDriver::m_nIntegrationLastRelocationFrame = -1;
2414#endif
2415 pAE->reset( true );
2416 pAE->unlock();
2417
2418 stopJackAudioDriver();
2419}
2420
2421void AudioEngineTests::testTransportRelocationOffsetsJack() {
2422 auto pHydrogen = Hydrogen::get_instance();
2423 auto pSong = pHydrogen->getSong();
2424 auto pPref = Preferences::get_instance();
2425 auto pAE = pHydrogen->getAudioEngine();
2426 auto pTransportPos = pAE->getTransportPosition();
2427 auto pCoreActionController = pHydrogen->getCoreActionController();
2428
2429 // When being JACK Timebase listener Hydrogen will adopt tempo setting
2430 // provided by an external application and discards internal changes. This
2431 // test would be identical to testTransportProcessingJack.
2432 if ( pHydrogen->getJackTimebaseState() ==
2434 return;
2435 }
2436
2437 pCoreActionController->activateTimeline( false );
2438
2439 pAE->lock( RIGHT_HERE );
2440 pAE->stop();
2441 if ( pAE->getState() == AudioEngine::State::Playing ) {
2442 pAE->stopPlayback();
2443 }
2444
2445 // For this call the AudioEngine still needs to be in state
2446 // Playing or Ready.
2447 pAE->reset( true );
2448 pAE->unlock();
2449
2450 auto pDriver = startJackAudioDriver();
2451 if ( pDriver == nullptr ) {
2452 throwException( "[testTransportRelocationOffsetsJack] Unable to use JACK driver" );
2453 }
2454 float fBpm, fLastBpm;
2455 bool bTempoChanged = false;
2456 const int nToggleColumn = 4;
2457 const int nToggleRow = 4;
2458 const float fOriginalSongSize = pAE->m_fSongSizeInTicks;
2459
2460 pAE->lock( RIGHT_HERE );
2461 fLastBpm = pAE->getBpmAtColumn( 0 );
2462#ifdef HAVE_INTEGRATION_TESTS
2463 JackAudioDriver::m_nIntegrationLastRelocationFrame = -1;
2464 pDriver->m_bIntegrationCheckRelocationLoop = true;
2465#endif
2466 pAE->unlock();
2467
2468 std::random_device randomSeed;
2469 std::default_random_engine randomEngine( randomSeed() );
2470 std::uniform_real_distribution<double> tickDist( 0, pAE->m_fSongSizeInTicks );
2471 std::uniform_real_distribution<float> tempoDist( MIN_BPM, MAX_BPM );
2472
2473 // Check consistency of updated frames and ticks while relocating
2474 // transport.
2475 double fNewTick;
2476 long long nNewFrame;
2477 double fTickMismatch;
2478 const int nProcessCycles = 100;
2479 for ( int nn = 0; nn < nProcessCycles; ++nn ) {
2480 if ( ! bTempoChanged && fLastBpm != pAE->getBpmAtColumn( 0 ) ) {
2481 bTempoChanged = true;
2482 }
2483
2484 // When being listener we have no way to properly check the resulting
2485 // tick as our ground truth is just the frame information provided by
2486 // the JACK server.
2487 if ( pHydrogen->getJackTimebaseState() !=
2489 if ( nn < nProcessCycles - 2 ) {
2490 fNewTick = tickDist( randomEngine );
2491 }
2492 else if ( nn < nProcessCycles - 1 ) {
2493 // Resulted in an unfortunate rounding error due to the
2494 // song end.
2495 fNewTick = pSong->lengthInTicks() - 1 + 0.928009209;
2496 }
2497 else {
2498 // There was a rounding error at this particular tick.
2499 fNewTick = std::fmin( 960, pSong->lengthInTicks() );
2500 }
2501
2502 pAE->lock( RIGHT_HERE );
2503
2504 // Ensure we relocate to a new position. Else the assertions in this
2505 // test will fail.
2506 while ( std::abs( fNewTick - pTransportPos->getDoubleTick() ) < 1 ) {
2507 fNewTick = tickDist( randomEngine );
2508 }
2509
2510 INFOLOG( QString( "relocate to tick [%1]->[%2]" )
2511 .arg( pTransportPos->getDoubleTick() ).arg( fNewTick ) );
2512 pAE->locate( fNewTick, true );
2513 pAE->unlock();
2514
2515 AudioEngineTests::waitForRelocation( pDriver, fNewTick, -1 );
2516 // We send the tick value to and receive it from the JACK server via
2517 // routines in the libjack2 library. We have to be relaxed about the
2518 // precision we can expect.
2519 if ( abs( pTransportPos->getDoubleTick() - fNewTick ) > 1e-1 ) {
2520 throwException( QString( "[testTransportRelocationOffsetsJack::tick] failed to relocate to tick. [%1] != [%2]" )
2521 .arg( pTransportPos->getDoubleTick() ).arg( fNewTick ) );
2522 }
2523
2524#ifdef HAVE_INTEGRATION_TESTS
2525 // In case there is an issue with the BBT <-> transport position
2526 // conversion or the m_nTimebaseFrameOffset, the driver will detect
2527 // multiple relocations (maybe one in each cycle).
2528 if ( pDriver->m_bIntegrationRelocationLoop ) {
2529 throwException( "[testTransportRelocationOffsetsJack::frame] relocation loop detected" );
2530 }
2531#endif
2532
2534 pTransportPos, "[testTransportRelocationOffsetsJack::tick] mismatch tick-based" );
2535 }
2536
2537 // Alter song size
2538 //
2539 // It's alright to not lock the AudioEngine when accessing the current
2540 // song size. It should never change during regular playback (which is
2541 // covered by a separate test).
2542 const auto nOldSongSize = pAE->m_fSongSizeInTicks;
2543 pCoreActionController->toggleGridCell( 4, 4 );
2544 if ( nOldSongSize == pAE->m_fSongSizeInTicks ) {
2545 throwException( "[testTransportRelocationOffsetsJack] song size did not change." );
2546 }
2547 INFOLOG( QString( "[testTransportRelocationOffsetsJack] update song size [%1] -> [%2]" )
2548 .arg( nOldSongSize ).arg( pAE->m_fSongSizeInTicks ) );
2549
2550 // Frame-based relocation
2551 // We sample ticks and convert them since we are using tempo markers.
2552 if ( nn < nProcessCycles - 1 ) {
2554 tickDist( randomEngine ), &fTickMismatch );
2555 } else {
2556 // With this one there were rounding mishaps in v1.2.3
2557 nNewFrame = std::min( static_cast<long long>(2174246),
2559 pSong->lengthInTicks(), &fTickMismatch ) );
2560 }
2561
2562 pAE->lock( RIGHT_HERE );
2563
2564 while ( nNewFrame == pTransportPos->getFrame() ) {
2566 tickDist( randomEngine ), &fTickMismatch );
2567 }
2568
2569#ifdef HAVE_INTEGRATION_TESTS
2570 if ( pHydrogen->getJackTimebaseState() ==
2572 // We are listener
2573 //
2574 // Discard the previous offset or we do not end up at the frame we
2575 // provided to locateTransport and the comparison fails.
2576 pDriver->m_nTimebaseFrameOffset = 0;
2577 JackAudioDriver::m_nIntegrationLastRelocationFrame = -1;
2578 }
2579#endif
2580
2582 pTransportPos, "[testTransportRelocationOffsetsJack] mismatch after song size update" );
2583
2584 INFOLOG( QString( "relocate to frame [%1]->[%2]" )
2585 .arg( pTransportPos->getFrame() ).arg( nNewFrame ) );
2586 pDriver->locateTransport( nNewFrame );
2587 pAE->unlock();
2588
2589 AudioEngineTests::waitForRelocation( pDriver, -1, nNewFrame );
2590
2591 long long nCurrentFrame;
2592 if ( pHydrogen->getJackTimebaseState() ==
2594 nCurrentFrame = pDriver->m_JackTransportPos.frame;
2595 }
2596 else {
2597 nCurrentFrame = pTransportPos->getFrame();
2598 }
2599
2600 if ( nNewFrame != nCurrentFrame ) {
2601 throwException( QString( "[testTransportRelocationOffsetsJack::frame] failed to relocate to frame. timebase state: [%1], nNewFrame [%2] != nCurrentFrame [%3], pPos->getFrame(): [%4]" )
2603 pDriver->getTimebaseState() ) )
2604 .arg( nNewFrame )
2605 .arg( nCurrentFrame )
2606 .arg( pTransportPos->getFrame() ) );
2607 }
2608
2609#ifdef HAVE_INTEGRATION_TESTS
2610 // In case there is an issue with the BBT <-> transport position
2611 // conversion or the m_nTimebaseFrameOffset, the driver will detect
2612 // multiple relocations (maybe one in each cycle).
2613 if ( pDriver->m_bIntegrationRelocationLoop ) {
2614 throwException( "[testTransportRelocationOffsetsJack::frame] relocation loop detected" );
2615 }
2616#endif
2617
2619 pTransportPos, "[testTransportRelocationOffsetsJack::frame] mismatch frame-based" );
2620
2621 // Alter tempo
2622 fBpm = tempoDist( randomEngine );
2623 pAE->lock( RIGHT_HERE );
2624 INFOLOG( QString( "[testTransportRelocationOffsetsJack] changing tempo [%1]->[%2]" )
2625 .arg( pAE->getBpmAtColumn( 0 ) ).arg( fBpm ) );
2626 pAE->setNextBpm( fBpm );
2627 pAE->unlock();
2628
2629 // Give Hydrogen some time to apply the tempo changes. Else, we might
2630 // run into race conditions with relocation being triggered before tempo
2631 // was applied. In such a case we loose the former offsets and won't be
2632 // able to properly check for matching position.
2633 QTest::qSleep( 25 );
2634
2636 pTransportPos, "[testTransportRelocationOffsetsJack::tempo] mismatch after tempo change" );
2637 }
2638
2639 pAE->lock( RIGHT_HERE );
2640#ifdef HAVE_INTEGRATION_TESTS
2641 pDriver->m_bIntegrationCheckRelocationLoop = false;
2642 JackAudioDriver::m_nIntegrationLastRelocationFrame = -1;
2643#endif
2644 pAE->reset( true );
2645 pAE->unlock();
2646
2647 if ( ! bTempoChanged ) {
2648 throwException( "[testTransportRelocationOffsetsJack] tempo was not change." );
2649 }
2650
2651 // Ensure the additional grid cell we activate/deactivate is set to its
2652 // original state.
2653 if ( pAE->m_fSongSizeInTicks != fOriginalSongSize ) {
2654 pCoreActionController->toggleGridCell( nToggleColumn, nToggleRow );
2655 }
2656
2657 stopJackAudioDriver();
2658}
2659
2660JackAudioDriver* AudioEngineTests::startJackAudioDriver() {
2661 INFOLOG( "Starting custom JACK audio driver..." );
2662
2663 auto pHydrogen = Hydrogen::get_instance();
2664 auto pAudioEngine = pHydrogen->getAudioEngine();
2665 auto pPref = Preferences::get_instance();
2666
2667 if ( pAudioEngine->getState() == AudioEngine::State::Testing ) {
2668 throwException( "[startJackAudioDriver] Engine must not be locked and in state testing yet!" );
2669 }
2670
2671 pAudioEngine->stopAudioDrivers();
2672
2673 // Start a modified version of the JACK audio driver.
2674 auto pDriver = new JackAudioDriver( jackTestProcessCallback );
2675 if ( pDriver == nullptr ) {
2676 throwException( "[startJackAudioDriver] Unable to create JackAudioDriver" );
2677 }
2678#ifdef H2CORE_HAVE_JACK
2679
2680 // Suppress default audio output
2681 pDriver->setConnectDefaults( false );
2682#else
2683 throwException( "[startJackAudioDriver] This function should not be run without JACK support" );
2684#endif
2685 pAudioEngine->lock( RIGHT_HERE );
2686
2687 if ( pDriver->init( pPref->m_nBufferSize ) != 0 ) {
2688 delete pDriver;
2689 pAudioEngine->unlock();
2690 throwException( "[startJackAudioDriver] Unable to initialize driver" );
2691 }
2692
2693 // Driver needs to be initialized in order to properly set its timebase
2694 // state.
2695 if ( pDriver->m_timebaseState == JackAudioDriver::Timebase::Controller &&
2696 m_referenceTimebase != JackAudioDriver::Timebase::Controller ) {
2697 INFOLOG( "Releasing test binary as Timebase controller" );
2698 pDriver->releaseTimebaseControl();
2699 }
2700 else if ( pDriver->m_timebaseState != JackAudioDriver::Timebase::Controller &&
2701 m_referenceTimebase == JackAudioDriver::Timebase::Controller ) {
2702 INFOLOG( "Register test binary as Timebase controller" );
2703 pDriver->initTimebaseControl();
2704 }
2705 pDriver->m_timebaseState = m_referenceTimebase;
2706 pDriver->m_timebaseTracking = JackAudioDriver::TimebaseTracking::Valid;
2707 pAudioEngine->m_MutexOutputPointer.lock();
2708
2709 pAudioEngine->m_pAudioDriver = pDriver;
2710 pAudioEngine->setState( AudioEngine::State::Ready );
2711
2712 pAudioEngine->m_MutexOutputPointer.unlock();
2713 pAudioEngine->unlock();
2714
2715 if ( pDriver->connect() != 0 ) {
2716 pAudioEngine->restartAudioDrivers();
2717 throwException( "[startJackAudioDriver] Unable to connect driver" );
2718 }
2719
2720 if ( pHydrogen->getSong() != nullptr ) {
2721 pAudioEngine->lock( RIGHT_HERE );
2722 pAudioEngine->handleDriverChange();
2723 pAudioEngine->unlock();
2724 }
2725
2726 INFOLOG( "DONE Starting custom JACK audio driver." );
2727
2728 return pDriver;
2729}
2730
2731void AudioEngineTests::stopJackAudioDriver() {
2732 INFOLOG( "Stopping custom JACK audio driver..." );
2733
2734 auto pHydrogen = Hydrogen::get_instance();
2735 auto pAudioEngine = pHydrogen->getAudioEngine();
2736
2737 if ( pAudioEngine->getState() == AudioEngine::State::Testing ) {
2738 throwException( "[stopJackAudioDriver] Engine must not be locked and in state testing yet!" );
2739 }
2740
2741 // We rely on the driver set via the Preferences (most probably FakeDriver).
2742 pAudioEngine->restartAudioDrivers();
2743
2744#ifdef H2CORE_HAVE_JACK
2745 auto pDriver = dynamic_cast<JackAudioDriver*>(pAudioEngine->m_pAudioDriver);
2746 if ( pDriver == nullptr ) {
2748 "[stopJackAudioDriver] No JACK driver after restart!" );
2749 }
2750
2751 pDriver->m_timebaseState = m_referenceTimebase;
2752 pDriver->m_timebaseTracking = JackAudioDriver::TimebaseTracking::Valid;
2753#endif
2754
2755 INFOLOG( "DONE Stopping custom JACK audio driver." );
2756}
2757
2758void AudioEngineTests::waitForRelocation( JackAudioDriver* pDriver,
2759 double fTick, long long nFrame ) {
2760 auto pHydrogen = Hydrogen::get_instance();
2761 auto pAE = pHydrogen->getAudioEngine();
2762 auto pTransportPos = pAE->getTransportPosition();
2763
2764 const int nMaxMilliSeconds = 5000;
2765 const int nSecondTryMilliSeconds = 1000;
2766 int nMilliSeconds = 0;
2767 const int nIncrement = 100;
2768
2769 // We send the tick value to and received it back from the JACK server
2770 // via routines of the libjack2 library. We have to be relaxed about the
2771 // precision we can expect from relocating.
2772 while ( true ) {
2773 long long nCurrentFrame;
2774 if ( pHydrogen->getJackTimebaseState() ==
2776 nCurrentFrame = pDriver->m_JackTransportPos.frame;
2777 } else {
2778 nCurrentFrame = pTransportPos->getFrame();
2779 }
2780
2781 if ( ( nFrame != -1 && nFrame == nCurrentFrame ) ||
2782 ( fTick != -1 &&
2783 abs( pTransportPos->getDoubleTick() - fTick ) < 1e-1 ) ) {
2784 return;
2785 }
2786
2787 if ( nMilliSeconds >= nMaxMilliSeconds ) {
2788 QString sTarget;
2789 if ( nFrame != -1 ) {
2790 sTarget = QString( "frame [%1]" ).arg( nFrame );
2791 } else {
2792 sTarget = QString( "tick [%1]" ).arg( fTick );
2793 }
2795 QString( "[AudioEngineTests::waitForRelocation] playback takes too long to reach %1" )
2796 .arg( sTarget ) );
2797 } else if ( nMilliSeconds == nSecondTryMilliSeconds ) {
2798 WARNINGLOG( QString( "[AudioEngineTests::waitForRelocation] Performing seconds attempt after [%1]ms" )
2799 .arg( nMilliSeconds ) );
2800
2801 // Occassionally the JACK server seems to drop our relocation
2802 // attempt silently. This is not good but acceptable since we
2803 // are firing them in rapid succession. That's not the usual
2804 // use-case. In order to not make this behavior break our test,
2805 // we do a second attempt to be sure.
2806 if ( fTick != -1 ) {
2807 pAE->lock( RIGHT_HERE );
2808 pAE->locate( fTick, true );
2809 pAE->unlock();
2810 }
2811 else {
2812 pAE->lock( RIGHT_HERE );
2813
2814#ifdef HAVE_INTEGRATION_TESTS
2815 if ( pHydrogen->getJackTimebaseState() ==
2817 // We are listener
2818 //
2819 // Discard the previous offset or we do not end up at
2820 // the frame we provided to locateTransport and the
2821 // comparison fails.
2822 pDriver->m_nTimebaseFrameOffset = 0;
2823 JackAudioDriver::m_nIntegrationLastRelocationFrame = -1;
2824 }
2825#endif
2826
2827 pDriver->locateTransport( nFrame );
2828 pAE->unlock();
2829 }
2830 }
2831
2832 QTest::qSleep( nIncrement );
2833 nMilliSeconds += nIncrement;
2834 }
2835}
2836
2837int AudioEngineTests::jackTestProcessCallback( uint32_t nframes, void* args ) {
2838
2839 AudioEngine* pAudioEngine = Hydrogen::get_instance()->getAudioEngine();
2840 auto pDriver = dynamic_cast<JackAudioDriver*>(pAudioEngine->m_pAudioDriver);
2841 if ( pDriver == nullptr ) {
2843 "[jackTestProcessCallback] No JACK driver!" );
2844 }
2845
2846 // For the JACK driver it is very important (#1867) to not do anything while
2847 // the JACK client is stopped/closed. Otherwise it will segfault on mutex
2848 // locking or message logging.
2849 if ( ! ( pAudioEngine->getState() == AudioEngine::State::Ready ||
2850 pAudioEngine->getState() == AudioEngine::State::Playing ) ) {
2851 return 0;
2852 }
2853 const auto sDrivers = pAudioEngine->getDriverNames();
2854
2855 if ( pAudioEngine->getState() == AudioEngine::State::Testing ) {
2857 QString( "[jackTestProcessCallback] [%1] engine must not be in state Testing" )
2858 .arg( sDrivers ) );
2859 }
2860
2861 pAudioEngine->clearAudioBuffers( nframes );
2862
2863 // Calculate maximum time to wait for audio engine lock. Using the
2864 // last calculated processing time as an estimate of the expected
2865 // processing time for this frame.
2866 float sampleRate = static_cast<float>(pDriver->getSampleRate());
2867 pAudioEngine->m_fMaxProcessTime = 1000.0 / ( sampleRate / nframes );
2868 float fSlackTime = pAudioEngine->m_fMaxProcessTime - pAudioEngine->m_fProcessTime;
2869
2870 // If we expect to take longer than the available time to process,
2871 // require immediate locking or not at all: we're bound to drop a
2872 // buffer anyway.
2873 if ( fSlackTime < 0.0 ) {
2874 fSlackTime = 0.0;
2875 }
2876
2877 /*
2878 * The "try_lock" was introduced for Bug #164 (Deadlock after during
2879 * alsa driver shutdown). The try_lock *should* only fail in rare circumstances
2880 * (like shutting down drivers). In such cases, it seems to be ok to interrupt
2881 * audio processing. Returning the special return value "2" enables the disk
2882 * writer driver to repeat the processing of the current data.
2883 */
2884 if ( !pAudioEngine->tryLockFor( std::chrono::microseconds( (int)(1000.0*fSlackTime) ),
2885 RIGHT_HERE ) ) {
2886 ___ERRORLOG( QString( "[%1] Failed to lock audioEngine in allowed %2 ms, missed buffer" )
2887 .arg( sDrivers ).arg( fSlackTime ) );
2888 return 0;
2889 }
2890
2891 // Now that the engine is locked we properly check its state.
2892 if ( ! ( pAudioEngine->getState() == AudioEngine::State::Ready ||
2893 pAudioEngine->getState() == AudioEngine::State::Playing ) ) {
2894 pAudioEngine->unlock();
2895 return 0;
2896 }
2897
2898 Hydrogen* pHydrogen = Hydrogen::get_instance();
2899 std::shared_ptr<Song> pSong = pHydrogen->getSong();
2900 if ( pSong == nullptr ) {
2902 QString( "[jackTestProcessCallback] [%1] no song set yet" )
2903 .arg( sDrivers ) );
2904 }
2905
2907 pAudioEngine->getTransportPosition(),
2908 QString( "[jackTestProcessCallback] [%1] : pre updated" )
2909 .arg( sDrivers ) );
2910
2911 // Sync transport with server (in case the current audio driver is
2912 // designed that way)
2913#ifdef H2CORE_HAVE_JACK
2914 if ( Hydrogen::get_instance()->hasJackTransport() ) {
2915 pDriver->updateTransportPosition();
2916
2917#ifdef HAVE_INTEGRATION_TESTS
2918 if ( pDriver->m_bIntegrationRelocationLoop ) {
2920 "[jackTestProcessCallback] Relocation loop detected!" );
2921 }
2922#endif
2923
2924 // Check consistency of current JACK position.
2925 const auto jackPos = pDriver->getJackPosition();
2926 if ( ( jackPos.valid & JackPositionBBT ) &&
2927 ! JackAudioDriver::isBBTValid( jackPos ) ) {
2929 "[jackTestProcessCallback] Inconsistent JACK BBT information" );
2930 }
2931
2932 // Check consistency of BBT conversion functions
2933 const auto pTransportPos = pAudioEngine->getTransportPosition();
2934 jack_position_t testPos;
2935 if ( pTransportPos->getDoubleTick() >=
2936 pAudioEngine->m_fSongSizeInTicks ) {
2937 testPos.frame = 0;
2938 testPos.tick = 0;
2939 } else {
2940 testPos.frame = pTransportPos->getFrame();
2941 testPos.tick = pTransportPos->getDoubleTick();
2942 }
2943 JackAudioDriver::transportToBBT( *pTransportPos, &testPos );
2944
2945 if ( ! JackAudioDriver::isBBTValid( testPos ) ) {
2947 "[jackTestProcessCallback::transportToBBT] Invalid JACK position: %1,\ntransport pos: %2" )
2949 .arg( pTransportPos->toQString() ) );
2950 }
2951
2952 const auto fTick = JackAudioDriver::bbtToTick( testPos );
2953 if ( pTransportPos->getDoubleTick() <
2954 pAudioEngine->m_fSongSizeInTicks &&
2955 std::abs( fTick - pTransportPos->getTick() ) > 1e-5 ) {
2957 "[jackTestProcessCallback] Mismatching ticks after BBT conversion: diff: %1, fTick: %2, pAE->m_fSongSizeInTicks: %3\nJACK pos: %4\nTransport pos: %5" )
2958 .arg( std::abs( fTick - pTransportPos->getTick() ), 0, 'f' )
2959 .arg( fTick ).arg( pAudioEngine->m_fSongSizeInTicks )
2961 .arg( pTransportPos->toQString() ) );
2962 }
2963 else if ( pTransportPos->getDoubleTick() >=
2964 pAudioEngine->m_fSongSizeInTicks && fTick != 0 ) {
2966 "[jackTestProcessCallback] Mismatching ticks after BBT conversion at end of song: fTick: %1, pAE->m_fSongSizeInTicks: %2\nJACK pos: %3\nTransport pos: %4" )
2967 .arg( fTick ).arg( pAudioEngine->m_fSongSizeInTicks )
2969 .arg( pTransportPos->toQString() ) );
2970 }
2971 }
2972 else {
2974 QString( "[jackTestProcessCallback] [%1] callback should only be used with JACK driver!" )
2975 .arg( sDrivers ) );
2976 }
2977#endif
2978 // Check whether the Timebase state is still the same.
2979 if ( pDriver->getTimebaseState() != AudioEngineTests::m_referenceTimebase ) {
2981 QString( "[jackTestProcessCallback] Timebase state changed. Current: [%1], reference: [%2]" )
2982 .arg( JackAudioDriver::TimebaseToQString( pDriver->getTimebaseState() ) )
2984 AudioEngineTests::m_referenceTimebase ) ) );
2985 }
2986
2987 // Check whether the tempo was changed.
2988 pAudioEngine->updateBpmAndTickSize( pAudioEngine->m_pTransportPosition );
2989 pAudioEngine->updateBpmAndTickSize( pAudioEngine->m_pQueuingPosition );
2990
2992 pAudioEngine->getTransportPosition(),
2993 QString( "[jackTestProcessCallback] [%1] : post JACK" )
2994 .arg( sDrivers ) );
2995
2996 // Update the state of the audio engine depending on whether it
2997 // was started or stopped by the user.
2998 if ( pAudioEngine->getNextState() == AudioEngine::State::Playing ) {
2999 if ( pAudioEngine->getState() == AudioEngine::State::Ready ) {
3000 pAudioEngine->startPlayback();
3001 }
3002
3003 pAudioEngine->setRealtimeFrame( pAudioEngine->m_pTransportPosition->getFrame() );
3004 } else {
3005 if ( pAudioEngine->getState() == AudioEngine::State::Playing ) {
3006 pAudioEngine->stopPlayback();
3007 }
3008
3009 // go ahead and increment the realtimeframes by nFrames
3010 // to support our realtime keyboard and midi event timing
3011 pAudioEngine->setRealtimeFrame( pAudioEngine->getRealtimeFrame() +
3012 static_cast<long long>(nframes) );
3013 }
3014
3015 // always update note queue.. could come from pattern or realtime input
3016 // (midi, keyboard)
3017 pAudioEngine->updateNoteQueue( nframes );
3018
3019 pAudioEngine->processAudio( nframes );
3020
3021 if ( pAudioEngine->getState() == AudioEngine::State::Playing ) {
3022
3023 // Check whether the end of the song has been reached.
3024 if ( pAudioEngine->isEndOfSongReached(
3025 pAudioEngine->m_pTransportPosition ) ) {
3026
3027 ___INFOLOG( QString( "[%1] End of song received" ).arg( sDrivers ) );
3028
3029 if ( pHydrogen->getMidiOutput() != nullptr ) {
3030 pHydrogen->getMidiOutput()->handleQueueAllNoteOff();
3031 }
3032
3033 pAudioEngine->stop();
3034 pAudioEngine->stopPlayback();
3035 pAudioEngine->locate( 0 );
3036 }
3037 else {
3038 // We are not at the end of the song, keep rolling.
3039 pAudioEngine->incrementTransportPosition( nframes );
3040 }
3041 }
3042
3044 pAudioEngine->getTransportPosition(),
3045 QString( "[jackTestProcessCallback] [%1] : post update" )
3046 .arg( sDrivers ) );
3047
3048 pAudioEngine->unlock();
3049
3050 return 0;
3051}
3052#endif // H2CORE_HAVE_JACK
3053
3054void AudioEngineTests::throwException( const QString& sMsg ) {
3055 auto pHydrogen = Hydrogen::get_instance();
3056 auto pAE = pHydrogen->getAudioEngine();
3057
3058 pAE->setState( AudioEngine::State::Ready );
3059 pAE->unlock();
3060
3061 const auto sMsgLocal8Bit = sMsg.toLocal8Bit();
3062 throw std::runtime_error( sMsgLocal8Bit.data() );
3063}
3064
3065
3066}; // namespace H2Core
#define RIGHT_HERE
Macro intended to be used for the logging of the locking of the H2Core::AudioEngine.
Definition AudioEngine.h:61
#define INFOLOG(x)
Definition Object.h:240
#define WARNINGLOG(x)
Definition Object.h:241
#define ___INFOLOG(x)
Definition Object.h:258
#define ___ERRORLOG(x)
Definition Object.h:260
#define FOREACH_NOTE_CST_IT_BEGIN_LENGTH(_notes, _it, _pattern)
Iterate over all accessible notes between position 0 and length of _pattern in an immutable way.
Definition Pattern.h:285
static std::vector< std::shared_ptr< Note > > copySongNoteQueue()
static void testTransportProcessingTimeline()
More detailed test of the transport and playhead integrity in case Timeline/tempo markers are involve...
static void testUpdateTransportPosition()
Checks is reproducible and works even without any song set.
static void testLoopMode()
Unit test checking that loop mode as well as deactivating it works.
static void testSongSizeChange()
Unit test checking consistency of transport position when playback was looped at least once and the s...
static void toggleAndCheckConsistency(int nToggleColumn, int nToggleRow, const QString &sContext)
Toggles the grid cell defined by nToggleColumn and nToggleRow twice and checks whether the transport ...
static void testNoteEnqueuing()
Unit test checking that all notes in a song are picked up once.
static void checkTransportPosition(std::shared_ptr< TransportPosition > pPos, const QString &sContext)
Checks the consistency of the transport position pPos by converting the current tick,...
static void testTransportRelocation()
Unit test checking the relocation of the transport position in audioEngine_process().
static void testTransportProcessing()
Unit test checking the incremental update of the transport position in audioEngine_process().
static void testNoteEnqueuingTimeline()
Checks whether the order of notes enqueued and processed by the Sampler is consistent on tempo change...
static void testSongSizeChangeInLoopMode()
Unit test checking consistency of transport position when playback was looped at least once and the s...
static void resetSampler(const QString &sContext)
static int processTransport(const QString &sContext, int nFrames, long long *nLastLookahead, long long *nLastTransportFrame, long long *nTotalFrames, long *nLastPlayheadTick, double *fLastTickIntervalEnd, bool bCheckLookahead=true)
static void mergeQueues(std::vector< std::shared_ptr< Note > > *noteList, std::vector< std::shared_ptr< Note > > newNotes)
Add every Note in newNotes not yet contained in noteList to the latter.
static void throwException(const QString &sMsg)
static void testFrameToTickConversion()
Unit test checking for consistency when converting frames to ticks and back.
static void testHumanization()
Unit test checking that custom note properties take effect and that humanization works as expected.
static void checkAudioConsistency(const std::vector< std::shared_ptr< Note > > oldNotes, const std::vector< std::shared_ptr< Note > > newNotes, const QString &sContext, int nPassedFrames, bool bTestAudio=true, float fPassedTicks=0.0)
Takes two instances of Sampler::m_playingNotesQueue and checks whether matching notes have exactly nP...
QString getDriverNames() const
static float getBpmAtColumn(int nColumn)
static constexpr float fHumanizePitchSD
Maximum value the standard deviation of the Gaussian distribution the random pitch contribution will ...
@ Playing
Transport is rolling.
@ Ready
Ready to process audio.
@ Testing
State used during the unit tests of the AudioEngine.
static constexpr float fHumanizeTimingSD
Maximum value the standard deviation of the Gaussian distribution the random pitch contribution will ...
static constexpr int nMaxTimeHumanize
Maximum time (in frames) a note's position can be off due to the humanization (lead-lag).
static constexpr float fHumanizeVelocitySD
Maximum value the standard deviation of the Gaussian distribution the random velocity contribution wi...
const std::shared_ptr< TransportPosition > getTransportPosition() const
virtual QString toQString(const QString &sPrefix="", bool bShort=true) const
Formatted string version for debugging purposes.
Definition Object.cpp:186
std::shared_ptr< Song > getSong() const
Get the current song.
Definition Hydrogen.h:123
static Hydrogen * get_instance()
Returns the current Hydrogen instance __instance.
Definition Hydrogen.h:84
AudioEngine * getAudioEngine() const
Definition Hydrogen.h:663
JACK (Jack Audio Connection Kit) server driver.
@ Valid
Current timebase state is on par with JACK server.
static void transportToBBT(const TransportPosition &transportPos, jack_position_t *pPos)
static QString JackTransportPosToQString(const jack_position_t &pPos)
static double bbtToTick(const jack_position_t &pos)
Timebase
Whether Hydrogen or another program is in Timebase control.
@ Listener
An external program is Timebase controller and Hydrogen will disregard all tempo markers on the Timel...
@ None
Only normal clients registered.
@ Controller
Hydrogen itself is Timebase controller and provides its current tempo to other Timebase listeners.
static QString TimebaseToQString(const Timebase &t)
static bool isBBTValid(const jack_position_t &pos)
static double pitchToFrequency(double fPitch)
Convert a logarithmic pitch-space value in semitones to a frequency-domain value.
Definition Note.h:387
static Preferences * get_instance()
Returns a pointer to the current Preferences singleton stored in __instance.
@ Selected
Only one pattern - the one currently selected in the GUI - will be played back.
Definition Song.h:106
static long long computeFrameFromTick(double fTick, double *fTickMismatch, int nSampleRate=0)
Calculates frame equivalent of fTick.
static double computeTickFromFrame(long long nFrame, int nSampleRate=0)
Calculates tick equivalent of nFrame.
#define MAX_NOTES
Maximum number of notes.
Definition config.dox:79
#define MAX_BPM
Definition Globals.h:36
#define MIN_BPM
Definition Globals.h:35