/*
 * Hydrogen
 * Copyright(c) 2002-2008 by Alex >Comix< Cominu [comix@users.sourceforge.net]
 * Copyright(c) 2008-2023 The hydrogen development team [hydrogen-devel@lists.sourceforge.net]
 *
 * http://www.hydrogen-music.org
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY, without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see https://www.gnu.org/licenses
 *
 */
#include <random>
#include <stdexcept>

#include <core/AudioEngine/AudioEngineTests.h>
#include <core/AudioEngine/AudioEngine.h>
#include <core/AudioEngine/TransportPosition.h>

#include <core/Basics/InstrumentComponent.h>
#include <core/Basics/InstrumentLayer.h>
#include <core/Basics/Pattern.h>
#include <core/Basics/PatternList.h>
#include <core/Basics/Note.h>
#include <core/Basics/Sample.h>
#include <core/Basics/Song.h>
#include <core/Sampler/Sampler.h>
#include <core/Hydrogen.h>
#include <core/CoreActionController.h>
#include <core/Preferences/Preferences.h>
#include <core/config.h>

namespace H2Core
{

void AudioEngineTests::testFrameToTickConversion() {
	auto pHydrogen = Hydrogen::get_instance();
	auto pCoreActionController = pHydrogen->getCoreActionController();
	auto pAE = pHydrogen->getAudioEngine();
	
	pCoreActionController->activateTimeline( true );
	pCoreActionController->addTempoMarker( 0, 120 );
	pCoreActionController->addTempoMarker( 3, 100 );
	pCoreActionController->addTempoMarker( 5, 40 );
	pCoreActionController->addTempoMarker( 7, 200 );

	auto checkFrame = []( long long nFrame, double fTolerance ) {
		const double fTick = TransportPosition::computeTickFromFrame( nFrame );

		double fTickMismatch;
		const long long nFrameCheck =
			TransportPosition::computeFrameFromTick( fTick, &fTickMismatch );
		
		if ( nFrameCheck != nFrame || std::abs( fTickMismatch ) > fTolerance ) {
			AudioEngineTests::throwException(
				QString( "[testFrameToTickConversion::checkFrame] nFrame: %1, fTick: %2, nFrameComputed: %3, fTickMismatch: %4, frame diff: %5, fTolerance: %6" )
				.arg( nFrame ).arg( fTick, 0, 'E', -1 ).arg( nFrameCheck )
				.arg( fTickMismatch, 0, 'E', -1 ).arg( nFrameCheck - nFrame )
				.arg( fTolerance, 0, 'E', -1 ) );
		}
	};
	checkFrame( 342732, 1e-10 );
	checkFrame( 1037223, 1e-10 );
	checkFrame( 453610333722, 1e-6 );

	auto checkTick = []( double fTick, double fTolerance ) {
		double fTickMismatch;
		const long long nFrame =
			TransportPosition::computeFrameFromTick( fTick, &fTickMismatch );
		
		const double fTickCheck =
			TransportPosition::computeTickFromFrame( nFrame ) + fTickMismatch;

		if ( abs( fTickCheck - fTick ) > fTolerance ) {
			AudioEngineTests::throwException(
				QString( "[testFrameToTickConversion::checkTick] nFrame: %1, fTick: %2, fTickComputed: %3, fTickMismatch: %4, tick diff: %5, fTolerance: %6" )
				.arg( nFrame ).arg( fTick, 0, 'E', -1 ).arg( fTickCheck, 0, 'E', -1 )
				.arg( fTickMismatch, 0, 'E', -1 ).arg( fTickCheck - fTick, 0, 'E', -1 )
				.arg( fTolerance, 0, 'E', -1 ));
		}
	};
	checkTick( 552, 1e-9 );
	checkTick( 1939, 1e-9 );
	checkTick( 534623409, 1e-6 );
	checkTick( pAE->m_fSongSizeInTicks * 3, 1e-9 );
}

void AudioEngineTests::testTransportProcessing() {
	auto pHydrogen = Hydrogen::get_instance();
	auto pSong = pHydrogen->getSong();
	auto pPref = Preferences::get_instance();
	auto pCoreActionController = pHydrogen->getCoreActionController();
	auto pAE = pHydrogen->getAudioEngine();
	auto pTransportPos = pAE->getTransportPosition();
	auto pQueuingPos = pAE->m_pQueuingPosition;
	
	pCoreActionController->activateTimeline( false );
	pCoreActionController->activateLoopMode( true );

	pAE->lock( RIGHT_HERE );

    std::random_device randomSeed;
    std::default_random_engine randomEngine( randomSeed() );
    std::uniform_int_distribution<int> frameDist( 1, pPref->m_nBufferSize );
	std::uniform_real_distribution<float> tempoDist( MIN_BPM, MAX_BPM );

	// For this call the AudioEngine still needs to be in state
	// Playing or Ready.
	pAE->reset( false );
	pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
	pAE->setState( AudioEngine::State::Testing );

	// Check consistency of updated frames, ticks, and queuing
	// position while using a random buffer size (e.g. like PulseAudio
	// does).
	uint32_t nFrames;
	double fCheckTick, fLastTickIntervalEnd;
	long long nCheckFrame, nLastTransportFrame, nTotalFrames, nLastLookahead;
	long nLastQueuingTick;
	int nn;

	auto resetVariables = [&]() {
		nLastTransportFrame = 0;
		nLastQueuingTick = 0;
		fLastTickIntervalEnd = 0;
		nTotalFrames = 0;
		nLastLookahead = 0;
		nn = 0;
	};
	resetVariables();

	const int nMaxCycles =
		std::max( std::ceil( static_cast<double>(pAE->m_fSongSizeInTicks) /
							 static_cast<double>(pPref->m_nBufferSize) *
							 static_cast<double>(pTransportPos->getTickSize()) * 4.0 ),
				  static_cast<double>(pAE->m_fSongSizeInTicks) );

	while ( pTransportPos->getDoubleTick() <
			pAE->getSongSizeInTicks() ) {
		nFrames = frameDist( randomEngine );
		processTransport(
			"testTransportProcessing : song mode : constant tempo", nFrames,
			&nLastLookahead, &nLastTransportFrame, &nTotalFrames,
			&nLastQueuingTick, &fLastTickIntervalEnd, true );

		nn++;
		if ( nn > nMaxCycles ) {
			AudioEngineTests::throwException(
				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" )
				.arg( pTransportPos->getFrame() )
				.arg( pTransportPos->getDoubleTick(), 0, 'f' )
				.arg( pTransportPos->getTickSize(), 0, 'f' )
				.arg( pAE->getSongSizeInTicks(), 0, 'f' )
				.arg( nMaxCycles ) );
		}
	}

	pAE->reset( false );
	pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
	resetVariables();

	float fBpm;
	float fLastBpm = pTransportPos->getBpm();
	
	const int nCyclesPerTempo = 11;
	while ( pTransportPos->getDoubleTick() <
			pAE->getSongSizeInTicks() ) {

		fBpm = tempoDist( randomEngine );
		pAE->setNextBpm( fBpm );
		pAE->updateBpmAndTickSize( pTransportPos );
		pAE->updateBpmAndTickSize( pQueuingPos );
		
		nLastLookahead = 0;
		
		for ( int cc = 0; cc < nCyclesPerTempo; ++cc ) {
			nFrames = frameDist( randomEngine );
			processTransport(
				QString( "testTransportProcessing : song mode : variable tempo %1->%2" )
				.arg( fLastBpm, 0, 'f' ).arg( fBpm, 0, 'f' ), nFrames, &nLastLookahead,
				&nLastTransportFrame, &nTotalFrames, &nLastQueuingTick,
				&fLastTickIntervalEnd, true );
		}
		
		fLastBpm = fBpm;

		nn++;
		if ( nn > nMaxCycles ) {
			AudioEngineTests::throwException(
				"[testTransportProcessing] [song mode : variable tempo] end of the song wasn't reached in time." );
		}
	}

	pAE->setState( AudioEngine::State::Ready );
	pAE->unlock();

	// Check consistency of playback in PatternMode
	pCoreActionController->activateSongMode( false );

	pAE->lock( RIGHT_HERE );
	pAE->setState( AudioEngine::State::Testing );

	resetVariables();

	fLastBpm = pTransportPos->getBpm();

	const int nDifferentTempos = 10;
	for ( int tt = 0; tt < nDifferentTempos; ++tt ) {

		fBpm = tempoDist( randomEngine );
		
		pAE->setNextBpm( fBpm );
		pAE->updateBpmAndTickSize( pTransportPos );
		pAE->updateBpmAndTickSize( pQueuingPos );
		
		nLastLookahead = 0;

		for ( int cc = 0; cc < nCyclesPerTempo; ++cc ) {
			nFrames = frameDist( randomEngine );
			processTransport(
				QString( "testTransportProcessing : pattern mode : variable tempo %1->%2" )
				.arg( fLastBpm ).arg( fBpm ), nFrames, &nLastLookahead,
				&nLastTransportFrame, &nTotalFrames, &nLastQueuingTick,
				&fLastTickIntervalEnd, true );
		}

		fLastBpm = fBpm;
	}
	
	pAE->setState( AudioEngine::State::Ready );
	pAE->unlock();
	pCoreActionController->activateSongMode( true );
}

void AudioEngineTests::testTransportProcessingTimeline() {
	auto pHydrogen = Hydrogen::get_instance();
	auto pSong = pHydrogen->getSong();
	auto pTimeline = pHydrogen->getTimeline();
	auto pPref = Preferences::get_instance();
	auto pCoreActionController = pHydrogen->getCoreActionController();
	auto pAE = pHydrogen->getAudioEngine();
	auto pTransportPos = pAE->getTransportPosition();
	auto pQueuingPos = pAE->m_pQueuingPosition;
	
	pCoreActionController->activateLoopMode( true );

	pAE->lock( RIGHT_HERE );

	// Activating the Timeline without requiring the AudioEngine to be locked.
	auto activateTimeline = [&]( bool bEnabled ) {
		pPref->setUseTimelineBpm( bEnabled );
		pSong->setIsTimelineActivated( bEnabled );

		if ( bEnabled ) {
			pTimeline->activate();
		} else {
			pTimeline->deactivate();
		}

		pAE->handleTimelineChange();
	};
	activateTimeline( true );

    std::random_device randomSeed;
    std::default_random_engine randomEngine( randomSeed() );
    std::uniform_int_distribution<int> frameDist( 1, pPref->m_nBufferSize );
	std::uniform_real_distribution<float> tempoDist( MIN_BPM, MAX_BPM );

	// For this call the AudioEngine still needs to be in state
	// Playing or Ready.
	pAE->reset( false );
	pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
	pAE->setState( AudioEngine::State::Testing );

	// Check consistency of updated frames, ticks, and queuing
	// position while using a random buffer size (e.g. like PulseAudio
	// does).
	uint32_t nFrames;
	double fCheckTick, fLastTickIntervalEnd;
	long long nCheckFrame, nLastTransportFrame, nTotalFrames, nLastLookahead;
	long nLastQueuingTick;
	int nn;

	auto resetVariables = [&]() {
		nLastTransportFrame = 0;
		nLastQueuingTick = 0;
		fLastTickIntervalEnd = 0;
		nTotalFrames = 0;
		nLastLookahead = 0;
		nn = 0;
	};
	resetVariables();

	const int nMaxCycles =
		std::max( std::ceil( static_cast<double>(pAE->m_fSongSizeInTicks) /
							 static_cast<double>(pPref->m_nBufferSize) *
							 static_cast<double>(pTransportPos->getTickSize()) * 4.0 ),
				  static_cast<double>(pAE->m_fSongSizeInTicks) );

	while ( pTransportPos->getDoubleTick() <
			pAE->getSongSizeInTicks() ) {
		nFrames = frameDist( randomEngine );
		processTransport(
			QString( "[testTransportProcessingTimeline : song mode : all timeline]" ),
			nFrames, &nLastLookahead, &nLastTransportFrame, &nTotalFrames,
			&nLastQueuingTick, &fLastTickIntervalEnd, false );

		nn++;
		if ( nn > nMaxCycles ) {
			AudioEngineTests::throwException(
				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" )
				.arg( pTransportPos->getFrame() )
				.arg( pTransportPos->getDoubleTick(), 0, 'f' )
				.arg( pTransportPos->getTickSize(), 0, 'f' )
				.arg( pAE->getSongSizeInTicks(), 0, 'f' )
				.arg( nMaxCycles ) );
		}
	}

	// Alternate Timeline usage and timeline deactivation with
	// "classical" bpm change".

	pAE->reset( false );
	pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
	resetVariables();

	float fBpm;
	float fLastBpm = pTransportPos->getBpm();
	
	const int nCyclesPerTempo = 11;
	while ( pTransportPos->getDoubleTick() <
			pAE->getSongSizeInTicks() ) {

		QString sContext;
		if ( nn % 2 == 0 ){
			activateTimeline( false );
			fBpm = tempoDist( randomEngine );
			pAE->setNextBpm( fBpm );
			pAE->updateBpmAndTickSize( pTransportPos );
			pAE->updateBpmAndTickSize( pQueuingPos );

			sContext = "no timeline";
		}
		else {
			activateTimeline( true );
			fBpm = AudioEngine::getBpmAtColumn( pTransportPos->getColumn() );
			
			sContext = "timeline";
		}
		
		for ( int cc = 0; cc < nCyclesPerTempo; ++cc ) {
			nFrames = frameDist( randomEngine );
			processTransport(
				QString( "testTransportProcessing : alternating timeline : bpm %1->%2 : %3" )
				.arg( fLastBpm ).arg( fBpm ).arg( sContext ),
				nFrames, &nLastLookahead, &nLastTransportFrame, &nTotalFrames,
				&nLastQueuingTick, &fLastTickIntervalEnd, false );
		}
		
		fLastBpm = fBpm;

		nn++;
		if ( nn > nMaxCycles ) {
			AudioEngineTests::throwException(
				"[testTransportProcessingTimeline] [alternating timeline] end of the song wasn't reached in time." );
		}
	}

	pAE->setState( AudioEngine::State::Ready );
	pAE->unlock();
}

void AudioEngineTests::testLoopMode() {
	auto pHydrogen = Hydrogen::get_instance();
	auto pSong = pHydrogen->getSong();
	auto pPref = Preferences::get_instance();
	auto pCoreActionController = pHydrogen->getCoreActionController();
	auto pAE = pHydrogen->getAudioEngine();
	auto pTransportPos = pAE->getTransportPosition();
	
	pCoreActionController->activateLoopMode( true );
	pCoreActionController->activateSongMode( true );

	pAE->lock( RIGHT_HERE );

	// For this call the AudioEngine still needs to be in state
	// Playing or Ready.
	pAE->reset( false );
	pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
	pAE->setState( AudioEngine::State::Testing );

	// Check consistency of updated frames, ticks, and queuing
	// position while using a random buffer size (e.g. like PulseAudio
	// does).
	double fLastTickIntervalEnd;
	long long nLastTransportFrame, nTotalFrames, nLastLookahead;
	long nLastQueuingTick;
	int nn;

	auto resetVariables = [&]() {
		nLastTransportFrame = 0;
		nLastQueuingTick = 0;
		fLastTickIntervalEnd = 0;
		nTotalFrames = 0;
		nLastLookahead = 0;
		nn = 0;
	};
	resetVariables();

	const int nLoops = 3;
	const double fSongSizeInTicks = pAE->m_fSongSizeInTicks;

	const int nMaxCycles =
		std::max( std::ceil( fSongSizeInTicks /
							 static_cast<double>(pPref->m_nBufferSize) *
							 static_cast<double>(pTransportPos->getTickSize()) * 4.0 ),
				  fSongSizeInTicks ) *
		nLoops;

	// Run nLoops cycles in total. nLoops - 1 with loop mode enabled
	// and disabling it in the nLoops'th run. This should cause the
	// transport stop when reaching the end of the song again. The
	// condition of the while loop will be true for some longer just
	// to be sure.
	bool bLoopEnabled = true;
	int nRet = 0;
	while ( pTransportPos->getDoubleTick() <
			fSongSizeInTicks * ( nLoops + 2 ) ) {
		nRet = processTransport(
			QString( "[testTransportProcessingTimeline : song mode : all timeline]" ),
			pPref->m_nBufferSize, &nLastLookahead, &nLastTransportFrame,
			&nTotalFrames, &nLastQueuingTick, &fLastTickIntervalEnd, false );

		if ( nRet == -1 ) {
			break;
		}
			

		// Transport did run for nLoops - 1 rounds, let's deactivate
		// loop mode.
		if ( bLoopEnabled && pTransportPos->getDoubleTick() >
			 fSongSizeInTicks * ( nLoops - 1 ) ) {
			pAE->unlock();
			pCoreActionController->activateLoopMode( false );
			pAE->lock( RIGHT_HERE );
		}
		
		nn++;
		if ( nn > nMaxCycles ||
			 pTransportPos->getDoubleTick() > fSongSizeInTicks * nLoops ) {
			AudioEngineTests::throwException(
				QString( "[testLoopMode] transport is rolling for too long. pTransportPos: %1,\n\tfSongSizeInTicks(): %2, nLoops: %3, pPref->m_nBufferSize: %4, nMaxCycles: %5" )
				.arg( pTransportPos->toQString() )
				.arg( fSongSizeInTicks, 0, 'f' ).arg( nLoops )
				.arg( pPref->m_nBufferSize ).arg( nMaxCycles ) );
		}
	}

	// Ensure transport did run the requested number of loops.
	if ( pAE->m_pQueuingPosition->getDoubleTick() < fSongSizeInTicks * nLoops ) {
		AudioEngineTests::throwException(
			QString( "[testLoopMode] transport ended prematurely. pAE->m_pQueuingPosition: %1,\n\tfSongSizeInTicks(): %2, nLoops: %3, pPref->m_nBufferSize: %4" )
			.arg( pAE->m_pQueuingPosition->toQString() )
			.arg( fSongSizeInTicks, 0, 'f' ).arg( nLoops )
			.arg( pPref->m_nBufferSize ) );
	}
		

	pAE->setState( AudioEngine::State::Ready );
	pAE->unlock();
}

int AudioEngineTests::processTransport( const QString& sContext,
										 int nFrames,
										 long long* nLastLookahead,
										 long long* nLastTransportFrame,
										 long long* nTotalFrames,
										 long* nLastQueuingTick,
										 double* fLastTickIntervalEnd,
										 bool bCheckLookahead ) {
	auto pAE = Hydrogen::get_instance()->getAudioEngine();
	auto pTransportPos = pAE->getTransportPosition();
	auto pQueuingPos = pAE->m_pQueuingPosition;

	double fTickStart, fTickEnd;
	const long long nLeadLag =
		pAE->computeTickInterval( &fTickStart, &fTickEnd, nFrames );
	fTickStart = pAE->coarseGrainTick( fTickStart );
	fTickEnd = pAE->coarseGrainTick( fTickEnd );

	if ( bCheckLookahead ) {
		// If this is the first call after a tempo change, the last
		// lookahead will be set to 0
		if ( *nLastLookahead != 0 &&
			 *nLastLookahead != nLeadLag + AudioEngine::nMaxTimeHumanize + 1 ) {
			AudioEngineTests::throwException(
				QString( "[processTransport : lookahead] [%1] with one and the same BPM/tick size the lookahead must be consistent! [ %2 -> %3 ]" )
				.arg( sContext ).arg( *nLastLookahead )
				.arg( nLeadLag + AudioEngine::nMaxTimeHumanize + 1 ) );
		}
		*nLastLookahead = nLeadLag + AudioEngine::nMaxTimeHumanize + 1;
	}

	const int nRet = pAE->updateNoteQueue( nFrames );
	pAE->incrementTransportPosition( nFrames );

	if ( nRet != 0 ) {
		// Don't check consistency at the end of the song as just the
		// remaining frames are covered.
		return nRet;
	}

	AudioEngineTests::checkTransportPosition(
		pTransportPos, "[processTransport] " + sContext );

	AudioEngineTests::checkTransportPosition(
		pQueuingPos, "[processTransport] " + sContext );

	if ( pTransportPos->getFrame() - nFrames -
		 pTransportPos->getFrameOffsetTempo() != *nLastTransportFrame ) {
		AudioEngineTests::throwException(
			QString( "[processTransport : transport] [%1] inconsistent frame update. pTransportPos->getFrame(): %2, nFrames: %3, nLastTransportFrame: %4, pTransportPos->getFrameOffsetTempo(): %5" )
			.arg( sContext ).arg( pTransportPos->getFrame() ).arg( nFrames )
			.arg( *nLastTransportFrame ).arg( pTransportPos->getFrameOffsetTempo() ) );
	}
	*nLastTransportFrame = pTransportPos->getFrame() -
			pTransportPos->getFrameOffsetTempo();

	const int nNoteQueueUpdate =
		static_cast<int>( fTickEnd ) - static_cast<int>( fTickStart );
	// We will only compare the queuing position in case interval
	// in updateNoteQueue covers at least one tick and, thus,
	// an update has actually taken place.
	if ( *nLastQueuingTick > 0 && nNoteQueueUpdate > 0 ) {
		if ( pQueuingPos->getTick() - nNoteQueueUpdate !=
			 *nLastQueuingTick ) {
			AudioEngineTests::throwException(
				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" )
				.arg( sContext ).arg( pQueuingPos->getTick() )
				.arg( nNoteQueueUpdate ).arg( *nLastQueuingTick )
				.arg( fTickStart, 0, 'f' ).arg( fTickEnd, 0, 'f' )
				.arg( nFrames ).arg( pTransportPos->toQString() )
				.arg( pQueuingPos->toQString() ) );
		}
	}
	*nLastQueuingTick = pQueuingPos->getTick();

	// Check whether the tick interval covered in updateNoteQueue
	// is consistent and does not include holes or overlaps.
	// In combination with testNoteEnqueuing this should
	// guarantuee that all note will be queued properly.
	if ( std::abs( fTickStart - *fLastTickIntervalEnd ) > 1E-4 ||
		 fTickStart > fTickEnd ) {
		AudioEngineTests::throwException(
			QString( "[processTransport : tick interval] [%1] inconsistent update. old: [ ... : %2 ], new: [ %3, %4 ], pTransportPos->getTickOffsetQueuing(): %5, diff: %6" )
			.arg( sContext ).arg( *fLastTickIntervalEnd ).arg( fTickStart )
			.arg( fTickEnd ).arg( pTransportPos->getTickOffsetQueuing() )
			.arg( std::abs( fTickStart - *fLastTickIntervalEnd ), 0, 'E', -1 ) );
	}
	*fLastTickIntervalEnd = fTickEnd;

	// Using the offset Hydrogen can keep track of the actual
	// number of frames passed since the playback was started
	// even in case a tempo change was issued by the user.
	*nTotalFrames += nFrames;
	if ( pTransportPos->getFrame() - pTransportPos->getFrameOffsetTempo() !=
		 *nTotalFrames ) {
		AudioEngineTests::throwException(
			QString( "[processTransport : total] [%1] total frames incorrect. pTransportPos->getFrame(): %2, pTransportPos->getFrameOffsetTempo(): %3, nTotalFrames: %4" )
			.arg( sContext ).arg( pTransportPos->getFrame() )
			.arg( pTransportPos->getFrameOffsetTempo() ).arg( *nTotalFrames ) );
	}

	return 0;
}

void AudioEngineTests::testTransportRelocation() {
	auto pHydrogen = Hydrogen::get_instance();
	auto pSong = pHydrogen->getSong();
	auto pCoreActionController = pHydrogen->getCoreActionController();
	auto pPref = Preferences::get_instance();
	auto pAE = pHydrogen->getAudioEngine();
	auto pTransportPos = pAE->getTransportPosition();

	pAE->lock( RIGHT_HERE );

    std::random_device randomSeed;
    std::default_random_engine randomEngine( randomSeed() );
    std::uniform_real_distribution<double> tickDist( 0, pAE->m_fSongSizeInTicks );
	std::uniform_int_distribution<long long> frameDist( 0, pPref->m_nBufferSize );

	// For this call the AudioEngine still needs to be in state
	// Playing or Ready.
	pAE->reset( false );
	pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
	pAE->setState( AudioEngine::State::Testing );

	// Check consistency of updated frames and ticks while relocating
	// transport.
	double fNewTick;
	long long nNewFrame;

	const int nProcessCycles = 100;
	for ( int nn = 0; nn < nProcessCycles; ++nn ) {

		if ( nn < nProcessCycles - 2 ) {
			fNewTick = tickDist( randomEngine );
		}
		else if ( nn < nProcessCycles - 1 ) {
			// Resulted in an unfortunate rounding error due to the
			// song end at 2112. 
			fNewTick = 2111.928009209;
		}
		else {
			// There was a rounding error at this particular tick.
			fNewTick = 960;
		}

		pAE->locate( fNewTick, false );

		AudioEngineTests::checkTransportPosition(
			pTransportPos, "[testTransportRelocation] mismatch tick-based" );

		if ( pAE->updateNoteQueue( pPref->m_nBufferSize ) == -1 &&
			pAE->m_fLastTickEnd < pAE->m_fSongSizeInTicks ) {
			AudioEngineTests::throwException(
				QString( "[testTransportRelocation] [tick] invalid end of song: fNewTick: %1, pAE->m_fSongSizeInTicks: %2, pAE->m_fLastTickEnd: %3, transport: %4;, queuing: %5" )
				.arg( fNewTick, 0, 'f' )
				.arg( pAE->m_pTransportPosition->toQString() )
				.arg( pAE->m_fSongSizeInTicks ).arg( pAE->m_fLastTickEnd )
				.arg( pAE->m_pTransportPosition->toQString() ) );
		}

		// Frame-based relocation
		nNewFrame = frameDist( randomEngine );
		pAE->locateToFrame( nNewFrame );

		AudioEngineTests::checkTransportPosition(
			pTransportPos, "[testTransportRelocation] mismatch frame-based" );

		if ( pAE->updateNoteQueue( pPref->m_nBufferSize ) == -1 &&
			pAE->m_fLastTickEnd < pAE->m_fSongSizeInTicks ) {
			AudioEngineTests::throwException(
				QString( "[testTransportRelocation] [frame] invalid end of song: nNewFrame: %1, pAE->m_fSongSizeInTicks: %2, pAE->m_fLastTickEnd: %3, transport: %4;, queuing: %5" )
				.arg( nNewFrame ).arg( pAE->m_pTransportPosition->toQString() )
				.arg( pAE->m_fSongSizeInTicks ).arg( pAE->m_fLastTickEnd )
				.arg( pAE->m_pTransportPosition->toQString() ) );
		}
	}

	pAE->reset( false );
	pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
	pAE->setState( AudioEngine::State::Ready );
	pAE->unlock();
}

void AudioEngineTests::testSongSizeChange() {
	auto pHydrogen = Hydrogen::get_instance();
	auto pCoreActionController = pHydrogen->getCoreActionController();
	auto pSong = pHydrogen->getSong();
	auto pAE = pHydrogen->getAudioEngine();

	const int nTestColumn = 4;
	
	pAE->lock( RIGHT_HERE );
	pAE->reset( false );
	pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
	pAE->setState( AudioEngine::State::Ready );
	pAE->unlock();
	
	pCoreActionController->activateLoopMode( true );
	pCoreActionController->locateToColumn( nTestColumn );
	
	pAE->lock( RIGHT_HERE );
	pAE->setState( AudioEngine::State::Testing );

	AudioEngineTests::toggleAndCheckConsistency( 1, 1, "[testSongSizeChange] prior" );
		
	// Toggle a grid cell after to the current transport position
	AudioEngineTests::toggleAndCheckConsistency( 6, 6, "[testSongSizeChange] after" );

	// Now we head to the "same" position inside the song but with the
	// transport being looped once.
	long nNextTick = pHydrogen->getTickForColumn( nTestColumn );
	if ( nNextTick == -1 ) {
		AudioEngineTests::throwException(
			QString( "[testSongSizeChange] Bad test design: there is no column [%1]" )
			.arg( nTestColumn ) );
	}

	nNextTick += pSong->lengthInTicks();
	
	pAE->locate( nNextTick );
	
	AudioEngineTests::toggleAndCheckConsistency( 1, 1, "[testSongSizeChange] looped:prior" );
		
	// Toggle a grid cell after to the current transport position
	AudioEngineTests::toggleAndCheckConsistency( 13, 6, "[testSongSizeChange] looped:after" );

	pAE->setState( AudioEngine::State::Ready );
	pAE->unlock();
	pCoreActionController->activateLoopMode( false );
}

void AudioEngineTests::testSongSizeChangeInLoopMode() {
	auto pHydrogen = Hydrogen::get_instance();
	auto pSong = pHydrogen->getSong();
	auto pCoreActionController = pHydrogen->getCoreActionController();
	auto pPref = Preferences::get_instance();
	auto pAE = pHydrogen->getAudioEngine();
	auto pTransportPos = pAE->getTransportPosition();
	
	pCoreActionController->activateTimeline( false );
	pCoreActionController->activateLoopMode( true );

	pAE->lock( RIGHT_HERE );

	const int nColumns = pSong->getPatternGroupVector()->size();

    std::random_device randomSeed;
    std::default_random_engine randomEngine( randomSeed() );
    std::uniform_real_distribution<double> tickDist( 1, pPref->m_nBufferSize );
	std::uniform_int_distribution<int> columnDist( nColumns, nColumns + 100 );

	// For this call the AudioEngine still needs to be in state
	// Playing or Ready.
	pAE->reset( false );
	pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
	pAE->setState( AudioEngine::State::Testing );

	const uint32_t nFrames = 500;
	const double fInitialSongSize = pAE->m_fSongSizeInTicks;
	int nNewColumn;
	double fTick;

	auto checkState = [&]( const QString& sContext, bool bSongSizeShouldChange ){
		AudioEngineTests::checkTransportPosition(
			pTransportPos,
			QString( "[testSongSizeChangeInLoopMode::checkState] [%1] before increment" )
			.arg( sContext ) );

		if ( bSongSizeShouldChange &&
			 fInitialSongSize == pAE->m_fSongSizeInTicks ) {
			AudioEngineTests::throwException(
				QString( "[testSongSizeChangeInLoopMode] [%1] song size stayed the same [%2->%3]")
				.arg( sContext ).arg( fInitialSongSize ).arg( pAE->m_fSongSizeInTicks ) );
		}
		else if ( ! bSongSizeShouldChange &&
			 fInitialSongSize != pAE->m_fSongSizeInTicks ) {
			AudioEngineTests::throwException(
				QString( "[testSongSizeChangeInLoopMode] [%1] unexpected song enlargement [%2->%3]")
				.arg( sContext ).arg( fInitialSongSize ).arg( pAE->m_fSongSizeInTicks ) );
		}

		pAE->incrementTransportPosition( nFrames );

		AudioEngineTests::checkTransportPosition(
			pTransportPos,
			QString( "[testSongSizeChangeInLoopMode::checkState] [%1] after increment" )
			.arg( sContext ) );
	};

	const int nNumberOfTogglings = 5;
	for ( int nn = 0; nn < nNumberOfTogglings; ++nn ) {

		fTick = tickDist( randomEngine );
		pAE->locate( fInitialSongSize + fTick );

		checkState( QString( "relocation to [%1]" ).arg( fTick ), false );

		nNewColumn = columnDist( randomEngine );

		pAE->unlock();
		pCoreActionController->toggleGridCell( nNewColumn, 0 );
		pAE->lock( RIGHT_HERE );

		checkState( QString( "toggling column [%1]" ).arg( nNewColumn ), true );
														  
		pAE->unlock();
		pCoreActionController->toggleGridCell( nNewColumn, 0 );
		pAE->lock( RIGHT_HERE );

		checkState( QString( "again toggling column [%1]" ).arg( nNewColumn ), false );
	}

	pAE->setState( AudioEngine::State::Ready );
	pAE->unlock();
}

void AudioEngineTests::testNoteEnqueuing() {
	auto pHydrogen = Hydrogen::get_instance();
	auto pSong = pHydrogen->getSong();
	auto pCoreActionController = pHydrogen->getCoreActionController();
	auto pPref = Preferences::get_instance();
	auto pAE = pHydrogen->getAudioEngine();
	auto pSampler = pAE->getSampler();
	auto pTransportPos = pAE->getTransportPosition();
	auto pQueuingPos = pAE->m_pQueuingPosition;

	pCoreActionController->activateTimeline( false );
	pCoreActionController->activateLoopMode( false );
	pCoreActionController->activateSongMode( true );
	pAE->lock( RIGHT_HERE );

    std::random_device randomSeed;
    std::default_random_engine randomEngine( randomSeed() );
    std::uniform_int_distribution<int> frameDist( pPref->m_nBufferSize / 2,
												  pPref->m_nBufferSize );

	// For this call the AudioEngine still needs to be in state
	// Playing or Ready.
	pAE->reset( false );
	pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
	pAE->setState( AudioEngine::State::Testing );

	// Check consistency of updated frames and ticks while using a
	// random buffer size (e.g. like PulseAudio does).
	
	uint32_t nFrames;
	int nn = 0;

	int nMaxCycles =
		std::max( std::ceil( static_cast<double>(pAE->m_fSongSizeInTicks) /
							 static_cast<double>(pPref->m_nBufferSize) *
							 static_cast<double>(pTransportPos->getTickSize()) * 4.0 ),
				  static_cast<double>(pAE->m_fSongSizeInTicks) );


	// Ensure the sampler is clean.
	AudioEngineTests::resetSampler( "testNoteEnqueuing : song mode" );

	auto notesInSong = pSong->getAllNotes();
	std::vector<std::shared_ptr<Note>> notesInSongQueue;
	std::vector<std::shared_ptr<Note>> notesInSamplerQueue;

	auto retrieveNotes = [&]( const QString& sContext ) {
		// Add freshly enqueued notes.
		AudioEngineTests::mergeQueues( &notesInSongQueue,
									   AudioEngineTests::copySongNoteQueue() );
		pAE->processAudio( nFrames );

		AudioEngineTests::mergeQueues( &notesInSamplerQueue,
									   pSampler->getPlayingNotesQueue() );

		pAE->incrementTransportPosition( nFrames );

		++nn;
		if ( nn > nMaxCycles ) {
			AudioEngineTests::throwException(
				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" )
				.arg( sContext ).arg( pTransportPos->getFrame() )
				.arg( pTransportPos->getDoubleTick(), 0, 'f' )
				.arg( pTransportPos->getTickSize(), 0, 'f' )
				.arg( pAE->m_fSongSizeInTicks, 0, 'f' ).arg( nMaxCycles ) );
		}
	};

	nn = 0;
	int nRes;
	while ( pQueuingPos->getDoubleTick() < pAE->m_fSongSizeInTicks ) {

		nFrames = frameDist( randomEngine );
		nRes = pAE->updateNoteQueue( nFrames );
		retrieveNotes( "song mode" );

		if ( nRes == -1 ) {
			break;
		}
	}

	auto checkQueueConsistency = [&]( const QString& sContext ) {
		if ( notesInSongQueue.size() !=
			 notesInSong.size() ) {
			QString sMsg = QString( "[testNoteEnqueuing::checkQueueConsistency] [%1] Mismatch between notes count in Song [%2] and NoteQueue [%3]. Song:\n" )
				.arg( sContext ).arg( notesInSong.size() )
				.arg( notesInSongQueue.size() );
			for ( int ii = 0; ii < notesInSong.size(); ++ii  ) {
				auto note = notesInSong[ ii ];
				sMsg.append( QString( "\t[%1] instr: %2, position: %3, noteStart: %4, velocity: %5\n")
							 .arg( ii )
							 .arg( note->get_instrument()->get_name() )
							 .arg( note->get_position() )
							 .arg( note->getNoteStart() )
							 .arg( note->get_velocity() ) );
			}
			sMsg.append( "NoteQueue:\n" );
			for ( int ii = 0; ii < notesInSongQueue.size(); ++ii  ) {
				auto note = notesInSongQueue[ ii ];
				sMsg.append( QString( "\t[%1] instr: %2, position: %3, noteStart: %4, velocity: %5\n")
							 .arg( ii )
							 .arg( note->get_instrument()->get_name() )
							 .arg( note->get_position() )
							 .arg( note->getNoteStart() )
							 .arg( note->get_velocity() ) );
			}

			AudioEngineTests::throwException( sMsg );
		}

		// We have to relax the test for larger buffer sizes. Else, the
		// notes will be already fully processed in and flush from the
		// Sampler before we had the chance to grab and compare them.
		if ( notesInSamplerQueue.size() !=
			 notesInSong.size() &&
			 pPref->m_nBufferSize < 1024 ) {
			QString sMsg = QString( "[testNoteEnqueuing::checkQueueConsistency] [%1] Mismatch between notes count in Song [%2] and Sampler [%3]. Song:\n" )
				.arg( sContext ).arg( notesInSong.size() )
				.arg( notesInSamplerQueue.size() );
			for ( int ii = 0; ii < notesInSong.size(); ++ii  ) {
				auto note = notesInSong[ ii ];
				sMsg.append( QString( "\t[%1] instr: %2, position: %3, noteStart: %4, velocity: %5\n")
							 .arg( ii )
							 .arg( note->get_instrument()->get_name() )
							 .arg( note->get_position() )
							 .arg( note->getNoteStart() )
							 .arg( note->get_velocity() ) );
			}
			sMsg.append( "SamplerQueue:\n" );
			for ( int ii = 0; ii < notesInSamplerQueue.size(); ++ii  ) {
				auto note = notesInSamplerQueue[ ii ];
				sMsg.append( QString( "\t[%1] instr: %2, position: %3, noteStart: %4, velocity: %5\n")
							 .arg( ii )
							 .arg( note->get_instrument()->get_name() )
							 .arg( note->get_position() )
							 .arg( note->getNoteStart() )
							 .arg( note->get_velocity() ) );
			}

			AudioEngineTests::throwException( sMsg );
		}
	};
	checkQueueConsistency( "song mode" );

	pAE->setState( AudioEngine::State::Ready );
	pAE->unlock();

	//////////////////////////////////////////////////////////////////
	// Perform the test in pattern mode
	//////////////////////////////////////////////////////////////////

	pCoreActionController->activateSongMode( false );
	pHydrogen->setPatternMode( Song::PatternMode::Selected );
	pHydrogen->setSelectedPatternNumber( 4 );

	pAE->lock( RIGHT_HERE );
	pAE->setState( AudioEngine::State::Testing );

	AudioEngineTests::resetSampler( "testNoteEnqueuing : pattern mode" );

	auto pPattern = 
		pSong->getPatternList()->get( pHydrogen->getSelectedPatternNumber() );
	if ( pPattern == nullptr ) {
		AudioEngineTests::throwException(
			QString( "[testNoteEnqueuing] null pattern selected [%1]" )
			.arg( pHydrogen->getSelectedPatternNumber() ) );
	}

	int nLoops = 5;
	notesInSong.clear();
	for ( int ii = 0; ii < nLoops; ++ii ) {
		FOREACH_NOTE_CST_IT_BEGIN_LENGTH( pPattern->get_notes(), it, pPattern ) {
			if ( it->second != nullptr ) {
				auto note = std::make_shared<Note>( it->second );
				note->set_position( note->get_position() +
									ii * pPattern->get_length() );
				notesInSong.push_back( note );
			}
		}
	}

	notesInSongQueue.clear();
	notesInSamplerQueue.clear();

	nMaxCycles =
		static_cast<int>(std::max( static_cast<float>(pPattern->get_length()) *
								   static_cast<float>(nLoops) *
								   pTransportPos->getTickSize() * 4 /
								   static_cast<float>(pPref->m_nBufferSize),
								   static_cast<float>(MAX_NOTES) *
								   static_cast<float>(nLoops) ));
	nn = 0;
	while ( pQueuingPos->getDoubleTick() < pPattern->get_length() * nLoops ) {

		nFrames = frameDist( randomEngine );
		pAE->updateNoteQueue( nFrames );
		retrieveNotes( "pattern mode" );
	}

	// Transport in pattern mode is always looped. We have to pop the
	// notes added during the second run due to the lookahead.
	auto popSurplusNotes = [&]( std::vector<std::shared_ptr<Note>>* queue ) {
		const int nNoteNumber = queue->size();
		for ( int ii = 0; ii < nNoteNumber; ++ii ) {
			auto pNote = queue->at( nNoteNumber - 1 - ii );
			if ( pNote != nullptr &&
				 pNote->get_position() >= pPattern->get_length() * nLoops ) {
				queue->pop_back();
			} else {
				break;
			}
		}
	};
	popSurplusNotes( &notesInSongQueue );
	popSurplusNotes( &notesInSamplerQueue );

	checkQueueConsistency( "pattern mode" );
	
	pAE->setState( AudioEngine::State::Ready );
	pAE->unlock();

	//////////////////////////////////////////////////////////////////
	// Perform the test in looped song mode
	//////////////////////////////////////////////////////////////////
	// In case the transport is looped the first note was lost the
	// first time transport was wrapped to the beginning again. This
	// occurred just in song mode.
	
	pCoreActionController->activateLoopMode( true );
	pCoreActionController->activateSongMode( true );

	pAE->lock( RIGHT_HERE );
	pAE->reset( false );
	pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
	pAE->setState( AudioEngine::State::Testing );

	nLoops = 1;

	nMaxCycles =
		std::max( std::ceil( static_cast<double>(pAE->m_fSongSizeInTicks) /
							 static_cast<double>(pPref->m_nBufferSize) *
							 static_cast<double>(pTransportPos->getTickSize()) * 4.0 ),
				  static_cast<double>(pAE->m_fSongSizeInTicks) ) *
		( nLoops + 1 );
	
	AudioEngineTests::resetSampler( "testNoteEnqueuing : loop mode" );

	notesInSong.clear();
	for ( int ii = 0; ii <= nLoops; ++ii ) {
		auto notesVec = pSong->getAllNotes();
		for ( auto nnote : notesVec ) {
			nnote->set_position( nnote->get_position() +
								 ii * pAE->m_fSongSizeInTicks );
		}
		notesInSong.insert( notesInSong.end(), notesVec.begin(), notesVec.end() );
	}

	notesInSongQueue.clear();
	notesInSamplerQueue.clear();

	nn = 0;
	while ( pQueuingPos->getDoubleTick() < 
			pAE->m_fSongSizeInTicks * ( nLoops + 1 ) ) {

		nFrames = frameDist( randomEngine );

		// Turn off loop mode once we entered the last loop cycle.
		if ( ( pTransportPos->getDoubleTick() >
			   pAE->m_fSongSizeInTicks * nLoops + 100 ) &&
			 pSong->getLoopMode() == Song::LoopMode::Enabled ) {
			pCoreActionController->activateLoopMode( false );
		}

		nRes = pAE->updateNoteQueue( nFrames );
		retrieveNotes( "looped song mode" );

		if ( nRes == -1 ) {
			break;
		}
	}

	checkQueueConsistency( "looped song mode" );
	
	pAE->setState( AudioEngine::State::Ready );
	pAE->unlock();
}

void AudioEngineTests::testNoteEnqueuingTimeline() {
	auto pHydrogen = Hydrogen::get_instance();
	auto pSong = pHydrogen->getSong();
	auto pAE = pHydrogen->getAudioEngine();
	auto pSampler = pAE->getSampler();
	auto pTransportPos = pAE->getTransportPosition();
	auto pPref = Preferences::get_instance();

	pAE->lock( RIGHT_HERE );

    std::random_device randomSeed;
    std::default_random_engine randomEngine( randomSeed() );
    std::uniform_int_distribution<int> frameDist( pPref->m_nBufferSize / 2,
												  pPref->m_nBufferSize );

	// For reset() the AudioEngine still needs to be in state
	// Playing or Ready.
	pAE->reset( false );
	pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
	pAE->setState( AudioEngine::State::Testing );
	AudioEngineTests::resetSampler( __PRETTY_FUNCTION__ );

	uint32_t nFrames;

	const int nMaxCycles =
		std::max( std::ceil( static_cast<double>(pAE->m_fSongSizeInTicks) /
							 static_cast<double>(pPref->m_nBufferSize) *
							 static_cast<double>(pTransportPos->getTickSize()) * 4.0 ),
				  static_cast<double>(pAE->m_fSongSizeInTicks) );

	auto notesInSong = pSong->getAllNotes();
	std::vector<std::shared_ptr<Note>> notesInSongQueue;
	std::vector<std::shared_ptr<Note>> notesInSamplerQueue;

	int nn = 0;
	bool bEndOfSongReached = false;
	while ( pTransportPos->getDoubleTick() <
			pAE->m_fSongSizeInTicks ) {

		nFrames = frameDist( randomEngine );

		if ( ! bEndOfSongReached ) {
			if ( pAE->updateNoteQueue( nFrames ) == -1 ) {
				bEndOfSongReached = true;
			}
		}

		// Add freshly enqueued notes.
		AudioEngineTests::mergeQueues( &notesInSongQueue,
									   AudioEngineTests::copySongNoteQueue() );

		pAE->processAudio( nFrames );

		AudioEngineTests::mergeQueues( &notesInSamplerQueue,
									   pSampler->getPlayingNotesQueue() );

		pAE->incrementTransportPosition( nFrames );

		++nn;
		if ( nn > nMaxCycles ) {
			AudioEngineTests::throwException(
				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" )
				.arg( pTransportPos->getFrame() )
				.arg( pTransportPos->getDoubleTick(), 0, 'f' )
				.arg( pTransportPos->getTickSize(), 0, 'f' )
				.arg( pAE->m_fSongSizeInTicks, 0, 'f' )
				.arg( nMaxCycles ) );
		}
	}

	if ( notesInSongQueue.size() != notesInSong.size() ) {
		AudioEngineTests::throwException(
			QString( "Mismatching number of notes in song [%1] and note queue [%2]" )
			.arg( notesInSong.size() )
			.arg( notesInSongQueue.size() ) );
	}

	if ( notesInSamplerQueue.size() != notesInSong.size() ) {
		AudioEngineTests::throwException(
			QString( "Mismatching number of notes in song [%1] and sampler queue [%2]" )
			.arg( notesInSong.size() )
			.arg( notesInSamplerQueue.size() ) );
	}

	// Ensure the ordering of the notes is identical
	for ( int ii = 0; ii < notesInSong.size(); ++ii ) {
		if ( ! notesInSong[ ii ]->match( notesInSongQueue[ ii ] ) ) {
		AudioEngineTests::throwException(
			QString( "Mismatch at note [%1] between song [%2] and song queue [%3]" )
			.arg( ii )
			.arg( notesInSong[ ii ]->toQString() )
			.arg( notesInSongQueue[ ii ]->toQString() ) );
		}
		if ( ! notesInSong[ ii ]->match( notesInSamplerQueue[ ii ] ) ) {
		AudioEngineTests::throwException(
			QString( "Mismatch at note [%1] between song [%2] and sampler queue [%3]" )
			.arg( ii )
			.arg( notesInSong[ ii ]->toQString() )
			.arg( notesInSamplerQueue[ ii ]->toQString() ) );
		}
	}

	pAE->setState( AudioEngine::State::Ready );
	pAE->unlock();
}

void AudioEngineTests::testHumanization() {
	auto pHydrogen = Hydrogen::get_instance();
	auto pSong = pHydrogen->getSong();
	auto pAE = pHydrogen->getAudioEngine();
	auto pSampler = pAE->getSampler();
	auto pTransportPos = pAE->getTransportPosition();
	auto pPref = Preferences::get_instance();
	auto pCoreActionController = pHydrogen->getCoreActionController();

	pCoreActionController->activateLoopMode( false );
	pCoreActionController->activateSongMode( true );

	pAE->lock( RIGHT_HERE );

	// For reset() the AudioEngine still needs to be in state
	// Playing or Ready.
	pAE->reset( false );
	pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
	pAE->setState( AudioEngine::State::Testing );

	// Rolls playback from beginning to the end of the song and
	// captures all notes added to the Sampler.
	auto getNotes = [&]( std::vector<std::shared_ptr<Note>> *notes ) {

		AudioEngineTests::resetSampler( "testHumanization::getNotes" );

		// Factor by which the number of frames processed when
		// retrieving notes will be smaller than the buffer size. This
		// vital because when using a large number of frames below the
		// notes might already be processed and flushed from the
		// Sampler before we had the chance to retrieve them.
		const double fStep = 10.0;
		const int nMaxCycles =
			std::max( std::ceil( static_cast<double>(pAE->m_fSongSizeInTicks) /
								 static_cast<double>(pPref->m_nBufferSize) * fStep *
								 static_cast<double>(pTransportPos->getTickSize()) * 4.0 ),
					  static_cast<double>(pAE->m_fSongSizeInTicks) );
		const uint32_t nFrames = static_cast<uint32_t>(
			std::round( static_cast<double>(pPref->m_nBufferSize) / fStep ) );

		pAE->locate( 0 );

		QString sPlayingPatterns;
		for ( const auto& pattern : *pTransportPos->getPlayingPatterns() ) {
			sPlayingPatterns += " " + pattern->get_name();
		}
		
		int nn = 0;
		bool bEndOfSongReached = false;
		while ( pTransportPos->getDoubleTick() < pAE->m_fSongSizeInTicks ||
				pAE->getEnqueuedNotesNumber() > 0 ) {

			if ( ! bEndOfSongReached ) {
				if ( pAE->updateNoteQueue( nFrames ) == -1 ) {
					bEndOfSongReached = true;
				}
			}

			pAE->processAudio( nFrames );
			
			AudioEngineTests::mergeQueues( notes,
										   pSampler->getPlayingNotesQueue() );
			
			pAE->incrementTransportPosition( nFrames );

			++nn;
			if ( nn > nMaxCycles ) {
				AudioEngineTests::throwException(
					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" )
					.arg( pTransportPos->getFrame() )
					.arg( pTransportPos->getDoubleTick(), 0, 'f' )
					.arg( pTransportPos->getTickSize(), 0, 'f' )
					.arg( pAE->m_fSongSizeInTicks, 0, 'f' )
					.arg( nMaxCycles ) );
			}
		}
	};

	// Sets the rotaries of velocity and timinig humanization as well
	// as of the pitch randomization of the Kick Instrument (used for
	// the notes in the test song) to a particular value between 0 and
	// 1.
	auto setHumanization = [&]( double fValue ) {
		fValue = std::clamp( fValue, 0.0, 1.0 );

		pSong->setHumanizeTimeValue( fValue );
		pSong->setHumanizeVelocityValue( fValue );

		pSong->getInstrumentList()->get( 0 )->set_random_pitch_factor( fValue );
	};

	auto setSwing = [&]( double fValue ) {
		fValue = std::clamp( fValue, 0.0, 1.0 );

		pSong->setSwingFactor( fValue );
	};

	// Reference notes with no humanization and property
	// customization.
	auto notesInSong = pSong->getAllNotes();

	// First pattern is activated per default.
	setHumanization( 0 );
	setSwing( 0 );

	std::vector<std::shared_ptr<Note>> notesReference;
	getNotes( &notesReference );
	
	if ( notesReference.size() != notesInSong.size() ) {
		AudioEngineTests::throwException(
			QString( "[testHumanization] [references] Bad test setup. Mismatching number of notes [%1 : %2]" )
			.arg( notesReference.size() )
			.arg( notesInSong.size() ) );
	}

	//////////////////////////////////////////////////////////////////
	// Pattern 2 contains notes of the same instrument at the same
	// positions but velocity, pan, leag&lag, and note key as well as
	// note octave are all customized. Check whether these
	// customizations reach the Sampler.
	pAE->unlock();
	pCoreActionController->toggleGridCell( 0, 0 );
	pCoreActionController->toggleGridCell( 0, 1 );
	pAE->lock( RIGHT_HERE );

	std::vector<std::shared_ptr<Note>> notesCustomized;
	getNotes( &notesCustomized );

	if ( notesReference.size() != notesCustomized.size() ) {
		AudioEngineTests::throwException(
			QString( "[testHumanization] [customization] Mismatching number of notes [%1 : %2]" )
			.arg( notesReference.size() )
			.arg( notesCustomized.size() ) );
	}
	
	for ( int ii = 0; ii < notesReference.size(); ++ii ) {
		auto pNoteReference = notesReference[ ii ];
		auto pNoteCustomized = notesCustomized[ ii ];

		if ( pNoteReference != nullptr && pNoteCustomized != nullptr ) {
			if ( pNoteReference->get_velocity() ==
				 pNoteCustomized->get_velocity() ) {
				AudioEngineTests::throwException(
					QString( "[testHumanization] [customization] Velocity of note [%1] was not altered" )
					.arg( ii ) );
			} else if ( pNoteReference->get_lead_lag() ==
				 pNoteCustomized->get_lead_lag() ) {
				AudioEngineTests::throwException(
					QString( "[testHumanization] [customization] Lead Lag of note [%1] was not altered" )
					.arg( ii ) );
			} else if ( pNoteReference->getNoteStart() ==
				 pNoteCustomized->getNoteStart() ) {
				// The note start incorporates the lead & lag
				// information and is the property used by the
				// Sampler.
				AudioEngineTests::throwException(
					QString( "[testHumanization] [customization] Note start of note [%1] was not altered" )
					.arg( ii ) );
			} else if ( pNoteReference->getPan() ==
				 pNoteCustomized->getPan() ) {
				AudioEngineTests::throwException(
					QString( "[testHumanization] [customization] Pan of note [%1] was not altered" )
					.arg( ii ) );
			} else if ( pNoteReference->get_total_pitch() ==
				 pNoteCustomized->get_total_pitch() ) {
				AudioEngineTests::throwException(
					QString( "[testHumanization] [customization] Total Pitch of note [%1] was not altered" )
					.arg( ii ) );
			}
		} else {
			AudioEngineTests::throwException(
				QString( "[testHumanization] [customization] Unable to access note [%1]" )
				.arg( ii ) );
		}
			
	}

	//////////////////////////////////////////////////////////////////
	// Check whether deviations of the humanized/randomized properties
	// are indeed distributed as a Gaussian with mean zero and an
	// expected standard deviation.
	//
	// Switch back to pattern 1
	pAE->unlock();
	pCoreActionController->toggleGridCell( 0, 1 );
	pCoreActionController->toggleGridCell( 0, 0 );
	pAE->lock( RIGHT_HERE );

	auto checkHumanization = [&]( double fValue, std::vector<std::shared_ptr<Note>>* pNotes ) {

		if ( notesReference.size() != pNotes->size() ) {
			AudioEngineTests::throwException(
				QString( "[testHumanization] [humanization] Mismatching number of notes [%1 : %2]" )
				.arg( notesReference.size() )
				.arg( pNotes->size() ) );
		}

		auto checkDeviation = []( std::vector<float>* pDeviations, float fTargetSD, const QString& sContext ) {

			float fMean = std::accumulate( pDeviations->begin(), pDeviations->end(),
										   0.0, std::plus<float>() ) /
				static_cast<float>( pDeviations->size() );

			// Standard deviation
			auto compVariance = [&]( float fValue, float fElement ) {
				return fValue + ( fElement - fMean ) * ( fElement - fMean );
			};
			float fSD = std::sqrt( std::accumulate( pDeviations->begin(),
													pDeviations->end(),
													0.0, compVariance ) /
								   static_cast<float>( pDeviations->size() ) );

			// As we look at random numbers, the observed mean and
			// standard deviation won't match. But there should be no
			// more than 50% difference or something when wrong.
			if ( std::abs( fMean ) > std::abs( fSD ) * 0.5 ) {
				AudioEngineTests::throwException(
					QString( "[testHumanization] [%1] Mismatching mean [%2] != [0] with std. deviation [%3]" )
					.arg( sContext ).arg( fMean, 0, 'E', -1 )
					.arg( fSD, 0, 'E', -1 ) );
			}
			if ( std::abs( fSD - fTargetSD ) > fTargetSD * 0.5 ) {
				AudioEngineTests::throwException(
					QString( "[testHumanization] [%1] Mismatching standard deviation [%2] != [%3], diff [%4]" )
					.arg( sContext ).arg( fSD, 0, 'E', -1 )
					.arg( fTargetSD, 0, 'E', -1 )
					.arg( fSD - fTargetSD, 0, 'E', -1 ) );
			}
			
		};
		
		std::vector<float> deviationsPitch( notesReference.size() );
		std::vector<float> deviationsVelocity( notesReference.size() );
		std::vector<float> deviationsTiming( notesReference.size() );

		for ( int ii = 0; ii < pNotes->size(); ++ii ) {
			auto pNoteReference = notesReference[ ii ];
			auto pNoteHumanized = pNotes->at( ii );

			if ( pNoteReference != nullptr && pNoteHumanized != nullptr ) {
				deviationsVelocity[ ii ] =
					pNoteReference->get_velocity() - pNoteHumanized->get_velocity();
				deviationsPitch[ ii ] =
					pNoteReference->get_pitch() - pNoteHumanized->get_pitch(); 
				deviationsTiming[ ii ] =
					pNoteReference->getNoteStart() - pNoteHumanized->getNoteStart();
			} else {
				AudioEngineTests::throwException(
					QString( "[testHumanization] [swing] Unable to access note [%1]" )
					.arg( ii ) );
			}
		}

		// Within the audio engine every property has its own factor
		// multiplied with the humanization value set via the
		// GUI. With the latter ranging from 0 to 1 the factor
		// represent the maximum standard deviation available.
		checkDeviation( &deviationsVelocity,
						AudioEngine::fHumanizeVelocitySD * fValue, "velocity" );
		checkDeviation( &deviationsTiming,
						AudioEngine::fHumanizeTimingSD *
						AudioEngine::nMaxTimeHumanize * fValue, "timing" );
		checkDeviation( &deviationsPitch,
						AudioEngine::fHumanizePitchSD * fValue, "pitch" );
	};

	setHumanization( 0.2 );
	std::vector<std::shared_ptr<Note>> notesHumanizedWeak;
	getNotes( &notesHumanizedWeak );
	checkHumanization( 0.2, &notesHumanizedWeak );

	setHumanization( 0.8 );
	std::vector<std::shared_ptr<Note>> notesHumanizedStrong;
	getNotes( &notesHumanizedStrong );
	checkHumanization( 0.8, &notesHumanizedStrong );
	
	//////////////////////////////////////////////////////////////////
	// Check whether swing works.
	//
	// There is still discussion about HOW the swing should work and
	// whether the current implementation is valid. Therefore, this
	// test will only check whether setting this option alters at
	// least one note position

	setHumanization( 0 );
	setSwing( 0.5 );
	std::vector<std::shared_ptr<Note>> notesSwing;
	getNotes( &notesSwing );

	if ( notesReference.size() != notesSwing.size() ) {
		AudioEngineTests::throwException(
			QString( "[testHumanization] [swing] Mismatching number of notes [%1 : %2]" )
			.arg( notesReference.size() )
			.arg( notesSwing.size() ) );
	}

	bool bNoteAltered = false;
	for ( int ii = 0; ii < notesReference.size(); ++ii ) {
		auto pNoteReference = notesReference[ ii ];
		auto pNoteSwing = notesSwing[ ii ];

		if ( pNoteReference != nullptr && pNoteSwing != nullptr ) {
			if ( pNoteReference->getNoteStart() !=
				 pNoteSwing->getNoteStart() ) {
				bNoteAltered = true;
			}
		} else {
			AudioEngineTests::throwException(
				QString( "[testHumanization] [swing] Unable to access note [%1]" )
				.arg( ii ) );
		}
	}
	if ( ! bNoteAltered ) {
		AudioEngineTests::throwException( "[testHumanization] [swing] No notes affected." );
	}

	//////////////////////////////////////////////////////////////////

	pAE->setState( AudioEngine::State::Ready );
	pAE->unlock();
}
	
void AudioEngineTests::mergeQueues( std::vector<std::shared_ptr<Note>>* noteList, std::vector<std::shared_ptr<Note>> newNotes ) {
	bool bNoteFound;
	for ( const auto& newNote : newNotes ) {
		bNoteFound = false;
		// Check whether the notes is already present.
		for ( const auto& presentNote : *noteList ) {
			if ( newNote != nullptr && presentNote != nullptr ) {
				if ( newNote->match( presentNote.get() ) &&
					 newNote->get_position() == presentNote->get_position() &&
					 newNote->get_velocity() == presentNote->get_velocity() ) {
					bNoteFound = true;
				}
			}
		}

		if ( ! bNoteFound ) {
			noteList->push_back( std::make_shared<Note>(newNote.get()) );
		}
	}
}

// Used for the Sampler note queue
void AudioEngineTests::mergeQueues( std::vector<std::shared_ptr<Note>>* noteList, std::vector<Note*> newNotes ) {
	bool bNoteFound;
	for ( const auto& newNote : newNotes ) {
		bNoteFound = false;
		// Check whether the notes is already present.
		for ( const auto& presentNote : *noteList ) {
			if ( newNote != nullptr && presentNote != nullptr ) {
				if ( newNote->match( presentNote.get() ) &&
					 newNote->get_position() == presentNote->get_position() &&
					 newNote->get_velocity() == presentNote->get_velocity() ) {
					bNoteFound = true;
				}
			}
		}

		if ( ! bNoteFound ) {
			noteList->push_back( std::make_shared<Note>(newNote) );
		}
	}
}

void AudioEngineTests::checkTransportPosition( std::shared_ptr<TransportPosition> pPos, const QString& sContext ) {

	auto pHydrogen = Hydrogen::get_instance();
	auto pSong = pHydrogen->getSong();
	auto pAE = pHydrogen->getAudioEngine();

	double fCheckTickMismatch;
	const long long nCheckFrame =
		TransportPosition::computeFrameFromTick(
			pPos->getDoubleTick(), &fCheckTickMismatch );
	const double fCheckTick =
		TransportPosition::computeTickFromFrame( pPos->getFrame() );
	
	if ( abs( fCheckTick + fCheckTickMismatch - pPos->getDoubleTick() ) > 1e-9 ||
		 abs( fCheckTickMismatch - pPos->m_fTickMismatch ) > 1e-9 ||
		 nCheckFrame != pPos->getFrame() ) {
		AudioEngineTests::throwException(
			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" )
			.arg( pPos->toQString( "", true ) ).arg( nCheckFrame )
			.arg( fCheckTick, 0 , 'f', 9 ).arg( fCheckTickMismatch, 0 , 'f', 9 )
			.arg( fCheckTick + fCheckTickMismatch - pPos->getDoubleTick(), 0, 'E' )
			.arg( fCheckTickMismatch - pPos->m_fTickMismatch, 0, 'E' )
			.arg( nCheckFrame - pPos->getFrame() ).arg( sContext ) );
	}

	long nCheckPatternStartTick;
	const int nCheckColumn = pHydrogen->getColumnForTick(
		pPos->getTick(), pSong->isLoopEnabled(), &nCheckPatternStartTick );
	const long nTicksSinceSongStart = static_cast<long>(std::floor(
		std::fmod( pPos->getDoubleTick(), pAE->m_fSongSizeInTicks ) ));
	
	if ( pHydrogen->getMode() == Song::Mode::Song && pPos->getColumn() != -1 &&
		 ( nCheckColumn != pPos->getColumn() ||
		   ( nCheckPatternStartTick != pPos->getPatternStartTick() ) ||
		   ( nTicksSinceSongStart - nCheckPatternStartTick !=
			 pPos->getPatternTickPosition() ) ) ) {
		AudioEngineTests::throwException(
			QString( "[checkTransportPosition] [%7] [column or pattern tick mismatch]. current position: [%1], nCheckColumn: %2, nCheckPatternStartTick: %3, nCheckPatternTickPosition: %4, nTicksSinceSongStart: %5, pAE->m_fSongSizeInTicks: %6" )
			.arg( pPos->toQString( "", true ) ).arg( nCheckColumn )
			.arg( nCheckPatternStartTick )
			.arg( nTicksSinceSongStart - nCheckPatternStartTick )
			.arg( nTicksSinceSongStart ).arg( pAE->m_fSongSizeInTicks, 0, 'f' )
			.arg( sContext ) );
	}
}

void AudioEngineTests::checkAudioConsistency( const std::vector<std::shared_ptr<Note>> oldNotes,
											  const std::vector<std::shared_ptr<Note>> newNotes, 
											  const QString& sContext,
											  int nPassedFrames, bool bTestAudio,
											  float fPassedTicks ) {
	auto pHydrogen = Hydrogen::get_instance();
	auto pSong = pHydrogen->getSong();
	auto pAE = pHydrogen->getAudioEngine();
	auto pTransportPos = pAE->getTransportPosition();

	double fPassedFrames = static_cast<double>(nPassedFrames);
	const int nSampleRate = pHydrogen->getAudioOutput()->getSampleRate();
	
	int nNotesFound = 0;
	for ( const auto& ppNewNote : newNotes ) {
		for ( const auto& ppOldNote : oldNotes ) {
			if ( ppNewNote->match( ppOldNote.get() ) &&
				 ppNewNote->get_humanize_delay() == 
				 ppOldNote->get_humanize_delay() &&
				 ppNewNote->get_velocity() == ppOldNote->get_velocity() ) {
				++nNotesFound;

				if ( bTestAudio ) {
					// Check for consistency in the Sample position
					// advanced by the Sampler upon rendering.
					for ( int nn = 0; nn < ppNewNote->get_instrument()->get_components()->size(); nn++ ) {
						auto pSelectedLayer = ppOldNote->get_layer_selected( nn );
						
						// The frames passed during the audio
						// processing depends on the sample rate of
						// the driver and sample and has to be
						// adjusted in here. This is equivalent to the
						// question whether Sampler::renderNote() or
						// Sampler::renderNoteResample() was used.
						if ( ppOldNote->getSample( nn )->get_sample_rate() !=
							 nSampleRate ||
							 ppOldNote->get_total_pitch() != 0.0 ) {
							// In here we assume the layer pitch is zero.
							fPassedFrames = static_cast<double>(nPassedFrames) *
								Note::pitchToFrequency( ppOldNote->get_total_pitch() ) *
								static_cast<float>(ppOldNote->getSample( nn )->get_sample_rate()) /
								static_cast<float>(nSampleRate);
						}
						
						const int nSampleFrames =
							ppNewNote->get_instrument()->get_component( nn )
							->get_layer( pSelectedLayer->SelectedLayer )
							->get_sample()->get_frames();
						const double fExpectedFrames =
							std::min( static_cast<double>(pSelectedLayer->SamplePosition) +
									  fPassedFrames,
									  static_cast<double>(nSampleFrames) );
						if ( std::abs( ppNewNote->get_layer_selected( nn )->SamplePosition -
									   fExpectedFrames ) > 1 ) {
							AudioEngineTests::throwException(
								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" )
								.arg( ppOldNote->toQString( "", true ) )
								.arg( ppNewNote->toQString( "", true ) )
								.arg( fPassedFrames, 0, 'f' ).arg( sContext )
								.arg( nSampleFrames ).arg( fExpectedFrames, 0, 'f' )
								.arg( ppOldNote->getSample( nn )->get_sample_rate() )
								.arg( nSampleRate )
								.arg( ppNewNote->get_layer_selected( nn )->SamplePosition -
									  fExpectedFrames, 0, 'g', 30 ) );
						}
					}
				}
				else { // if ( bTestAudio )
					// Check whether changes in note start position
					// were properly applied in the note queue of the
					// audio engine.
					if ( ppNewNote->get_position() - fPassedTicks !=
						 ppOldNote->get_position() ) {
						AudioEngineTests::throwException(
							QString( "[checkAudioConsistency] [%5] glitch in note queue.\n\tPre: %1\n\tPost: %2\n\tfPassedTicks: %3, diff (new - passed - old): %4" )
							.arg( ppOldNote->toQString( "", true ) )
							.arg( ppNewNote->toQString( "", true ) )
							.arg( fPassedTicks )
							.arg( ppNewNote->get_position() - fPassedTicks -
								  ppOldNote->get_position() ).arg( sContext ) );
					}
				}
			}
		}
	}

	// If one of the note vectors is empty - especially the new notes
	// - we can not test anything. But such things might happen as we
	// try various sample sizes and all notes might be already played
	// back and flushed.
	if ( nNotesFound == 0 &&
		 oldNotes.size() > 0 &&
		 newNotes.size() > 0 ) {
		QString sMsg = QString( "[checkAudioConsistency] [%1] bad test design. No notes played back." )
			.arg( sContext );
		sMsg.append( "\nold notes:" );
		for ( auto const& nnote : oldNotes ) {
			sMsg.append( "\n" + nnote->toQString( "    ", true ) );
		}
		sMsg.append( "\nnew notes:" );
		for ( auto const& nnote : newNotes ) {
			sMsg.append( "\n" + nnote->toQString( "    ", true ) );
		}
		sMsg.append( QString( "\n\npTransportPos->getDoubleTick(): %1, pTransportPos->getFrame(): %2, nPassedFrames: %3, fPassedTicks: %4, pTransportPos->getTickSize(): %5" )
					 .arg( pTransportPos->getDoubleTick(), 0, 'f' )
					 .arg( pTransportPos->getFrame() )
					 .arg( nPassedFrames )
					 .arg( fPassedTicks, 0, 'f' )
					 .arg( pTransportPos->getTickSize(), 0, 'f' ) );
		sMsg.append( "\n\n notes in song:" );
		for ( auto const& nnote : pSong->getAllNotes() ) {
			sMsg.append( "\n" + nnote->toQString( "    ", true ) );
		}
		AudioEngineTests::throwException( sMsg );
	}
}

std::vector<std::shared_ptr<Note>> AudioEngineTests::copySongNoteQueue() {
	auto pAE = Hydrogen::get_instance()->getAudioEngine();
	std::vector<Note*> rawNotes;
	std::vector<std::shared_ptr<Note>> notes;
	for ( ; ! pAE->m_songNoteQueue.empty(); pAE->m_songNoteQueue.pop() ) {
		rawNotes.push_back( pAE->m_songNoteQueue.top() );
		notes.push_back( std::make_shared<Note>( pAE->m_songNoteQueue.top() ) );
	}

	for ( auto nnote : rawNotes ) {
		pAE->m_songNoteQueue.push( nnote );
	}

	return notes;
}

void AudioEngineTests::toggleAndCheckConsistency( int nToggleColumn, int nToggleRow, const QString& sContext ) {
	auto pHydrogen = Hydrogen::get_instance();
	auto pCoreActionController = pHydrogen->getCoreActionController();
	auto pSong = pHydrogen->getSong();
	auto pAE = pHydrogen->getAudioEngine();
	auto pSampler = pAE->getSampler();
	auto pTransportPos = pAE->getTransportPosition();
	
	const unsigned long nBufferSize = pHydrogen->getAudioOutput()->getBufferSize();

	pAE->updateNoteQueue( nBufferSize );
	pAE->processAudio( nBufferSize );
	pAE->incrementTransportPosition( nBufferSize );

	// Cache some stuff in order to compare it later on.
	long nOldSongSize;
	int nOldColumn;
	float fPrevTempo, fPrevTickSize;
	double fPrevTickStart, fPrevTickEnd;
	long long nPrevLeadLag;

	std::vector<std::shared_ptr<Note>> notesSamplerPreToggle,
		notesSamplerPostToggle, notesSamplerPostRolling;

	auto notesSongQueuePreToggle = AudioEngineTests::copySongNoteQueue();

	auto toggleAndCheck = [&]( const QString& sContext ) {
		notesSamplerPreToggle.clear();
		for ( const auto& ppNote : pSampler->getPlayingNotesQueue() ) {
			notesSamplerPreToggle.push_back( std::make_shared<Note>( ppNote ) );
		}

		nPrevLeadLag = pAE->computeTickInterval( &fPrevTickStart, &fPrevTickEnd,
												 nBufferSize );
		nOldSongSize = pSong->lengthInTicks();
		nOldColumn = pTransportPos->getColumn();
		fPrevTempo = pTransportPos->getBpm();
		fPrevTickSize = pTransportPos->getTickSize();
	
		pAE->unlock();
		pCoreActionController->toggleGridCell( nToggleColumn, nToggleRow );
		pAE->lock( RIGHT_HERE );

		const QString sNewContext =
			QString( "toggleAndCheckConsistency::toggleAndCheck : %1 : toggling (%2,%3)" )
			.arg( sContext ).arg( nToggleColumn ).arg( nToggleRow );

		// Check whether there was a change in song size
		const long nNewSongSize = pSong->lengthInTicks();
		if ( nNewSongSize == nOldSongSize ) {
			AudioEngineTests::throwException(
				QString( "[%1] no change in song size" ).arg( sNewContext ) );
		}

		// Check whether current frame and tick information are still
		// consistent.
		AudioEngineTests::checkTransportPosition( pTransportPos, sNewContext );

		// m_songNoteQueue have been updated properly.
		const auto notesSongQueuePostToggle = AudioEngineTests::copySongNoteQueue();
		AudioEngineTests::checkAudioConsistency(
			notesSongQueuePreToggle, notesSongQueuePostToggle,
			sNewContext + " : song queue", 0, false,
			pTransportPos->getTickOffsetSongSize() );

		// The post toggle state will become the pre toggle state during
		// the next toggling.
		notesSongQueuePreToggle = notesSongQueuePostToggle;

		notesSamplerPostToggle.clear();
		for ( const auto& ppNote : pSampler->getPlayingNotesQueue() ) {
			notesSamplerPostToggle.push_back( std::make_shared<Note>( ppNote ) );
		}
		AudioEngineTests::checkAudioConsistency(
			notesSamplerPreToggle, notesSamplerPostToggle,
			sNewContext + " : sampler queue", 0, true,
			pTransportPos->getTickOffsetSongSize() );

		// Column must be consistent. Unless the song length shrunk due to
		// the toggling and the previous column was located beyond the new
		// end (in which case transport will be reset to 0).
		if ( nOldColumn < pSong->getPatternGroupVector()->size() ) {
			// Transport was not reset to 0 - happens in most cases.

			if ( nOldColumn != pTransportPos->getColumn() &&
				 nOldColumn < pSong->getPatternGroupVector()->size() ) {
				AudioEngineTests::throwException(
					QString( "[%3] Column changed old: %1, new: %2" )
					.arg( nOldColumn ).arg( pTransportPos->getColumn() )
					.arg( sNewContext ) );
			}

			double fTickEnd, fTickStart;
			const long long nLeadLag =
				pAE->computeTickInterval( &fTickStart, &fTickEnd, nBufferSize );
			if ( std::abs( nLeadLag - nPrevLeadLag ) > 1 ) {
				AudioEngineTests::throwException(
					QString( "[%3] LeadLag should be constant since there should be change in tick size. old: %1, new: %2" )
					.arg( nPrevLeadLag ).arg( nLeadLag ).arg( sNewContext ) );
			}
			if ( std::abs( fTickStart - pTransportPos->getTickOffsetSongSize() - fPrevTickStart ) > 4e-3 ) {
				AudioEngineTests::throwException(
					QString( "[%5] Mismatch in the start of the tick interval handled by updateNoteQueue new [%1] != [%2] old+offset, old: %3, offset: %4" )
					.arg( fTickStart, 0, 'f' )
					.arg( fPrevTickStart + pTransportPos->getTickOffsetSongSize(), 0, 'f' )
					.arg( fPrevTickStart, 0, 'f' )
					.arg( pTransportPos->getTickOffsetSongSize(), 0, 'f' )
					.arg( sNewContext ) );
			}
			if ( std::abs( fTickEnd - pTransportPos->getTickOffsetSongSize() - fPrevTickEnd ) > 4e-3 ) {
				AudioEngineTests::throwException(
					QString( "[%5] Mismatch in the end of the tick interval handled by updateNoteQueue new [%1] != [%2] old+offset, old: %3, offset: %4" )
					.arg( fTickEnd, 0, 'f' )
					.arg( fPrevTickEnd + pTransportPos->getTickOffsetSongSize(), 0, 'f' )
					.arg( fPrevTickEnd, 0, 'f' )
					.arg( pTransportPos->getTickOffsetSongSize(), 0, 'f' )
					.arg( sNewContext ) );
			}
		}
		else if ( pTransportPos->getColumn() != 0 &&
				  nOldColumn >= pSong->getPatternGroupVector()->size() ) {
			AudioEngineTests::throwException(
				QString( "[%4] Column reset failed nOldColumn: %1, pTransportPos->getColumn() (new): %2, pSong->getPatternGroupVector()->size() (new): %3" )
				.arg( nOldColumn )
				.arg( pTransportPos->getColumn() )
				.arg( pSong->getPatternGroupVector()->size() )
				.arg( sNewContext ) );
		}
	
		// Now we emulate that playback continues without any new notes
		// being added and expect the rendering of the notes currently
		// played back by the Sampler to start off precisely where we
		// stopped before the song size change. New notes might still be
		// added due to the lookahead, so, we just check for the
		// processing of notes we already encountered.
		pAE->incrementTransportPosition( nBufferSize );
		pAE->processAudio( nBufferSize );

		// Update the end of the tick interval usually handled by
		// updateNoteQueue().
		double fTickEndRolling, fTickStartUnused;
		pAE->computeTickInterval( &fTickStartUnused, &fTickEndRolling, nBufferSize );
		
		pAE->incrementTransportPosition( nBufferSize );
		pAE->processAudio( nBufferSize );

		pAE->m_fLastTickEnd = fTickEndRolling;

		// Check whether tempo and tick size have not changed.
		if ( fPrevTempo != pTransportPos->getBpm() ||
			 fPrevTickSize != pTransportPos->getTickSize() ) {
			AudioEngineTests::throwException(
				QString( "[%1] tempo and ticksize are affected" )
				.arg( sNewContext ) );
		}

		notesSamplerPostRolling.clear();
		for ( const auto& ppNote : pSampler->getPlayingNotesQueue() ) {
			notesSamplerPostRolling.push_back( std::make_shared<Note>( ppNote ) );
		}
		AudioEngineTests::checkAudioConsistency(
			notesSamplerPostToggle, notesSamplerPostRolling,
			QString( "toggleAndCheckConsistency::toggleAndCheck : %1 : rolling after toggle (%2,%3)" )
			.arg( sContext ).arg( nToggleColumn ).arg( nToggleRow ),
			nBufferSize * 2, true );
	};

	// Toggle the grid cell.
	toggleAndCheck( sContext + " : 1. toggle" );

	// Toggle the same grid cell again.
	toggleAndCheck( sContext + " : 2. toggle" );
}

void AudioEngineTests::resetSampler( const QString& sContext ) {
	auto pHydrogen = Hydrogen::get_instance();
	auto pSong = pHydrogen->getSong();
	auto pAE = pHydrogen->getAudioEngine();
	auto pSampler = pAE->getSampler();
	auto pPref = Preferences::get_instance();

	// Larger number to account for both small buffer sizes and long
	// samples.
	const int nMaxCleaningCycles = 5000;
	int nn = 0;

	// Ensure the sampler is clean.
	while ( pSampler->isRenderingNotes() ) {
		pAE->processAudio( pPref->m_nBufferSize );
		pAE->incrementTransportPosition( pPref->m_nBufferSize );
		++nn;
		
		// {//DEBUG
		//  QString msg = QString( "[%1] nn: %2, note:" )
		// 	   .arg( sContext ).arg( nn );
		// 	auto pNoteQueue = pSampler->getPlayingNotesQueue();
		// 	if ( pNoteQueue.size() > 0 ) {
		// 		auto pNote = pNoteQueue[0];
		// 		if ( pNote != nullptr ) {
		// 			msg.append( pNote->toQString("", true ) );
		// 		} else {
		// 			msg.append( " nullptr" );
		// 		}
		// 		DEBUGLOG( msg );
		// 	}
		// }
		
		if ( nn > nMaxCleaningCycles ) {
			AudioEngineTests::throwException(
				QString( "[%1] Sampler is in weird state" )
				.arg( sContext ) );
		}
	}
	
	pAE->reset( false );
	pAE->m_fSongSizeInTicks = pSong->lengthInTicks();
}

void AudioEngineTests::throwException( const QString& sMsg ) {
	auto pHydrogen = Hydrogen::get_instance();
	auto pAE = pHydrogen->getAudioEngine();

	pAE->setState( AudioEngine::State::Ready );
	pAE->unlock();
	
	throw std::runtime_error( sMsg.toLocal8Bit().data() );
}

	
}; // namespace H2Core
