hydrogen 1.2.3
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-2024 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
28
31#include <core/Basics/Pattern.h>
33#include <core/Basics/Note.h>
34#include <core/Basics/Sample.h>
35#include <core/Basics/Song.h>
37#include <core/Hydrogen.h>
40#include <core/config.h>
41
42namespace H2Core
43{
44
46 auto pHydrogen = Hydrogen::get_instance();
47 auto pCoreActionController = pHydrogen->getCoreActionController();
48 auto pAE = pHydrogen->getAudioEngine();
49
50 pCoreActionController->activateTimeline( true );
51 pCoreActionController->addTempoMarker( 0, 120 );
52 pCoreActionController->addTempoMarker( 3, 100 );
53 pCoreActionController->addTempoMarker( 5, 40 );
54 pCoreActionController->addTempoMarker( 7, 200 );
55
56 auto checkFrame = []( long long nFrame, double fTolerance ) {
57 const double fTick = TransportPosition::computeTickFromFrame( nFrame );
58
59 double fTickMismatch;
60 const long long nFrameCheck =
61 TransportPosition::computeFrameFromTick( fTick, &fTickMismatch );
62
63 if ( nFrameCheck != nFrame || std::abs( fTickMismatch ) > fTolerance ) {
65 QString( "[testFrameToTickConversion::checkFrame] nFrame: %1, fTick: %2, nFrameComputed: %3, fTickMismatch: %4, frame diff: %5, fTolerance: %6" )
66 .arg( nFrame ).arg( fTick, 0, 'E', -1 ).arg( nFrameCheck )
67 .arg( fTickMismatch, 0, 'E', -1 ).arg( nFrameCheck - nFrame )
68 .arg( fTolerance, 0, 'E', -1 ) );
69 }
70 };
71 checkFrame( 342732, 1e-10 );
72 checkFrame( 1037223, 1e-10 );
73 checkFrame( 453610333722, 1e-6 );
74
75 auto checkTick = []( double fTick, double fTolerance ) {
76 double fTickMismatch;
77 const long long nFrame =
78 TransportPosition::computeFrameFromTick( fTick, &fTickMismatch );
79
80 const double fTickCheck =
81 TransportPosition::computeTickFromFrame( nFrame ) + fTickMismatch;
82
83 if ( abs( fTickCheck - fTick ) > fTolerance ) {
85 QString( "[testFrameToTickConversion::checkTick] nFrame: %1, fTick: %2, fTickComputed: %3, fTickMismatch: %4, tick diff: %5, fTolerance: %6" )
86 .arg( nFrame ).arg( fTick, 0, 'E', -1 ).arg( fTickCheck, 0, 'E', -1 )
87 .arg( fTickMismatch, 0, 'E', -1 ).arg( fTickCheck - fTick, 0, 'E', -1 )
88 .arg( fTolerance, 0, 'E', -1 ));
89 }
90 };
91 checkTick( 552, 1e-9 );
92 checkTick( 1939, 1e-9 );
93 checkTick( 534623409, 1e-6 );
94 checkTick( pAE->m_fSongSizeInTicks * 3, 1e-9 );
95}
96
98 auto pHydrogen = Hydrogen::get_instance();
99 auto pSong = pHydrogen->getSong();
100 auto pPref = Preferences::get_instance();
101 auto pCoreActionController = pHydrogen->getCoreActionController();
102 auto pAE = pHydrogen->getAudioEngine();
103 auto pTransportPos = pAE->getTransportPosition();
104 auto pQueuingPos = pAE->m_pQueuingPosition;
105
106 pCoreActionController->activateTimeline( false );
107 pCoreActionController->activateLoopMode( true );
108
109 pAE->lock( RIGHT_HERE );
110 pAE->setState( AudioEngine::State::Testing );
111
112 std::random_device randomSeed;
113 std::default_random_engine randomEngine( randomSeed() );
114 std::uniform_int_distribution<int> frameDist( 1, pPref->m_nBufferSize );
115 std::uniform_real_distribution<float> tempoDist( MIN_BPM, MAX_BPM );
116
117 // For this call the AudioEngine still needs to be in state
118 // Playing or Ready.
119 pAE->reset( false );
120 pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
121
122 // Check consistency of updated frames, ticks, and queuing
123 // position while using a random buffer size (e.g. like PulseAudio
124 // does).
125 uint32_t nFrames;
126 double fCheckTick, fLastTickIntervalEnd;
127 long long nCheckFrame, nLastTransportFrame, nTotalFrames, nLastLookahead;
128 long nLastQueuingTick;
129 int nn;
130
131 auto resetVariables = [&]() {
132 nLastTransportFrame = 0;
133 nLastQueuingTick = 0;
134 fLastTickIntervalEnd = 0;
135 nTotalFrames = 0;
136 nLastLookahead = 0;
137 nn = 0;
138 };
139 resetVariables();
140
141 const int nMaxCycles =
142 std::max( std::ceil( static_cast<double>(pAE->m_fSongSizeInTicks) /
143 static_cast<double>(pPref->m_nBufferSize) *
144 static_cast<double>(pTransportPos->getTickSize()) * 4.0 ),
145 static_cast<double>(pAE->m_fSongSizeInTicks) );
146
147 while ( pTransportPos->getDoubleTick() <
148 pAE->getSongSizeInTicks() ) {
149 nFrames = frameDist( randomEngine );
151 "testTransportProcessing : song mode : constant tempo", nFrames,
152 &nLastLookahead, &nLastTransportFrame, &nTotalFrames,
153 &nLastQueuingTick, &fLastTickIntervalEnd, true );
154
155 nn++;
156 if ( nn > nMaxCycles ) {
158 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" )
159 .arg( pTransportPos->getFrame() )
160 .arg( pTransportPos->getDoubleTick(), 0, 'f' )
161 .arg( pTransportPos->getTickSize(), 0, 'f' )
162 .arg( pAE->getSongSizeInTicks(), 0, 'f' )
163 .arg( nMaxCycles ) );
164 }
165 }
166
167 pAE->reset( false );
168 pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
169
170 pAE->setState( AudioEngine::State::Ready );
171 pAE->unlock();
172
173 // Check whether all frames are covered when running playback in song mode
174 // without looping.
175 pCoreActionController->activateLoopMode( false );
176
177 pAE->lock( RIGHT_HERE );
178 pAE->setState( AudioEngine::State::Testing );
179 resetVariables();
180 while ( nn <= nMaxCycles ) {
181 nFrames = frameDist( randomEngine );
182 pAE->incrementTransportPosition( nFrames );
183
184 if ( pAE->isEndOfSongReached( pAE->m_pTransportPosition ) ) {
185 // End of song reached
186 if ( pTransportPos->getTick() < pAE->getSongSizeInTicks() ) {
188 QString( "[testTransportProcessing] [song mode : no looping] final tick was not reached at song end. pTransportPos->getTick: [%1], pAE->getSongSizeInTicks: %2" )
189 .arg( pTransportPos->getTick() ).arg( pAE->getSongSizeInTicks() ) );
190 }
191 break;
192 }
193
194 nn++;
195 if ( nn > nMaxCycles ) {
197 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" )
198 .arg( pTransportPos->getFrame() )
199 .arg( pTransportPos->getDoubleTick(), 0, 'f' )
200 .arg( pTransportPos->getTickSize(), 0, 'f' )
201 .arg( pAE->getSongSizeInTicks(), 0, 'f' )
202 .arg( nMaxCycles ) );
203 }
204 }
205
206 pAE->reset( false );
207 pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
208
209 pAE->setState( AudioEngine::State::Ready );
210 pAE->unlock();
211
212 // Check whether all frames are covered when running playback in song mode
213 // without looping.
214 pCoreActionController->activateLoopMode( true );
215
216 pAE->lock( RIGHT_HERE );
217 pAE->setState( AudioEngine::State::Testing );
218
219 resetVariables();
220
221 float fBpm;
222 float fLastBpm = pTransportPos->getBpm();
223
224 const int nCyclesPerTempo = 11;
225 while ( pTransportPos->getDoubleTick() <
226 pAE->getSongSizeInTicks() ) {
227
228 fBpm = tempoDist( randomEngine );
229 pAE->setNextBpm( fBpm );
230 pAE->updateBpmAndTickSize( pTransportPos );
231 pAE->updateBpmAndTickSize( pQueuingPos );
232
233 nLastLookahead = 0;
234
235 for ( int cc = 0; cc < nCyclesPerTempo; ++cc ) {
236 nFrames = frameDist( randomEngine );
238 QString( "testTransportProcessing : song mode : variable tempo %1->%2" )
239 .arg( fLastBpm, 0, 'f' ).arg( fBpm, 0, 'f' ), nFrames, &nLastLookahead,
240 &nLastTransportFrame, &nTotalFrames, &nLastQueuingTick,
241 &fLastTickIntervalEnd, true );
242 }
243
244 fLastBpm = fBpm;
245
246 nn++;
247 if ( nn > nMaxCycles ) {
249 "[testTransportProcessing] [song mode : variable tempo] end of the song wasn't reached in time." );
250 }
251 }
252
253 pAE->setState( AudioEngine::State::Ready );
254 pAE->unlock();
255
256 // Check consistency of playback in PatternMode
257 pCoreActionController->activateSongMode( false );
258
259 pAE->lock( RIGHT_HERE );
260 pAE->setState( AudioEngine::State::Testing );
261
262 resetVariables();
263
264 fLastBpm = pTransportPos->getBpm();
265
266 const int nDifferentTempos = 10;
267 for ( int tt = 0; tt < nDifferentTempos; ++tt ) {
268
269 fBpm = tempoDist( randomEngine );
270
271 pAE->setNextBpm( fBpm );
272 pAE->updateBpmAndTickSize( pTransportPos );
273 pAE->updateBpmAndTickSize( pQueuingPos );
274
275 nLastLookahead = 0;
276
277 for ( int cc = 0; cc < nCyclesPerTempo; ++cc ) {
278 nFrames = frameDist( randomEngine );
280 QString( "testTransportProcessing : pattern mode : variable tempo %1->%2" )
281 .arg( fLastBpm ).arg( fBpm ), nFrames, &nLastLookahead,
282 &nLastTransportFrame, &nTotalFrames, &nLastQueuingTick,
283 &fLastTickIntervalEnd, true );
284 }
285
286 fLastBpm = fBpm;
287 }
288
289 pAE->setState( AudioEngine::State::Ready );
290 pAE->unlock();
291 pCoreActionController->activateSongMode( true );
292}
293
295 auto pHydrogen = Hydrogen::get_instance();
296 auto pSong = pHydrogen->getSong();
297 auto pTimeline = pHydrogen->getTimeline();
298 auto pPref = Preferences::get_instance();
299 auto pCoreActionController = pHydrogen->getCoreActionController();
300 auto pAE = pHydrogen->getAudioEngine();
301 auto pTransportPos = pAE->getTransportPosition();
302 auto pQueuingPos = pAE->m_pQueuingPosition;
303
304 pCoreActionController->activateLoopMode( true );
305
306 pAE->lock( RIGHT_HERE );
307 pAE->setState( AudioEngine::State::Testing );
308
309 // Activating the Timeline without requiring the AudioEngine to be locked.
310 auto activateTimeline = [&]( bool bEnabled ) {
311 pPref->setUseTimelineBpm( bEnabled );
312 pSong->setIsTimelineActivated( bEnabled );
313
314 if ( bEnabled ) {
315 pTimeline->activate();
316 } else {
317 pTimeline->deactivate();
318 }
319
320 pAE->handleTimelineChange();
321 };
322 activateTimeline( true );
323
324 std::random_device randomSeed;
325 std::default_random_engine randomEngine( randomSeed() );
326 std::uniform_int_distribution<int> frameDist( 1, pPref->m_nBufferSize );
327 std::uniform_real_distribution<float> tempoDist( MIN_BPM, MAX_BPM );
328
329 // For this call the AudioEngine still needs to be in state
330 // Playing or Ready.
331 pAE->reset( false );
332 pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
333
334 // Check consistency of updated frames, ticks, and queuing
335 // position while using a random buffer size (e.g. like PulseAudio
336 // does).
337 uint32_t nFrames;
338 double fCheckTick, fLastTickIntervalEnd;
339 long long nCheckFrame, nLastTransportFrame, nTotalFrames, nLastLookahead;
340 long nLastQueuingTick;
341 int nn;
342
343 auto resetVariables = [&]() {
344 nLastTransportFrame = 0;
345 nLastQueuingTick = 0;
346 fLastTickIntervalEnd = 0;
347 nTotalFrames = 0;
348 nLastLookahead = 0;
349 nn = 0;
350 };
351 resetVariables();
352
353 const int nMaxCycles =
354 std::max( std::ceil( static_cast<double>(pAE->m_fSongSizeInTicks) /
355 static_cast<double>(pPref->m_nBufferSize) *
356 static_cast<double>(pTransportPos->getTickSize()) * 4.0 ),
357 static_cast<double>(pAE->m_fSongSizeInTicks) );
358
359 while ( pTransportPos->getDoubleTick() <
360 pAE->getSongSizeInTicks() ) {
361 nFrames = frameDist( randomEngine );
363 QString( "[testTransportProcessingTimeline : song mode : all timeline]" ),
364 nFrames, &nLastLookahead, &nLastTransportFrame, &nTotalFrames,
365 &nLastQueuingTick, &fLastTickIntervalEnd, false );
366
367 nn++;
368 if ( nn > nMaxCycles ) {
370 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" )
371 .arg( pTransportPos->getFrame() )
372 .arg( pTransportPos->getDoubleTick(), 0, 'f' )
373 .arg( pTransportPos->getTickSize(), 0, 'f' )
374 .arg( pAE->getSongSizeInTicks(), 0, 'f' )
375 .arg( nMaxCycles ) );
376 }
377 }
378
379 // Alternate Timeline usage and timeline deactivation with
380 // "classical" bpm change".
381
382 pAE->reset( false );
383 pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
384 resetVariables();
385
386 float fBpm;
387 float fLastBpm = pTransportPos->getBpm();
388
389 const int nCyclesPerTempo = 11;
390 while ( pTransportPos->getDoubleTick() <
391 pAE->getSongSizeInTicks() ) {
392
393 QString sContext;
394 if ( nn % 2 == 0 ){
395 activateTimeline( false );
396 fBpm = tempoDist( randomEngine );
397 pAE->setNextBpm( fBpm );
398 pAE->updateBpmAndTickSize( pTransportPos );
399 pAE->updateBpmAndTickSize( pQueuingPos );
400
401 sContext = "no timeline";
402 }
403 else {
404 activateTimeline( true );
405 fBpm = AudioEngine::getBpmAtColumn( pTransportPos->getColumn() );
406
407 sContext = "timeline";
408 }
409
410 for ( int cc = 0; cc < nCyclesPerTempo; ++cc ) {
411 nFrames = frameDist( randomEngine );
413 QString( "testTransportProcessing : alternating timeline : bpm %1->%2 : %3" )
414 .arg( fLastBpm ).arg( fBpm ).arg( sContext ),
415 nFrames, &nLastLookahead, &nLastTransportFrame, &nTotalFrames,
416 &nLastQueuingTick, &fLastTickIntervalEnd, false );
417 }
418
419 fLastBpm = fBpm;
420
421 nn++;
422 if ( nn > nMaxCycles ) {
424 "[testTransportProcessingTimeline] [alternating timeline] end of the song wasn't reached in time." );
425 }
426 }
427
428 pAE->setState( AudioEngine::State::Ready );
429 pAE->unlock();
430}
431
433 auto pHydrogen = Hydrogen::get_instance();
434 auto pSong = pHydrogen->getSong();
435 auto pPref = Preferences::get_instance();
436 auto pCoreActionController = pHydrogen->getCoreActionController();
437 auto pAE = pHydrogen->getAudioEngine();
438 auto pTransportPos = pAE->getTransportPosition();
439
440 pCoreActionController->activateLoopMode( true );
441 pCoreActionController->activateSongMode( true );
442
443 pAE->lock( RIGHT_HERE );
444 pAE->setState( AudioEngine::State::Testing );
445
446 // For this call the AudioEngine still needs to be in state
447 // Playing or Ready.
448 pAE->reset( false );
449 pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
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 pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
662
663 // Check consistency of updated frames and ticks while relocating
664 // transport.
665 double fNewTick;
666 long long nNewFrame;
667
668 const int nProcessCycles = 100;
669 for ( int nn = 0; nn < nProcessCycles; ++nn ) {
670
671 if ( nn < nProcessCycles - 2 ) {
672 fNewTick = tickDist( randomEngine );
673 }
674 else if ( nn < nProcessCycles - 1 ) {
675 // Resulted in an unfortunate rounding error due to the
676 // song end at 2112.
677 fNewTick = 2111.928009209;
678 }
679 else {
680 // There was a rounding error at this particular tick.
681 fNewTick = 960;
682 }
683
684 pAE->locate( fNewTick, false );
685
687 pTransportPos, "[testTransportRelocation] mismatch tick-based" );
688
689 // Frame-based relocation
690 nNewFrame = frameDist( randomEngine );
691 pAE->locateToFrame( nNewFrame );
692
694 pTransportPos, "[testTransportRelocation] mismatch frame-based" );
695
696 }
697
698 pAE->reset( false );
699 pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
700 pAE->setState( AudioEngine::State::Ready );
701 pAE->unlock();
702}
703
705 auto pHydrogen = Hydrogen::get_instance();
706 auto pCoreActionController = pHydrogen->getCoreActionController();
707 auto pSong = pHydrogen->getSong();
708 auto pAE = pHydrogen->getAudioEngine();
709
710 const int nTestColumn = 4;
711
712 pAE->lock( RIGHT_HERE );
713 pAE->setState( AudioEngine::State::Testing );
714 pAE->reset( false );
715 pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
716 pAE->setState( AudioEngine::State::Ready );
717 pAE->unlock();
718
719 pCoreActionController->activateLoopMode( true );
720 pCoreActionController->locateToColumn( nTestColumn );
721
722 pAE->lock( RIGHT_HERE );
723 pAE->setState( AudioEngine::State::Testing );
724
725 AudioEngineTests::toggleAndCheckConsistency( 1, 1, "[testSongSizeChange] prior" );
726
727 // Toggle a grid cell after to the current transport position
728 AudioEngineTests::toggleAndCheckConsistency( 6, 6, "[testSongSizeChange] after" );
729
730 // Now we head to the "same" position inside the song but with the
731 // transport being looped once.
732 long nNextTick = pHydrogen->getTickForColumn( nTestColumn );
733 if ( nNextTick == -1 ) {
735 QString( "[testSongSizeChange] Bad test design: there is no column [%1]" )
736 .arg( nTestColumn ) );
737 }
738
739 nNextTick += pSong->lengthInTicks();
740
741 pAE->locate( nNextTick );
742
743 AudioEngineTests::toggleAndCheckConsistency( 1, 1, "[testSongSizeChange] looped:prior" );
744
745 // Toggle a grid cell after to the current transport position
746 AudioEngineTests::toggleAndCheckConsistency( 13, 6, "[testSongSizeChange] looped:after" );
747
748 pAE->setState( AudioEngine::State::Ready );
749 pAE->unlock();
750 pCoreActionController->activateLoopMode( false );
751}
752
754 auto pHydrogen = Hydrogen::get_instance();
755 auto pSong = pHydrogen->getSong();
756 auto pCoreActionController = pHydrogen->getCoreActionController();
757 auto pPref = Preferences::get_instance();
758 auto pAE = pHydrogen->getAudioEngine();
759 auto pTransportPos = pAE->getTransportPosition();
760
761 pCoreActionController->activateTimeline( false );
762 pCoreActionController->activateLoopMode( true );
763
764 pAE->lock( RIGHT_HERE );
765 pAE->setState( AudioEngine::State::Testing );
766
767 const int nColumns = pSong->getPatternGroupVector()->size();
768
769 std::random_device randomSeed;
770 std::default_random_engine randomEngine( randomSeed() );
771 std::uniform_real_distribution<double> tickDist( 1, pPref->m_nBufferSize );
772 std::uniform_int_distribution<int> columnDist( nColumns, nColumns + 100 );
773
774 // For this call the AudioEngine still needs to be in state
775 // Playing or Ready.
776 pAE->reset( false );
777 pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
778
779 const uint32_t nFrames = 500;
780 const double fInitialSongSize = pAE->m_fSongSizeInTicks;
781 int nNewColumn;
782 double fTick;
783
784 auto checkState = [&]( const QString& sContext, bool bSongSizeShouldChange ){
786 pTransportPos,
787 QString( "[testSongSizeChangeInLoopMode::checkState] [%1] before increment" )
788 .arg( sContext ) );
789
790 if ( bSongSizeShouldChange &&
791 fInitialSongSize == pAE->m_fSongSizeInTicks ) {
793 QString( "[testSongSizeChangeInLoopMode] [%1] song size stayed the same [%2->%3]")
794 .arg( sContext ).arg( fInitialSongSize ).arg( pAE->m_fSongSizeInTicks ) );
795 }
796 else if ( ! bSongSizeShouldChange &&
797 fInitialSongSize != pAE->m_fSongSizeInTicks ) {
799 QString( "[testSongSizeChangeInLoopMode] [%1] unexpected song enlargement [%2->%3]")
800 .arg( sContext ).arg( fInitialSongSize ).arg( pAE->m_fSongSizeInTicks ) );
801 }
802
803 pAE->incrementTransportPosition( nFrames );
804
806 pTransportPos,
807 QString( "[testSongSizeChangeInLoopMode::checkState] [%1] after increment" )
808 .arg( sContext ) );
809 };
810
811 const int nNumberOfTogglings = 5;
812 for ( int nn = 0; nn < nNumberOfTogglings; ++nn ) {
813
814 fTick = tickDist( randomEngine );
815 pAE->locate( fInitialSongSize + fTick );
816
817 checkState( QString( "relocation to [%1]" ).arg( fTick ), false );
818
819 nNewColumn = columnDist( randomEngine );
820
821 pAE->setState( AudioEngine::State::Ready );
822 pAE->unlock();
823 pCoreActionController->toggleGridCell( nNewColumn, 0 );
824 pAE->lock( RIGHT_HERE );
825 pAE->setState( AudioEngine::State::Testing );
826
827 checkState( QString( "toggling column [%1]" ).arg( nNewColumn ), true );
828
829 pAE->setState( AudioEngine::State::Ready );
830 pAE->unlock();
831 pCoreActionController->toggleGridCell( nNewColumn, 0 );
832 pAE->lock( RIGHT_HERE );
833 pAE->setState( AudioEngine::State::Testing );
834
835 checkState( QString( "again toggling column [%1]" ).arg( nNewColumn ), false );
836 }
837
838 pAE->setState( AudioEngine::State::Ready );
839 pAE->unlock();
840}
841
843 auto pHydrogen = Hydrogen::get_instance();
844 auto pSong = pHydrogen->getSong();
845 auto pCoreActionController = pHydrogen->getCoreActionController();
846 auto pPref = Preferences::get_instance();
847 auto pAE = pHydrogen->getAudioEngine();
848 auto pSampler = pAE->getSampler();
849 auto pTransportPos = pAE->getTransportPosition();
850 auto pQueuingPos = pAE->m_pQueuingPosition;
851
852 pCoreActionController->activateTimeline( false );
853 pCoreActionController->activateLoopMode( false );
854 pCoreActionController->activateSongMode( true );
855 pAE->lock( RIGHT_HERE );
856 pAE->setState( AudioEngine::State::Testing );
857
858 std::random_device randomSeed;
859 std::default_random_engine randomEngine( randomSeed() );
860 std::uniform_int_distribution<int> frameDist( pPref->m_nBufferSize / 2,
861 pPref->m_nBufferSize );
862
863 // For this call the AudioEngine still needs to be in state
864 // Playing or Ready.
865 pAE->reset( false );
866 pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
867
868 // Check consistency of updated frames and ticks while using a
869 // random buffer size (e.g. like PulseAudio does).
870
871 uint32_t nFrames;
872 int nn = 0;
873
874 int nMaxCycles =
875 std::max( std::ceil( static_cast<double>(pAE->m_fSongSizeInTicks) /
876 static_cast<double>(pPref->m_nBufferSize) *
877 static_cast<double>(pTransportPos->getTickSize()) * 4.0 ),
878 static_cast<double>(pAE->m_fSongSizeInTicks) );
879
880
881 // Ensure the sampler is clean.
882 AudioEngineTests::resetSampler( "testNoteEnqueuing : song mode" );
883
884 auto notesInSong = pSong->getAllNotes();
885 std::vector<std::shared_ptr<Note>> notesInSongQueue;
886 std::vector<std::shared_ptr<Note>> notesInSamplerQueue;
887
888 auto retrieveNotes = [&]( const QString& sContext ) {
889 // Add freshly enqueued notes.
890 AudioEngineTests::mergeQueues( &notesInSongQueue,
892 pAE->processAudio( nFrames );
893
894 AudioEngineTests::mergeQueues( &notesInSamplerQueue,
895 pSampler->getPlayingNotesQueue() );
896
897 pAE->incrementTransportPosition( nFrames );
898
899 ++nn;
900 if ( nn > nMaxCycles ) {
902 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" )
903 .arg( sContext ).arg( pTransportPos->getFrame() )
904 .arg( pTransportPos->getDoubleTick(), 0, 'f' )
905 .arg( pTransportPos->getTickSize(), 0, 'f' )
906 .arg( pAE->m_fSongSizeInTicks, 0, 'f' ).arg( nMaxCycles ) );
907 }
908 };
909
910 nn = 0;
911 int nRes;
912 while ( pQueuingPos->getDoubleTick() < pAE->m_fSongSizeInTicks ) {
913
914 nFrames = frameDist( randomEngine );
915 pAE->updateNoteQueue( nFrames );
916 retrieveNotes( "song mode" );
917 }
918
919 auto checkQueueConsistency = [&]( const QString& sContext ) {
920 if ( notesInSongQueue.size() !=
921 notesInSong.size() ) {
922 QString sMsg = QString( "[testNoteEnqueuing::checkQueueConsistency] [%1] Mismatch between notes count in Song [%2] and NoteQueue [%3]. Song:\n" )
923 .arg( sContext ).arg( notesInSong.size() )
924 .arg( notesInSongQueue.size() );
925 for ( int ii = 0; ii < notesInSong.size(); ++ii ) {
926 auto note = notesInSong[ ii ];
927 sMsg.append( QString( "\t[%1] instr: %2, position: %3, noteStart: %4, velocity: %5\n")
928 .arg( ii )
929 .arg( note->get_instrument()->get_name() )
930 .arg( note->get_position() )
931 .arg( note->getNoteStart() )
932 .arg( note->get_velocity() ) );
933 }
934 sMsg.append( "NoteQueue:\n" );
935 for ( int ii = 0; ii < notesInSongQueue.size(); ++ii ) {
936 auto note = notesInSongQueue[ ii ];
937 sMsg.append( QString( "\t[%1] instr: %2, position: %3, noteStart: %4, velocity: %5\n")
938 .arg( ii )
939 .arg( note->get_instrument()->get_name() )
940 .arg( note->get_position() )
941 .arg( note->getNoteStart() )
942 .arg( note->get_velocity() ) );
943 }
944
946 }
947
948 // We have to relax the test for larger buffer sizes. Else, the
949 // notes will be already fully processed in and flush from the
950 // Sampler before we had the chance to grab and compare them.
951 if ( notesInSamplerQueue.size() !=
952 notesInSong.size() &&
953 pPref->m_nBufferSize < 1024 ) {
954 QString sMsg = QString( "[testNoteEnqueuing::checkQueueConsistency] [%1] Mismatch between notes count in Song [%2] and Sampler [%3]. Song:\n" )
955 .arg( sContext ).arg( notesInSong.size() )
956 .arg( notesInSamplerQueue.size() );
957 for ( int ii = 0; ii < notesInSong.size(); ++ii ) {
958 auto note = notesInSong[ ii ];
959 sMsg.append( QString( "\t[%1] instr: %2, position: %3, noteStart: %4, velocity: %5\n")
960 .arg( ii )
961 .arg( note->get_instrument()->get_name() )
962 .arg( note->get_position() )
963 .arg( note->getNoteStart() )
964 .arg( note->get_velocity() ) );
965 }
966 sMsg.append( "SamplerQueue:\n" );
967 for ( int ii = 0; ii < notesInSamplerQueue.size(); ++ii ) {
968 auto note = notesInSamplerQueue[ ii ];
969 sMsg.append( QString( "\t[%1] instr: %2, position: %3, noteStart: %4, velocity: %5\n")
970 .arg( ii )
971 .arg( note->get_instrument()->get_name() )
972 .arg( note->get_position() )
973 .arg( note->getNoteStart() )
974 .arg( note->get_velocity() ) );
975 }
976
978 }
979 };
980 checkQueueConsistency( "song mode" );
981
982 pAE->setState( AudioEngine::State::Ready );
983 pAE->unlock();
984
986 // Perform the test in pattern mode
988
989 pCoreActionController->activateSongMode( false );
990 pHydrogen->setPatternMode( Song::PatternMode::Selected );
991 pHydrogen->setSelectedPatternNumber( 4 );
992
993 pAE->lock( RIGHT_HERE );
994 pAE->setState( AudioEngine::State::Testing );
995
996 AudioEngineTests::resetSampler( "testNoteEnqueuing : pattern mode" );
997
998 auto pPattern =
999 pSong->getPatternList()->get( pHydrogen->getSelectedPatternNumber() );
1000 if ( pPattern == nullptr ) {
1002 QString( "[testNoteEnqueuing] null pattern selected [%1]" )
1003 .arg( pHydrogen->getSelectedPatternNumber() ) );
1004 }
1005
1006 int nLoops = 5;
1007 notesInSong.clear();
1008 for ( int ii = 0; ii < nLoops; ++ii ) {
1009 FOREACH_NOTE_CST_IT_BEGIN_LENGTH( pPattern->get_notes(), it, pPattern ) {
1010 if ( it->second != nullptr ) {
1011 auto note = std::make_shared<Note>( it->second );
1012 note->set_position( note->get_position() +
1013 ii * pPattern->get_length() );
1014 notesInSong.push_back( note );
1015 }
1016 }
1017 }
1018
1019 notesInSongQueue.clear();
1020 notesInSamplerQueue.clear();
1021
1022 nMaxCycles =
1023 static_cast<int>(std::max( static_cast<float>(pPattern->get_length()) *
1024 static_cast<float>(nLoops) *
1025 pTransportPos->getTickSize() * 4 /
1026 static_cast<float>(pPref->m_nBufferSize),
1027 static_cast<float>(MAX_NOTES) *
1028 static_cast<float>(nLoops) ));
1029 nn = 0;
1030 while ( pQueuingPos->getDoubleTick() < pPattern->get_length() * nLoops ) {
1031
1032 nFrames = frameDist( randomEngine );
1033 pAE->updateNoteQueue( nFrames );
1034 retrieveNotes( "pattern mode" );
1035 }
1036
1037 // Transport in pattern mode is always looped. We have to pop the
1038 // notes added during the second run due to the lookahead.
1039 auto popSurplusNotes = [&]( std::vector<std::shared_ptr<Note>>* queue ) {
1040 const int nNoteNumber = queue->size();
1041 for ( int ii = 0; ii < nNoteNumber; ++ii ) {
1042 auto pNote = queue->at( nNoteNumber - 1 - ii );
1043 if ( pNote != nullptr &&
1044 pNote->get_position() >= pPattern->get_length() * nLoops ) {
1045 queue->pop_back();
1046 } else {
1047 break;
1048 }
1049 }
1050 };
1051 popSurplusNotes( &notesInSongQueue );
1052 popSurplusNotes( &notesInSamplerQueue );
1053
1054 checkQueueConsistency( "pattern mode" );
1055
1056 pAE->setState( AudioEngine::State::Ready );
1057 pAE->unlock();
1058
1060 // Perform the test in looped song mode
1062 // In case the transport is looped the first note was lost the
1063 // first time transport was wrapped to the beginning again. This
1064 // occurred just in song mode.
1065
1066 pCoreActionController->activateLoopMode( true );
1067 pCoreActionController->activateSongMode( true );
1068
1069 pAE->lock( RIGHT_HERE );
1070 pAE->setState( AudioEngine::State::Testing );
1071 pAE->reset( false );
1072 pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
1073
1074 nLoops = 1;
1075
1076 nMaxCycles =
1077 std::max( std::ceil( static_cast<double>(pAE->m_fSongSizeInTicks) /
1078 static_cast<double>(pPref->m_nBufferSize) *
1079 static_cast<double>(pTransportPos->getTickSize()) * 4.0 ),
1080 static_cast<double>(pAE->m_fSongSizeInTicks) ) *
1081 ( nLoops + 1 );
1082
1083 AudioEngineTests::resetSampler( "testNoteEnqueuing : loop mode" );
1084
1085 notesInSong.clear();
1086 for ( int ii = 0; ii <= nLoops; ++ii ) {
1087 auto notesVec = pSong->getAllNotes();
1088 for ( auto nnote : notesVec ) {
1089 nnote->set_position( nnote->get_position() +
1090 ii * pAE->m_fSongSizeInTicks );
1091 }
1092 notesInSong.insert( notesInSong.end(), notesVec.begin(), notesVec.end() );
1093 }
1094
1095 notesInSongQueue.clear();
1096 notesInSamplerQueue.clear();
1097
1098 nn = 0;
1099 while ( pQueuingPos->getDoubleTick() <
1100 pAE->m_fSongSizeInTicks * ( nLoops + 1 ) ) {
1101
1102 nFrames = frameDist( randomEngine );
1103
1104 // Turn off loop mode once we entered the last loop cycle.
1105 if ( ( pTransportPos->getDoubleTick() >
1106 pAE->m_fSongSizeInTicks * nLoops + 100 ) &&
1107 pSong->getLoopMode() == Song::LoopMode::Enabled ) {
1108 pAE->setState( AudioEngine::State::Ready );
1109 pAE->unlock();
1110 pCoreActionController->activateLoopMode( false );
1111 pAE->lock( RIGHT_HERE );
1112 pAE->setState( AudioEngine::State::Testing );
1113 }
1114
1115 pAE->updateNoteQueue( nFrames );
1116 retrieveNotes( "looped song mode" );
1117 }
1118
1119 checkQueueConsistency( "looped song mode" );
1120
1121 pAE->setState( AudioEngine::State::Ready );
1122 pAE->unlock();
1123}
1124
1126 auto pHydrogen = Hydrogen::get_instance();
1127 auto pSong = pHydrogen->getSong();
1128 auto pAE = pHydrogen->getAudioEngine();
1129 auto pSampler = pAE->getSampler();
1130 auto pTransportPos = pAE->getTransportPosition();
1131 auto pPref = Preferences::get_instance();
1132
1133 pAE->lock( RIGHT_HERE );
1134 pAE->setState( AudioEngine::State::Testing );
1135
1136 std::random_device randomSeed;
1137 std::default_random_engine randomEngine( randomSeed() );
1138 std::uniform_int_distribution<int> frameDist( pPref->m_nBufferSize / 2,
1139 pPref->m_nBufferSize );
1140
1141 // For reset() the AudioEngine still needs to be in state
1142 // Playing or Ready.
1143 pAE->reset( false );
1144 pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
1145 AudioEngineTests::resetSampler( __PRETTY_FUNCTION__ );
1146
1147 uint32_t nFrames;
1148
1149 const int nMaxCycles =
1150 std::max( std::ceil( static_cast<double>(pAE->m_fSongSizeInTicks) /
1151 static_cast<double>(pPref->m_nBufferSize) *
1152 static_cast<double>(pTransportPos->getTickSize()) * 4.0 ),
1153 static_cast<double>(pAE->m_fSongSizeInTicks) );
1154
1155 auto notesInSong = pSong->getAllNotes();
1156 std::vector<std::shared_ptr<Note>> notesInSongQueue;
1157 std::vector<std::shared_ptr<Note>> notesInSamplerQueue;
1158
1159 int nn = 0;
1160 while ( pTransportPos->getDoubleTick() <
1161 pAE->m_fSongSizeInTicks ) {
1162
1163 nFrames = frameDist( randomEngine );
1164
1165 pAE->updateNoteQueue( nFrames );
1166
1167 // Add freshly enqueued notes.
1168 AudioEngineTests::mergeQueues( &notesInSongQueue,
1170
1171 pAE->processAudio( nFrames );
1172
1173 AudioEngineTests::mergeQueues( &notesInSamplerQueue,
1174 pSampler->getPlayingNotesQueue() );
1175
1176 pAE->incrementTransportPosition( nFrames );
1177
1178 ++nn;
1179 if ( nn > nMaxCycles ) {
1181 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" )
1182 .arg( pTransportPos->getFrame() )
1183 .arg( pTransportPos->getDoubleTick(), 0, 'f' )
1184 .arg( pTransportPos->getTickSize(), 0, 'f' )
1185 .arg( pAE->m_fSongSizeInTicks, 0, 'f' )
1186 .arg( nMaxCycles ) );
1187 }
1188 }
1189
1190 if ( notesInSongQueue.size() != notesInSong.size() ) {
1192 QString( "Mismatching number of notes in song [%1] and note queue [%2]" )
1193 .arg( notesInSong.size() )
1194 .arg( notesInSongQueue.size() ) );
1195 }
1196
1197 if ( notesInSamplerQueue.size() != notesInSong.size() ) {
1199 QString( "Mismatching number of notes in song [%1] and sampler queue [%2]" )
1200 .arg( notesInSong.size() )
1201 .arg( notesInSamplerQueue.size() ) );
1202 }
1203
1204 // Ensure the ordering of the notes is identical
1205 for ( int ii = 0; ii < notesInSong.size(); ++ii ) {
1206 if ( ! notesInSong[ ii ]->match( notesInSongQueue[ ii ] ) ) {
1208 QString( "Mismatch at note [%1] between song [%2] and song queue [%3]" )
1209 .arg( ii )
1210 .arg( notesInSong[ ii ]->toQString() )
1211 .arg( notesInSongQueue[ ii ]->toQString() ) );
1212 }
1213 if ( ! notesInSong[ ii ]->match( notesInSamplerQueue[ ii ] ) ) {
1215 QString( "Mismatch at note [%1] between song [%2] and sampler queue [%3]" )
1216 .arg( ii )
1217 .arg( notesInSong[ ii ]->toQString() )
1218 .arg( notesInSamplerQueue[ ii ]->toQString() ) );
1219 }
1220 }
1221
1222 pAE->setState( AudioEngine::State::Ready );
1223 pAE->unlock();
1224}
1225
1227 auto pHydrogen = Hydrogen::get_instance();
1228 auto pSong = pHydrogen->getSong();
1229 auto pAE = pHydrogen->getAudioEngine();
1230 auto pSampler = pAE->getSampler();
1231 auto pTransportPos = pAE->getTransportPosition();
1232 auto pPref = Preferences::get_instance();
1233 auto pCoreActionController = pHydrogen->getCoreActionController();
1234
1235 pCoreActionController->activateLoopMode( false );
1236 pCoreActionController->activateSongMode( true );
1237
1238 pAE->lock( RIGHT_HERE );
1239 pAE->setState( AudioEngine::State::Testing );
1240
1241 // For reset() the AudioEngine still needs to be in state
1242 // Playing or Ready.
1243 pAE->reset( false );
1244 pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
1245
1246 // Rolls playback from beginning to the end of the song and
1247 // captures all notes added to the Sampler.
1248 auto getNotes = [&]( std::vector<std::shared_ptr<Note>> *notes ) {
1249
1250 AudioEngineTests::resetSampler( "testHumanization::getNotes" );
1251
1252 // Factor by which the number of frames processed when
1253 // retrieving notes will be smaller than the buffer size. This
1254 // vital because when using a large number of frames below the
1255 // notes might already be processed and flushed from the
1256 // Sampler before we had the chance to retrieve them.
1257 const double fStep = 10.0;
1258 const int nMaxCycles =
1259 std::max( std::ceil( static_cast<double>(pAE->m_fSongSizeInTicks) /
1260 static_cast<double>(pPref->m_nBufferSize) * fStep *
1261 static_cast<double>(pTransportPos->getTickSize()) * 4.0 ),
1262 static_cast<double>(pAE->m_fSongSizeInTicks) );
1263 const uint32_t nFrames = static_cast<uint32_t>(
1264 std::round( static_cast<double>(pPref->m_nBufferSize) / fStep ) );
1265
1266 pAE->locate( 0 );
1267
1268 QString sPlayingPatterns;
1269 for ( const auto& pattern : *pTransportPos->getPlayingPatterns() ) {
1270 sPlayingPatterns += " " + pattern->get_name();
1271 }
1272
1273 int nn = 0;
1274 while ( pTransportPos->getDoubleTick() < pAE->m_fSongSizeInTicks ||
1275 pAE->getEnqueuedNotesNumber() > 0 ) {
1276
1277 pAE->updateNoteQueue( nFrames );
1278
1279 pAE->processAudio( nFrames );
1280
1282 pSampler->getPlayingNotesQueue() );
1283
1284 pAE->incrementTransportPosition( nFrames );
1285
1286 ++nn;
1287 if ( nn > nMaxCycles ) {
1289 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" )
1290 .arg( pTransportPos->getFrame() )
1291 .arg( pTransportPos->getDoubleTick(), 0, 'f' )
1292 .arg( pTransportPos->getTickSize(), 0, 'f' )
1293 .arg( pAE->m_fSongSizeInTicks, 0, 'f' )
1294 .arg( nMaxCycles ) );
1295 }
1296 }
1297 };
1298
1299 // Sets the rotaries of velocity and timinig humanization as well
1300 // as of the pitch randomization of the Kick Instrument (used for
1301 // the notes in the test song) to a particular value between 0 and
1302 // 1.
1303 auto setHumanization = [&]( double fValue ) {
1304 fValue = std::clamp( fValue, 0.0, 1.0 );
1305
1306 pSong->setHumanizeTimeValue( fValue );
1307 pSong->setHumanizeVelocityValue( fValue );
1308
1309 pSong->getInstrumentList()->get( 0 )->set_random_pitch_factor( fValue );
1310 };
1311
1312 auto setSwing = [&]( double fValue ) {
1313 fValue = std::clamp( fValue, 0.0, 1.0 );
1314
1315 pSong->setSwingFactor( fValue );
1316 };
1317
1318 // Reference notes with no humanization and property
1319 // customization.
1320 auto notesInSong = pSong->getAllNotes();
1321
1322 // First pattern is activated per default.
1323 setHumanization( 0 );
1324 setSwing( 0 );
1325
1326 std::vector<std::shared_ptr<Note>> notesReference;
1327 getNotes( &notesReference );
1328
1329 if ( notesReference.size() != notesInSong.size() ) {
1331 QString( "[testHumanization] [references] Bad test setup. Mismatching number of notes [%1 : %2]" )
1332 .arg( notesReference.size() )
1333 .arg( notesInSong.size() ) );
1334 }
1335
1337 // Pattern 2 contains notes of the same instrument at the same
1338 // positions but velocity, pan, leag&lag, and note key as well as
1339 // note octave are all customized. Check whether these
1340 // customizations reach the Sampler.
1341 pAE->setState( AudioEngine::State::Ready );
1342 pAE->unlock();
1343 pCoreActionController->toggleGridCell( 0, 0 );
1344 pCoreActionController->toggleGridCell( 0, 1 );
1345 pAE->lock( RIGHT_HERE );
1346 pAE->setState( AudioEngine::State::Testing );
1347
1348 std::vector<std::shared_ptr<Note>> notesCustomized;
1349 getNotes( &notesCustomized );
1350
1351 if ( notesReference.size() != notesCustomized.size() ) {
1353 QString( "[testHumanization] [customization] Mismatching number of notes [%1 : %2]" )
1354 .arg( notesReference.size() )
1355 .arg( notesCustomized.size() ) );
1356 }
1357
1358 for ( int ii = 0; ii < notesReference.size(); ++ii ) {
1359 auto pNoteReference = notesReference[ ii ];
1360 auto pNoteCustomized = notesCustomized[ ii ];
1361
1362 if ( pNoteReference != nullptr && pNoteCustomized != nullptr ) {
1363 if ( pNoteReference->get_velocity() ==
1364 pNoteCustomized->get_velocity() ) {
1366 QString( "[testHumanization] [customization] Velocity of note [%1] was not altered" )
1367 .arg( ii ) );
1368 } else if ( pNoteReference->get_lead_lag() ==
1369 pNoteCustomized->get_lead_lag() ) {
1371 QString( "[testHumanization] [customization] Lead Lag of note [%1] was not altered" )
1372 .arg( ii ) );
1373 } else if ( pNoteReference->getNoteStart() ==
1374 pNoteCustomized->getNoteStart() ) {
1375 // The note start incorporates the lead & lag
1376 // information and is the property used by the
1377 // Sampler.
1379 QString( "[testHumanization] [customization] Note start of note [%1] was not altered" )
1380 .arg( ii ) );
1381 } else if ( pNoteReference->getPan() ==
1382 pNoteCustomized->getPan() ) {
1384 QString( "[testHumanization] [customization] Pan of note [%1] was not altered" )
1385 .arg( ii ) );
1386 } else if ( pNoteReference->get_total_pitch() ==
1387 pNoteCustomized->get_total_pitch() ) {
1389 QString( "[testHumanization] [customization] Total Pitch of note [%1] was not altered" )
1390 .arg( ii ) );
1391 }
1392 } else {
1394 QString( "[testHumanization] [customization] Unable to access note [%1]" )
1395 .arg( ii ) );
1396 }
1397
1398 }
1399
1401 // Check whether deviations of the humanized/randomized properties
1402 // are indeed distributed as a Gaussian with mean zero and an
1403 // expected standard deviation.
1404 //
1405 // Switch back to pattern 1
1406 pAE->setState( AudioEngine::State::Ready );
1407 pAE->unlock();
1408 pCoreActionController->toggleGridCell( 0, 1 );
1409 pCoreActionController->toggleGridCell( 0, 0 );
1410 pAE->lock( RIGHT_HERE );
1411 pAE->setState( AudioEngine::State::Testing );
1412
1413 auto checkHumanization = [&]( double fValue, std::vector<std::shared_ptr<Note>>* pNotes ) {
1414
1415 if ( notesReference.size() != pNotes->size() ) {
1417 QString( "[testHumanization] [humanization] Mismatching number of notes [%1 : %2]" )
1418 .arg( notesReference.size() )
1419 .arg( pNotes->size() ) );
1420 }
1421
1422 auto checkDeviation = []( std::vector<float>* pDeviations, float fTargetSD, const QString& sContext ) {
1423
1424 float fMean = std::accumulate( pDeviations->begin(), pDeviations->end(),
1425 0.0, std::plus<float>() ) /
1426 static_cast<float>( pDeviations->size() );
1427
1428 // Standard deviation
1429 auto compVariance = [&]( float fValue, float fElement ) {
1430 return fValue + ( fElement - fMean ) * ( fElement - fMean );
1431 };
1432 float fSD = std::sqrt( std::accumulate( pDeviations->begin(),
1433 pDeviations->end(),
1434 0.0, compVariance ) /
1435 static_cast<float>( pDeviations->size() ) );
1436
1437 // As we look at random numbers, the observed mean and
1438 // standard deviation won't match. But there should be no
1439 // more than 50% difference or something when wrong.
1440 if ( std::abs( fMean ) > std::abs( fSD ) * 0.5 ) {
1442 QString( "[testHumanization] [%1] Mismatching mean [%2] != [0] with std. deviation [%3]" )
1443 .arg( sContext ).arg( fMean, 0, 'E', -1 )
1444 .arg( fSD, 0, 'E', -1 ) );
1445 }
1446 if ( std::abs( fSD - fTargetSD ) > fTargetSD * 0.5 ) {
1448 QString( "[testHumanization] [%1] Mismatching standard deviation [%2] != [%3], diff [%4]" )
1449 .arg( sContext ).arg( fSD, 0, 'E', -1 )
1450 .arg( fTargetSD, 0, 'E', -1 )
1451 .arg( fSD - fTargetSD, 0, 'E', -1 ) );
1452 }
1453
1454 };
1455
1456 std::vector<float> deviationsPitch( notesReference.size() );
1457 std::vector<float> deviationsVelocity( notesReference.size() );
1458 std::vector<float> deviationsTiming( notesReference.size() );
1459
1460 for ( int ii = 0; ii < pNotes->size(); ++ii ) {
1461 auto pNoteReference = notesReference[ ii ];
1462 auto pNoteHumanized = pNotes->at( ii );
1463
1464 if ( pNoteReference != nullptr && pNoteHumanized != nullptr ) {
1465 deviationsVelocity[ ii ] =
1466 pNoteReference->get_velocity() - pNoteHumanized->get_velocity();
1467 deviationsPitch[ ii ] =
1468 pNoteReference->get_pitch() - pNoteHumanized->get_pitch();
1469 deviationsTiming[ ii ] =
1470 pNoteReference->getNoteStart() - pNoteHumanized->getNoteStart();
1471 } else {
1473 QString( "[testHumanization] [swing] Unable to access note [%1]" )
1474 .arg( ii ) );
1475 }
1476 }
1477
1478 // Within the audio engine every property has its own factor
1479 // multiplied with the humanization value set via the
1480 // GUI. With the latter ranging from 0 to 1 the factor
1481 // represent the maximum standard deviation available.
1482 checkDeviation( &deviationsVelocity,
1483 AudioEngine::fHumanizeVelocitySD * fValue, "velocity" );
1484 checkDeviation( &deviationsTiming,
1486 AudioEngine::nMaxTimeHumanize * fValue, "timing" );
1487 checkDeviation( &deviationsPitch,
1488 AudioEngine::fHumanizePitchSD * fValue, "pitch" );
1489 };
1490
1491 setHumanization( 0.2 );
1492 std::vector<std::shared_ptr<Note>> notesHumanizedWeak;
1493 getNotes( &notesHumanizedWeak );
1494 checkHumanization( 0.2, &notesHumanizedWeak );
1495
1496 setHumanization( 0.8 );
1497 std::vector<std::shared_ptr<Note>> notesHumanizedStrong;
1498 getNotes( &notesHumanizedStrong );
1499 checkHumanization( 0.8, &notesHumanizedStrong );
1500
1502 // Check whether swing works.
1503 //
1504 // There is still discussion about HOW the swing should work and
1505 // whether the current implementation is valid. Therefore, this
1506 // test will only check whether setting this option alters at
1507 // least one note position
1508
1509 setHumanization( 0 );
1510 setSwing( 0.5 );
1511 std::vector<std::shared_ptr<Note>> notesSwing;
1512 getNotes( &notesSwing );
1513
1514 if ( notesReference.size() != notesSwing.size() ) {
1516 QString( "[testHumanization] [swing] Mismatching number of notes [%1 : %2]" )
1517 .arg( notesReference.size() )
1518 .arg( notesSwing.size() ) );
1519 }
1520
1521 bool bNoteAltered = false;
1522 for ( int ii = 0; ii < notesReference.size(); ++ii ) {
1523 auto pNoteReference = notesReference[ ii ];
1524 auto pNoteSwing = notesSwing[ ii ];
1525
1526 if ( pNoteReference != nullptr && pNoteSwing != nullptr ) {
1527 if ( pNoteReference->getNoteStart() !=
1528 pNoteSwing->getNoteStart() ) {
1529 bNoteAltered = true;
1530 }
1531 } else {
1533 QString( "[testHumanization] [swing] Unable to access note [%1]" )
1534 .arg( ii ) );
1535 }
1536 }
1537 if ( ! bNoteAltered ) {
1538 AudioEngineTests::throwException( "[testHumanization] [swing] No notes affected." );
1539 }
1540
1542
1543 pAE->setState( AudioEngine::State::Ready );
1544 pAE->unlock();
1545}
1546
1547void AudioEngineTests::mergeQueues( std::vector<std::shared_ptr<Note>>* noteList, std::vector<std::shared_ptr<Note>> newNotes ) {
1548 bool bNoteFound;
1549 for ( const auto& newNote : newNotes ) {
1550 bNoteFound = false;
1551 // Check whether the notes is already present.
1552 for ( const auto& presentNote : *noteList ) {
1553 if ( newNote != nullptr && presentNote != nullptr ) {
1554 if ( newNote->match( presentNote.get() ) &&
1555 newNote->get_position() == presentNote->get_position() &&
1556 newNote->get_velocity() == presentNote->get_velocity() ) {
1557 bNoteFound = true;
1558 }
1559 }
1560 }
1561
1562 if ( ! bNoteFound ) {
1563 noteList->push_back( std::make_shared<Note>(newNote.get()) );
1564 }
1565 }
1566}
1567
1568// Used for the Sampler note queue
1569void AudioEngineTests::mergeQueues( std::vector<std::shared_ptr<Note>>* noteList, std::vector<Note*> newNotes ) {
1570 bool bNoteFound;
1571 for ( const auto& newNote : newNotes ) {
1572 bNoteFound = false;
1573 // Check whether the notes is already present.
1574 for ( const auto& presentNote : *noteList ) {
1575 if ( newNote != nullptr && presentNote != nullptr ) {
1576 if ( newNote->match( presentNote.get() ) &&
1577 newNote->get_position() == presentNote->get_position() &&
1578 newNote->get_velocity() == presentNote->get_velocity() ) {
1579 bNoteFound = true;
1580 }
1581 }
1582 }
1583
1584 if ( ! bNoteFound ) {
1585 noteList->push_back( std::make_shared<Note>(newNote) );
1586 }
1587 }
1588}
1589
1590void AudioEngineTests::checkTransportPosition( std::shared_ptr<TransportPosition> pPos, const QString& sContext ) {
1591
1592 auto pHydrogen = Hydrogen::get_instance();
1593 auto pSong = pHydrogen->getSong();
1594 auto pAE = pHydrogen->getAudioEngine();
1595
1596 double fCheckTickMismatch;
1597 const long long nCheckFrame =
1599 pPos->getDoubleTick(), &fCheckTickMismatch );
1600 const double fCheckTick =
1601 TransportPosition::computeTickFromFrame( pPos->getFrame() );
1602
1603 if ( abs( fCheckTick + fCheckTickMismatch - pPos->getDoubleTick() ) > 1e-9 ||
1604 abs( fCheckTickMismatch - pPos->m_fTickMismatch ) > 1e-9 ||
1605 nCheckFrame != pPos->getFrame() ) {
1607 QString( "[checkTransportPosition] [%8] [tick or frame mismatch]. original position: [%1], nCheckFrame: %2, fCheckTick: %3, fCheckTickMismatch: %4, fCheckTick + fCheckTickMismatch - pPos->getDoubleTick(): %5, fCheckTickMismatch - pPos->m_fTickMismatch: %6, nCheckFrame - pPos->getFrame(): %7" )
1608 .arg( pPos->toQString( "", true ) ).arg( nCheckFrame )
1609 .arg( fCheckTick, 0 , 'f', 9 ).arg( fCheckTickMismatch, 0 , 'f', 9 )
1610 .arg( fCheckTick + fCheckTickMismatch - pPos->getDoubleTick(), 0, 'E' )
1611 .arg( fCheckTickMismatch - pPos->m_fTickMismatch, 0, 'E' )
1612 .arg( nCheckFrame - pPos->getFrame() ).arg( sContext ) );
1613 }
1614
1615 long nCheckPatternStartTick;
1616 const int nCheckColumn = pHydrogen->getColumnForTick(
1617 pPos->getTick(), pSong->isLoopEnabled(), &nCheckPatternStartTick );
1618 const long nTicksSinceSongStart = static_cast<long>(std::floor(
1619 std::fmod( pPos->getDoubleTick(), pAE->m_fSongSizeInTicks ) ));
1620
1621 if ( pHydrogen->getMode() == Song::Mode::Song && pPos->getColumn() != -1 &&
1622 ( nCheckColumn != pPos->getColumn() ||
1623 ( nCheckPatternStartTick != pPos->getPatternStartTick() ) ||
1624 ( nTicksSinceSongStart - nCheckPatternStartTick !=
1625 pPos->getPatternTickPosition() ) ) ) {
1627 QString( "[checkTransportPosition] [%7] [column or pattern tick mismatch]. current position: [%1], nCheckColumn: %2, nCheckPatternStartTick: %3, nCheckPatternTickPosition: %4, nTicksSinceSongStart: %5, pAE->m_fSongSizeInTicks: %6" )
1628 .arg( pPos->toQString( "", true ) ).arg( nCheckColumn )
1629 .arg( nCheckPatternStartTick )
1630 .arg( nTicksSinceSongStart - nCheckPatternStartTick )
1631 .arg( nTicksSinceSongStart ).arg( pAE->m_fSongSizeInTicks, 0, 'f' )
1632 .arg( sContext ) );
1633 }
1634}
1635
1636void AudioEngineTests::checkAudioConsistency( const std::vector<std::shared_ptr<Note>> oldNotes,
1637 const std::vector<std::shared_ptr<Note>> newNotes,
1638 const QString& sContext,
1639 int nPassedFrames, bool bTestAudio,
1640 float fPassedTicks ) {
1641 auto pHydrogen = Hydrogen::get_instance();
1642 auto pSong = pHydrogen->getSong();
1643 auto pAE = pHydrogen->getAudioEngine();
1644 auto pTransportPos = pAE->getTransportPosition();
1645
1646 double fPassedFrames = static_cast<double>(nPassedFrames);
1647 const int nSampleRate = pHydrogen->getAudioOutput()->getSampleRate();
1648
1649 int nNotesFound = 0;
1650 for ( const auto& ppNewNote : newNotes ) {
1651 for ( const auto& ppOldNote : oldNotes ) {
1652 if ( ppNewNote->match( ppOldNote.get() ) &&
1653 ppNewNote->get_humanize_delay() ==
1654 ppOldNote->get_humanize_delay() &&
1655 ppNewNote->get_velocity() == ppOldNote->get_velocity() ) {
1656 ++nNotesFound;
1657
1658 if ( bTestAudio ) {
1659 // Check for consistency in the Sample position
1660 // advanced by the Sampler upon rendering.
1661 for ( int nn = 0; nn < ppNewNote->get_instrument()->get_components()->size(); nn++ ) {
1662 auto pSelectedLayer = ppOldNote->get_layer_selected( nn );
1663
1664 // The frames passed during the audio
1665 // processing depends on the sample rate of
1666 // the driver and sample and has to be
1667 // adjusted in here. This is equivalent to the
1668 // question whether Sampler::renderNote() or
1669 // Sampler::renderNoteResample() was used.
1670 if ( ppOldNote->getSample( nn )->get_sample_rate() !=
1671 nSampleRate ||
1672 ppOldNote->get_total_pitch() != 0.0 ) {
1673 // In here we assume the layer pitch is zero.
1674 fPassedFrames = static_cast<double>(nPassedFrames) *
1675 Note::pitchToFrequency( ppOldNote->get_total_pitch() ) *
1676 static_cast<float>(ppOldNote->getSample( nn )->get_sample_rate()) /
1677 static_cast<float>(nSampleRate);
1678 }
1679
1680 const int nSampleFrames =
1681 ppNewNote->get_instrument()->get_component( nn )
1682 ->get_layer( pSelectedLayer->nSelectedLayer )
1683 ->get_sample()->get_frames();
1684 const double fExpectedFrames =
1685 std::min( static_cast<double>(pSelectedLayer->fSamplePosition) +
1686 fPassedFrames,
1687 static_cast<double>(nSampleFrames) );
1688 if ( std::abs( ppNewNote->get_layer_selected( nn )->fSamplePosition -
1689 fExpectedFrames ) > 1 ) {
1691 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" )
1692 .arg( ppOldNote->toQString( "", true ) )
1693 .arg( ppNewNote->toQString( "", true ) )
1694 .arg( fPassedFrames, 0, 'f' ).arg( sContext )
1695 .arg( nSampleFrames ).arg( fExpectedFrames, 0, 'f' )
1696 .arg( ppOldNote->getSample( nn )->get_sample_rate() )
1697 .arg( nSampleRate )
1698 .arg( ppNewNote->get_layer_selected( nn )->fSamplePosition -
1699 fExpectedFrames, 0, 'g', 30 ) );
1700 }
1701 }
1702 }
1703 else { // if ( bTestAudio )
1704 // Check whether changes in note start position
1705 // were properly applied in the note queue of the
1706 // audio engine.
1707 if ( ppNewNote->get_position() - fPassedTicks !=
1708 ppOldNote->get_position() ) {
1710 QString( "[checkAudioConsistency] [%5] glitch in note queue.\n\tPre: %1\n\tPost: %2\n\tfPassedTicks: %3, diff (new - passed - old): %4" )
1711 .arg( ppOldNote->toQString( "", true ) )
1712 .arg( ppNewNote->toQString( "", true ) )
1713 .arg( fPassedTicks )
1714 .arg( ppNewNote->get_position() - fPassedTicks -
1715 ppOldNote->get_position() ).arg( sContext ) );
1716 }
1717 }
1718 }
1719 }
1720 }
1721
1722 // If one of the note vectors is empty - especially the new notes
1723 // - we can not test anything. But such things might happen as we
1724 // try various sample sizes and all notes might be already played
1725 // back and flushed.
1726 if ( nNotesFound == 0 &&
1727 oldNotes.size() > 0 &&
1728 newNotes.size() > 0 ) {
1729 QString sMsg = QString( "[checkAudioConsistency] [%1] bad test design. No notes played back." )
1730 .arg( sContext );
1731 sMsg.append( "\nold notes:" );
1732 for ( auto const& nnote : oldNotes ) {
1733 sMsg.append( "\n" + nnote->toQString( " ", true ) );
1734 }
1735 sMsg.append( "\nnew notes:" );
1736 for ( auto const& nnote : newNotes ) {
1737 sMsg.append( "\n" + nnote->toQString( " ", true ) );
1738 }
1739 sMsg.append( QString( "\n\npTransportPos->getDoubleTick(): %1, pTransportPos->getFrame(): %2, nPassedFrames: %3, fPassedTicks: %4, pTransportPos->getTickSize(): %5" )
1740 .arg( pTransportPos->getDoubleTick(), 0, 'f' )
1741 .arg( pTransportPos->getFrame() )
1742 .arg( nPassedFrames )
1743 .arg( fPassedTicks, 0, 'f' )
1744 .arg( pTransportPos->getTickSize(), 0, 'f' ) );
1745 sMsg.append( "\n\n notes in song:" );
1746 for ( auto const& nnote : pSong->getAllNotes() ) {
1747 sMsg.append( "\n" + nnote->toQString( " ", true ) );
1748 }
1750 }
1751}
1752
1753std::vector<std::shared_ptr<Note>> AudioEngineTests::copySongNoteQueue() {
1754 auto pAE = Hydrogen::get_instance()->getAudioEngine();
1755 std::vector<Note*> rawNotes;
1756 std::vector<std::shared_ptr<Note>> notes;
1757 for ( ; ! pAE->m_songNoteQueue.empty(); pAE->m_songNoteQueue.pop() ) {
1758 rawNotes.push_back( pAE->m_songNoteQueue.top() );
1759 notes.push_back( std::make_shared<Note>( pAE->m_songNoteQueue.top() ) );
1760 }
1761
1762 for ( auto nnote : rawNotes ) {
1763 pAE->m_songNoteQueue.push( nnote );
1764 }
1765
1766 return notes;
1767}
1768
1769void AudioEngineTests::toggleAndCheckConsistency( int nToggleColumn, int nToggleRow, const QString& sContext ) {
1770 auto pHydrogen = Hydrogen::get_instance();
1771 auto pCoreActionController = pHydrogen->getCoreActionController();
1772 auto pSong = pHydrogen->getSong();
1773 auto pAE = pHydrogen->getAudioEngine();
1774 auto pSampler = pAE->getSampler();
1775 auto pTransportPos = pAE->getTransportPosition();
1776
1777 const unsigned long nBufferSize = pHydrogen->getAudioOutput()->getBufferSize();
1778
1779 pAE->updateNoteQueue( nBufferSize );
1780 pAE->processAudio( nBufferSize );
1781 pAE->incrementTransportPosition( nBufferSize );
1782
1783 // Cache some stuff in order to compare it later on.
1784 long nOldSongSize;
1785 int nOldColumn;
1786 float fPrevTempo, fPrevTickSize;
1787 double fPrevTickStart, fPrevTickEnd;
1788 long long nPrevLeadLag;
1789
1790 std::vector<std::shared_ptr<Note>> notesSamplerPreToggle,
1791 notesSamplerPostToggle, notesSamplerPostRolling;
1792
1793 auto notesSongQueuePreToggle = AudioEngineTests::copySongNoteQueue();
1794
1795 auto toggleAndCheck = [&]( const QString& sContext ) {
1796 notesSamplerPreToggle.clear();
1797 for ( const auto& ppNote : pSampler->getPlayingNotesQueue() ) {
1798 notesSamplerPreToggle.push_back( std::make_shared<Note>( ppNote ) );
1799 }
1800
1801 nPrevLeadLag = pAE->computeTickInterval( &fPrevTickStart, &fPrevTickEnd,
1802 nBufferSize );
1803 nOldSongSize = pSong->lengthInTicks();
1804 nOldColumn = pTransportPos->getColumn();
1805 fPrevTempo = pTransportPos->getBpm();
1806 fPrevTickSize = pTransportPos->getTickSize();
1807
1808 pAE->setState( AudioEngine::State::Ready );
1809 pAE->unlock();
1810 pCoreActionController->toggleGridCell( nToggleColumn, nToggleRow );
1811 pAE->lock( RIGHT_HERE );
1812 pAE->setState( AudioEngine::State::Testing );
1813
1814 const QString sNewContext =
1815 QString( "toggleAndCheckConsistency::toggleAndCheck : %1 : toggling (%2,%3)" )
1816 .arg( sContext ).arg( nToggleColumn ).arg( nToggleRow );
1817
1818 // Check whether there was a change in song size
1819 const long nNewSongSize = pSong->lengthInTicks();
1820 if ( nNewSongSize == nOldSongSize ) {
1822 QString( "[%1] no change in song size" ).arg( sNewContext ) );
1823 }
1824
1825 // Check whether current frame and tick information are still
1826 // consistent.
1827 AudioEngineTests::checkTransportPosition( pTransportPos, sNewContext );
1828
1829 // m_songNoteQueue have been updated properly.
1830 const auto notesSongQueuePostToggle = AudioEngineTests::copySongNoteQueue();
1832 notesSongQueuePreToggle, notesSongQueuePostToggle,
1833 sNewContext + " : song queue", 0, false,
1834 pTransportPos->getTickOffsetSongSize() );
1835
1836 // The post toggle state will become the pre toggle state during
1837 // the next toggling.
1838 notesSongQueuePreToggle = notesSongQueuePostToggle;
1839
1840 notesSamplerPostToggle.clear();
1841 for ( const auto& ppNote : pSampler->getPlayingNotesQueue() ) {
1842 notesSamplerPostToggle.push_back( std::make_shared<Note>( ppNote ) );
1843 }
1845 notesSamplerPreToggle, notesSamplerPostToggle,
1846 sNewContext + " : sampler queue", 0, true,
1847 pTransportPos->getTickOffsetSongSize() );
1848
1849 // Column must be consistent. Unless the song length shrunk due to
1850 // the toggling and the previous column was located beyond the new
1851 // end (in which case transport will be reset to 0).
1852 if ( nOldColumn < pSong->getPatternGroupVector()->size() ) {
1853 // Transport was not reset to 0 - happens in most cases.
1854
1855 if ( nOldColumn != pTransportPos->getColumn() &&
1856 nOldColumn < pSong->getPatternGroupVector()->size() ) {
1858 QString( "[%3] Column changed old: %1, new: %2" )
1859 .arg( nOldColumn ).arg( pTransportPos->getColumn() )
1860 .arg( sNewContext ) );
1861 }
1862
1863 double fTickEnd, fTickStart;
1864 const long long nLeadLag =
1865 pAE->computeTickInterval( &fTickStart, &fTickEnd, nBufferSize );
1866 if ( std::abs( nLeadLag - nPrevLeadLag ) > 1 ) {
1868 QString( "[%3] LeadLag should be constant since there should be change in tick size. old: %1, new: %2" )
1869 .arg( nPrevLeadLag ).arg( nLeadLag ).arg( sNewContext ) );
1870 }
1871 if ( std::abs( fTickStart - pTransportPos->getTickOffsetSongSize() - fPrevTickStart ) > 4e-3 ) {
1873 QString( "[%5] Mismatch in the start of the tick interval handled by updateNoteQueue new [%1] != [%2] old+offset, old: %3, offset: %4" )
1874 .arg( fTickStart, 0, 'f' )
1875 .arg( fPrevTickStart + pTransportPos->getTickOffsetSongSize(), 0, 'f' )
1876 .arg( fPrevTickStart, 0, 'f' )
1877 .arg( pTransportPos->getTickOffsetSongSize(), 0, 'f' )
1878 .arg( sNewContext ) );
1879 }
1880 if ( std::abs( fTickEnd - pTransportPos->getTickOffsetSongSize() - fPrevTickEnd ) > 4e-3 ) {
1882 QString( "[%5] Mismatch in the end of the tick interval handled by updateNoteQueue new [%1] != [%2] old+offset, old: %3, offset: %4" )
1883 .arg( fTickEnd, 0, 'f' )
1884 .arg( fPrevTickEnd + pTransportPos->getTickOffsetSongSize(), 0, 'f' )
1885 .arg( fPrevTickEnd, 0, 'f' )
1886 .arg( pTransportPos->getTickOffsetSongSize(), 0, 'f' )
1887 .arg( sNewContext ) );
1888 }
1889 }
1890 else if ( pTransportPos->getColumn() != 0 &&
1891 nOldColumn >= pSong->getPatternGroupVector()->size() ) {
1893 QString( "[%4] Column reset failed nOldColumn: %1, pTransportPos->getColumn() (new): %2, pSong->getPatternGroupVector()->size() (new): %3" )
1894 .arg( nOldColumn )
1895 .arg( pTransportPos->getColumn() )
1896 .arg( pSong->getPatternGroupVector()->size() )
1897 .arg( sNewContext ) );
1898 }
1899
1900 // Now we emulate that playback continues without any new notes
1901 // being added and expect the rendering of the notes currently
1902 // played back by the Sampler to start off precisely where we
1903 // stopped before the song size change. New notes might still be
1904 // added due to the lookahead, so, we just check for the
1905 // processing of notes we already encountered.
1906 pAE->incrementTransportPosition( nBufferSize );
1907 pAE->processAudio( nBufferSize );
1908
1909 // Update the end of the tick interval usually handled by
1910 // updateNoteQueue().
1911 double fTickEndRolling, fTickStartUnused;
1912 pAE->computeTickInterval( &fTickStartUnused, &fTickEndRolling, nBufferSize );
1913
1914 pAE->incrementTransportPosition( nBufferSize );
1915 pAE->processAudio( nBufferSize );
1916
1917 pAE->m_fLastTickEnd = fTickEndRolling;
1918
1919 // Check whether tempo and tick size have not changed.
1920 if ( fPrevTempo != pTransportPos->getBpm() ||
1921 fPrevTickSize != pTransportPos->getTickSize() ) {
1923 QString( "[%1] tempo and ticksize are affected" )
1924 .arg( sNewContext ) );
1925 }
1926
1927 notesSamplerPostRolling.clear();
1928 for ( const auto& ppNote : pSampler->getPlayingNotesQueue() ) {
1929 notesSamplerPostRolling.push_back( std::make_shared<Note>( ppNote ) );
1930 }
1932 notesSamplerPostToggle, notesSamplerPostRolling,
1933 QString( "toggleAndCheckConsistency::toggleAndCheck : %1 : rolling after toggle (%2,%3)" )
1934 .arg( sContext ).arg( nToggleColumn ).arg( nToggleRow ),
1935 nBufferSize * 2, true );
1936 };
1937
1938 // Toggle the grid cell.
1939 toggleAndCheck( sContext + " : 1. toggle" );
1940
1941 // Toggle the same grid cell again.
1942 toggleAndCheck( sContext + " : 2. toggle" );
1943}
1944
1945void AudioEngineTests::resetSampler( const QString& sContext ) {
1946 auto pHydrogen = Hydrogen::get_instance();
1947 auto pSong = pHydrogen->getSong();
1948 auto pAE = pHydrogen->getAudioEngine();
1949 auto pSampler = pAE->getSampler();
1950 auto pPref = Preferences::get_instance();
1951
1952 // Larger number to account for both small buffer sizes and long
1953 // samples.
1954 const int nMaxCleaningCycles = 5000;
1955 int nn = 0;
1956
1957 // Ensure the sampler is clean.
1958 while ( pSampler->isRenderingNotes() ) {
1959 pAE->processAudio( pPref->m_nBufferSize );
1960 pAE->incrementTransportPosition( pPref->m_nBufferSize );
1961 ++nn;
1962
1963 // {//DEBUG
1964 // QString msg = QString( "[%1] nn: %2, note:" )
1965 // .arg( sContext ).arg( nn );
1966 // auto pNoteQueue = pSampler->getPlayingNotesQueue();
1967 // if ( pNoteQueue.size() > 0 ) {
1968 // auto pNote = pNoteQueue[0];
1969 // if ( pNote != nullptr ) {
1970 // msg.append( pNote->toQString("", true ) );
1971 // } else {
1972 // msg.append( " nullptr" );
1973 // }
1974 // DEBUGLOG( msg );
1975 // }
1976 // }
1977
1978 if ( nn > nMaxCleaningCycles ) {
1980 QString( "[%1] Sampler is in weird state" )
1981 .arg( sContext ) );
1982 }
1983 }
1984
1985 pAE->reset( false );
1986 pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
1987}
1988
1989void AudioEngineTests::throwException( const QString& sMsg ) {
1990 auto pHydrogen = Hydrogen::get_instance();
1991 auto pAE = pHydrogen->getAudioEngine();
1992
1993 pAE->setState( AudioEngine::State::Ready );
1994 pAE->unlock();
1995
1996 throw std::runtime_error( sMsg.toLocal8Bit().data() );
1997}
1998
1999
2000}; // namespace H2Core
#define RIGHT_HERE
Macro intended to be used for the logging of the locking of the H2Core::AudioEngine.
Definition AudioEngine.h:59
#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 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...
static float getBpmAtColumn(int nColumn)
static constexpr float fHumanizePitchSD
Maximum value the standard deviation of the Gaussian distribution the random pitch contribution will ...
@ 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:172
std::shared_ptr< Song > getSong() const
Get the current song.
Definition Hydrogen.h:122
static Hydrogen * get_instance()
Returns the current Hydrogen instance __instance.
Definition Hydrogen.h:83
AudioEngine * getAudioEngine() const
Definition Hydrogen.h:649
static double pitchToFrequency(double fPitch)
Convert a logarithmic pitch-space value in semitones to a frequency-domain value.
Definition Note.h:388
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.
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