diff --git a/.appveyor.yml b/.appveyor.yml index 6edb970671..e1c9eb44dc 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -65,7 +65,6 @@ environment: OPENSSL_DIR: OpenSSL-Win64 LIBSSL: libssl-1_1-x64.dll LIBCRYPTO: libcrypto-1_1-x64.dll - QT_LOCK_REPO: 'mingw/x86_64/mingw-w64-x86_64' CHOCO_ARCH: PROGRAM_FILES: "Program Files" @@ -81,7 +80,6 @@ environment: OPENSSL_DIR: OpenSSL-Win32 LIBSSL: libssl-1_1.dll LIBCRYPTO: libcrypto-1_1.dll - QT_LOCK_REPO: 'mingw/mingw32/mingw-w64-i686' CHOCO_ARCH: --x86 PROGRAM_FILES: "Program Files (x86)" @@ -159,10 +157,17 @@ for: wait echo "=== Build cache usage is " $( du -h $cdir | tail -1 ) "===" - test_script: |- - TMPDIR=/tmp src/tests/tests --appveyor + TMPDIR=/tmp src/tests/tests --appveyor --output-file=testOutput.log + + res=$?; + if [ "$res" != "0" ]; then + # The tests failed. Upload all output to ease debugging. + appveyor PushArtifact testOutput.log + fi + + ( exit $res ) - cache: /Users/appveyor/cache_dir @@ -265,9 +270,16 @@ for: test_script: |- cd build - TMPDIR=/tmp src/tests/tests --appveyor + + TMPDIR=/tmp src/tests/tests --appveyor --output-file=testOutput.log + res=$? wait + + if [ "$res" != "0" ]; then + # The tests failed. Upload all output to ease debugging. + appveyor PushArtifact testOutput.log + fi echo "=== Build cache usage is " $( du -h $cdir | tail -1 ) "===" ( exit $res ) @@ -289,7 +301,7 @@ for: cmake --version g++ --version - choco install %CHOCO_ARCH% jack --version 1.9.17 -my + choco install %CHOCO_ARCH% -y jack REM *** Results are ignored since a dependency was not properly installed in 32 bit Windows. But the .dll files required are installed regardless, so we don't care.*** @@ -302,10 +314,10 @@ for: c:\msys64\usr\bin\pacman --noconfirm -S -q %MSYS_REPO%-portaudio c:\msys64\usr\bin\pacman --noconfirm -S -q %MSYS_REPO%-portmidi c:\msys64\usr\bin\pacman --noconfirm -S -q %MSYS_REPO%-libwinpthread-git - c:\msys64\usr\bin\pacman --noconfirm -S -q %MSYS_REPO%-qt5 + REM *** As of 2022-20-09 the package database refresh (-y) is required to get the newest qt package and to avoid a critical bug in less recent ones. In addition, the refresh _must_ take place _after_ installing libwinpthread (which otherwise fails). *** + c:\msys64\usr\bin\pacman --noconfirm -S -q -y %MSYS_REPO%-qt5 c:\msys64\usr\bin\pacman --noconfirm -S -q %MSYS_REPO%-ladspa-sdk c:\msys64\usr\bin\pacman --noconfirm -S -q %MSYS_REPO%-ccache - c:\msys64\usr\bin\pacman --noconfirm -U https://repo.msys2.org/%QT_LOCK_REPO%-qt5-tools-5.15.2-2-any.pkg.tar.zst ccache -M 256M ccache -s @@ -334,7 +346,12 @@ for: SET CORE_PATH=%cd%\src\core echo %CORE_PATH% set PATH=%CORE_PATH%;%PATH% - src\tests\tests.exe --appveyor || cmd /c "exit /b 1" + src\tests\tests.exe --appveyor --output-file=testOutput.log + + REM *** In case the test passed, we delete its output. If it fails, the + REM script won't reach this point and the output will be uploaded later on *** + IF %ERRORLEVEL% EQU 0 (del testOutput.log) + 7z a %APPVEYOR_BUILD_FOLDER%\testresults.zip %TEMP%\hydrogen || cmd /c "exit /b 1" if %APPVEYOR_REPO_BRANCH%==%ARTIFACT_BRANCH% appveyor PushArtifact %APPVEYOR_BUILD_FOLDER%\testresults.zip @@ -370,9 +387,11 @@ on_finish: - cmd: if %APPVEYOR_REPO_BRANCH%==%ARTIFACT_BRANCH% appveyor PushArtifact %APPVEYOR_BUILD_FOLDER%\build\CMakeCache.txt - cmd: if %APPVEYOR_REPO_BRANCH%==%ARTIFACT_BRANCH% appveyor PushArtifact %APPVEYOR_BUILD_FOLDER%\build\CMakeFiles\CMakeOutput.log - cmd: if %APPVEYOR_REPO_BRANCH%==%ARTIFACT_BRANCH% appveyor PushArtifact %APPVEYOR_BUILD_FOLDER%\build\CMakeFiles\CMakeError.log + + - cmd: if EXIST %APPVEYOR_BUILD_FOLDER%\build\testOutput.log appveyor PushArtifact %APPVEYOR_BUILD_FOLDER%\build\testOutput.log + - cmd: if %APPVEYOR_REPO_BRANCH%==%ARTIFACT_BRANCH% if not %job_name%==Windows32 appveyor PushArtifact %APPVEYOR_BUILD_FOLDER%\build\Hydrogen-1.2-win64.exe || cmd /c "exit /b 1" - cmd: if %APPVEYOR_REPO_BRANCH%==%ARTIFACT_BRANCH% if %job_name%==Windows32 appveyor PushArtifact %APPVEYOR_BUILD_FOLDER%\build\Hydrogen-1.2-win32.exe || cmd /c "exit /b 1" - - cmd: | if %APPVEYOR_REPO_BRANCH%==%ARTIFACT_BRANCH% curl -F file=@%APPVEYOR_BUILD_FOLDER%\build\test_installation.xml https://ci.appveyor.com/api/testresults/junit/%APPVEYOR_JOB_ID% diff --git a/.gitignore b/.gitignore index 6fe5bf0d74..0dc6d62289 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ *.kdev4 *~ +\#*\# +.\#* *.o *.cbp *.layout diff --git a/src/core/AudioEngine/AudioEngine.cpp b/src/core/AudioEngine/AudioEngine.cpp index 278151ff5f..5e02ceef86 100644 --- a/src/core/AudioEngine/AudioEngine.cpp +++ b/src/core/AudioEngine/AudioEngine.cpp @@ -21,6 +21,7 @@ */ #include +#include #ifdef WIN32 # include "core/Timehelper.h" @@ -42,6 +43,7 @@ #include #include #include +#include #include #include @@ -60,37 +62,14 @@ #include #include -#include // TODO: remove this line as soon as possible +#include #include -#include + #include -#include namespace H2Core { -const int AudioEngine::nMaxTimeHumanize = 2000; - -inline int randomValue( int max ) -{ - return rand() % max; -} - -inline float getGaussian( float z ) -{ - // gaussian distribution -- dimss - float x1, x2, w; - do { - x1 = 2.0 * ( ( ( float ) rand() ) / static_cast(RAND_MAX) ) - 1.0; - x2 = 2.0 * ( ( ( float ) rand() ) / static_cast(RAND_MAX) ) - 1.0; - w = x1 * x1 + x2 * x2; - } while ( w >= 1.0 ); - - w = sqrtf( ( -2.0 * logf( w ) ) / w ); - return x1 * w * z + 0.0; // tunable -} - - /** Gets the current time. * \return Current time obtained by gettimeofday()*/ inline timeval currentTime2() @@ -101,41 +80,32 @@ inline timeval currentTime2() } AudioEngine::AudioEngine() - : TransportInfo() - , m_pSampler( nullptr ) + : m_pSampler( nullptr ) , m_pSynth( nullptr ) , m_pAudioDriver( nullptr ) , m_pMidiDriver( nullptr ) , m_pMidiDriverOut( nullptr ) , m_state( State::Initialized ) , m_pMetronomeInstrument( nullptr ) - , m_nPatternStartTick( 0 ) - , m_nPatternTickPosition( 0 ) - , m_nPatternSize( MAX_NOTES ) , m_fSongSizeInTicks( 0 ) - , m_nRealtimeFrames( 0 ) + , m_nRealtimeFrame( 0 ) , m_fMasterPeak_L( 0.0f ) , m_fMasterPeak_R( 0.0f ) - , m_nColumn( -1 ) , m_nextState( State::Ready ) , m_fProcessTime( 0.0f ) , m_fLadspaTime( 0.0f ) , m_fMaxProcessTime( 0.0f ) , m_fNextBpm( 120 ) , m_pLocker({nullptr, 0, nullptr}) - , m_currentTickTime( {0,0}) - , m_fTickMismatch( 0 ) - , m_fLastTickIntervalEnd( -1 ) - , m_nFrameOffset( 0 ) - , m_fTickOffset( 0 ) + , m_fLastTickEnd( 0 ) + , m_bLookaheadApplied( false ) { - + m_pTransportPosition = std::make_shared( "Transport" ); + m_pQueuingPosition = std::make_shared( "Queuing" ); m_pSampler = new Sampler; m_pSynth = new Synth; - gettimeofday( &m_currentTickTime, nullptr ); - m_pEventQueue = EventQueue::get_instance(); srand( time( nullptr ) ); @@ -150,11 +120,8 @@ AudioEngine::AudioEngine() pCompo->set_layer(pLayer, 0); m_pMetronomeInstrument->get_components()->push_back( pCompo ); m_pMetronomeInstrument->set_is_metronome_instrument(true); - - m_pPlayingPatterns = new PatternList(); - m_pPlayingPatterns->setNeedsLock( true ); - m_pNextPatterns = new PatternList(); - m_pNextPatterns->setNeedsLock( true ); + m_pMetronomeInstrument->set_volume( + Preferences::get_instance()->m_fMetronomeVolume ); m_AudioProcessCallback = &audioEngine_process; @@ -175,16 +142,14 @@ AudioEngine::~AudioEngine() this->lock( RIGHT_HERE ); INFOLOG( "*** Hydrogen audio engine shutdown ***" ); - clearNoteQueue(); + clearNoteQueues(); - // change the current audio engine state setState( State::Uninitialized ); - delete m_pPlayingPatterns; - m_pPlayingPatterns = nullptr; - - delete m_pNextPatterns; - m_pNextPatterns = nullptr; + m_pTransportPosition->reset(); + m_pTransportPosition = nullptr; + m_pQueuingPosition->reset(); + m_pQueuingPosition = nullptr; m_pMetronomeInstrument = nullptr; @@ -194,7 +159,6 @@ AudioEngine::~AudioEngine() delete Effects::get_instance(); #endif -// delete Sequencer::get_instance(); delete m_pSampler; delete m_pSynth; } @@ -297,17 +261,13 @@ void AudioEngine::startPlayback() { INFOLOG( "" ); - // check current state if ( getState() != State::Ready ) { ERRORLOG( "Error the audio engine is not in State::Ready" ); return; } - // change the current audio engine state setState( State::Playing ); - - // The locking of the pattern editor only takes effect if the - // transport is rolling. + handleSelectedPattern(); } @@ -315,7 +275,6 @@ void AudioEngine::stopPlayback() { INFOLOG( "" ); - // check current state if ( getState() != State::Playing ) { ERRORLOG( QString( "Error the audio engine is not in State::Playing but [%1]" ) .arg( static_cast( getState() ) ) ); @@ -328,27 +287,26 @@ void AudioEngine::stopPlayback() void AudioEngine::reset( bool bWithJackBroadcast ) { const auto pHydrogen = Hydrogen::get_instance(); + clearNoteQueues(); + m_fMasterPeak_L = 0.0f; m_fMasterPeak_R = 0.0f; - setFrames( 0 ); - setTick( 0 ); - setColumn( -1 ); - m_nPatternStartTick = 0; - m_nPatternTickPosition = 0; - m_fTickMismatch = 0; - m_nFrameOffset = 0; - m_fTickOffset = 0; - m_fLastTickIntervalEnd = -1; - - updateBpmAndTickSize(); - - clearNoteQueue(); + m_fLastTickEnd = 0; + m_bLookaheadApplied = false; + + m_pTransportPosition->reset(); + m_pQueuingPosition->reset(); + + updateBpmAndTickSize( m_pTransportPosition ); + updateBpmAndTickSize( m_pQueuingPosition ); + + updatePlayingPatterns(); #ifdef H2CORE_HAVE_JACK if ( pHydrogen->hasJackTransport() && bWithJackBroadcast ) { - // Tell all other JACK clients to relocate as well. This has - // to be called after updateFrames(). + // Tell the JACK server to locate to the beginning as well + // (done in the next run of audioEngine_process()). static_cast( m_pAudioDriver )->locateTransport( 0 ); } #endif @@ -370,14 +328,6 @@ double AudioEngine::computeDoubleTickSize( const int nSampleRate, const float fB return fTickSize; } -long long AudioEngine::computeFrame( double fTick, float fTickSize ) { - return std::round( fTick * fTickSize ); -} - -double AudioEngine::computeTick( long long nFrame, float fTickSize ) { - return nFrame / fTickSize; -} - float AudioEngine::getElapsedTime() const { const auto pHydrogen = Hydrogen::get_instance(); @@ -387,41 +337,48 @@ float AudioEngine::getElapsedTime() const { return 0; } - return ( getFrames() - m_nFrameOffset )/ static_cast(pDriver->getSampleRate()); + return ( m_pTransportPosition->getFrame() - + m_pTransportPosition->getFrameOffsetTempo() )/ + static_cast(pDriver->getSampleRate()); } void AudioEngine::locate( const double fTick, bool bWithJackBroadcast ) { const auto pHydrogen = Hydrogen::get_instance(); - const auto pDriver = pHydrogen->getAudioOutput(); - - long long nNewFrame; // DEBUGLOG( QString( "fTick: %1" ).arg( fTick ) ); #ifdef H2CORE_HAVE_JACK // In case Hydrogen is using the JACK server to sync transport, it - // has to be up to the server to relocate to a different - // position. + // is up to the server to relocate to a different position. It + // does so after the current cycle of audioEngine_process() and we + // will pick it up at the beginning of the next one. if ( pHydrogen->hasJackTransport() && bWithJackBroadcast ) { - nNewFrame = computeFrameFromTick( fTick, &m_fTickMismatch ); + double fTickMismatch; + const long long nNewFrame = TransportPosition::computeFrameFromTick( + fTick, &fTickMismatch ); static_cast( m_pAudioDriver )->locateTransport( nNewFrame ); return; } #endif - reset( false ); - nNewFrame = computeFrameFromTick( fTick, &m_fTickMismatch ); + resetOffsets(); + m_fLastTickEnd = fTick; + const long long nNewFrame = TransportPosition::computeFrameFromTick( + fTick, &m_pTransportPosition->m_fTickMismatch ); + + updateTransportPosition( fTick, nNewFrame, m_pTransportPosition ); + m_pQueuingPosition->set( m_pTransportPosition ); - setFrames( nNewFrame ); - updateTransportPosition( fTick ); + handleTempoChange(); } void AudioEngine::locateToFrame( const long long nFrame ) { - const auto pHydrogen = Hydrogen::get_instance(); + + // DEBUGLOG( QString( "nFrame: %1" ).arg( nFrame ) ); - reset( false ); + resetOffsets(); - double fNewTick = computeTickFromFrame( nFrame ); + double fNewTick = TransportPosition::computeTickFromFrame( nFrame ); // As the tick mismatch is lost when converting a sought location // from ticks into frames, sending it to the JACK server, @@ -430,31 +387,43 @@ void AudioEngine::locateToFrame( const long long nFrame ) { // relocation. if ( std::fmod( fNewTick, std::floor( fNewTick ) ) >= 0.97 ) { INFOLOG( QString( "Computed tick [%1] will be rounded to [%2] in order to avoid glitches" ) - .arg( fNewTick ).arg( std::round( fNewTick ) ) ); + .arg( fNewTick, 0, 'E', -1 ).arg( std::round( fNewTick ) ) ); fNewTick = std::round( fNewTick ); } + m_fLastTickEnd = fNewTick; - // Important step to assure the tick mismatch is set and - // tick<->frame can be converted properly. - long long nNewFrame = computeFrameFromTick( fNewTick, &m_fTickMismatch ); - if ( nNewFrame != nFrame ) { - ERRORLOG( QString( "Something went wrong: nFrame: %1, nNewFrame: %2, fNewTick: %3, m_fTickMismatch: %4" ) - .arg( nFrame ) - .arg( nNewFrame ) - .arg( fNewTick ) - .arg( m_fTickMismatch ) ); - } - setFrames( nNewFrame ); - - updateTransportPosition( fNewTick ); + // Assure tick<->frame can be converted properly using mismatch. + const long long nNewFrame = TransportPosition::computeFrameFromTick( + fNewTick, &m_pTransportPosition->m_fTickMismatch ); + + updateTransportPosition( fNewTick, nNewFrame, m_pTransportPosition ); + m_pQueuingPosition->set( m_pTransportPosition ); - // While the locate function is wrapped by a caller in the + handleTempoChange(); + + // While the locate() function is wrapped by a caller in the // CoreActionController - which takes care of queuing the // relocation event - this function is only meant to be used in - // very specific circumstances and is not that nicely wrapped. + // very specific circumstances and has to queue it itself. EventQueue::get_instance()->push_event( EVENT_RELOCATION, 0 ); } +void AudioEngine::resetOffsets() { + clearNoteQueues(); + + m_fLastTickEnd = 0; + m_bLookaheadApplied = false; + + m_pTransportPosition->setFrameOffsetTempo( 0 ); + m_pTransportPosition->setTickOffsetQueuing( 0 ); + m_pTransportPosition->setTickOffsetSongSize( 0 ); + m_pTransportPosition->setLastLeadLagFactor( 0 ); + m_pQueuingPosition->setFrameOffsetTempo( 0 ); + m_pQueuingPosition->setTickOffsetQueuing( 0 ); + m_pQueuingPosition->setTickOffsetSongSize( 0 ); + m_pQueuingPosition->setLastLeadLagFactor( 0 ); +} + void AudioEngine::incrementTransportPosition( uint32_t nFrames ) { auto pSong = Hydrogen::get_instance()->getSong(); @@ -462,564 +431,254 @@ void AudioEngine::incrementTransportPosition( uint32_t nFrames ) { return; } - setFrames( getFrames() + nFrames ); + const long long nNewFrame = m_pTransportPosition->getFrame() + nFrames; + const double fNewTick = TransportPosition::computeTickFromFrame( nNewFrame ); + m_pTransportPosition->m_fTickMismatch = 0; - double fNewTick = computeTickFromFrame( getFrames() ); - m_fTickMismatch = 0; - - // DEBUGLOG( QString( "nFrames: %1, old frames: %2, getDoubleTick(): %3, newTick: %4, ticksize: %5" ) + // DEBUGLOG( QString( "nFrames: %1, old frame: %2, new frame: %3, old tick: %4, new tick: %5, ticksize: %6" ) // .arg( nFrames ) - // .arg( getFrames() - nFrames ) - // .arg( getDoubleTick(), 0, 'f' ) + // .arg( m_pTransportPosition->getFrame() ) + // .arg( nNewFrame ) + // .arg( m_pTransportPosition->getDoubleTick(), 0, 'f' ) // .arg( fNewTick, 0, 'f' ) - // .arg( getTickSize(), 0, 'f' ) ); + // .arg( m_pTransportPosition->getTickSize(), 0, 'f' ) ); + + updateTransportPosition( fNewTick, nNewFrame, m_pTransportPosition ); - updateTransportPosition( fNewTick ); + // We are not updating the queuing position in here. This will be + // done in updateNoteQueue(). } -void AudioEngine::updateTransportPosition( double fTick ) { +void AudioEngine::updateTransportPosition( double fTick, long long nFrame, std::shared_ptr pPos ) { const auto pHydrogen = Hydrogen::get_instance(); const auto pSong = pHydrogen->getSong(); - const auto pDriver = pHydrogen->getAudioOutput(); assert( pSong ); + + // WARNINGLOG( QString( "[Before] fTick: %1, nFrame: %2, pos: %3" ) + // .arg( fTick, 0, 'f' ) + // .arg( nFrame ) + // .arg( pPos->toQString( "", true ) ) ); - // WARNINGLOG( QString( "[Before] frame: %5, tick: %1, pTickPos: %2, pStartPos: %3, column: %4, provided ticks: %6" ) - // .arg( getDoubleTick(), 0, 'f' ) - // .arg( m_nPatternTickPosition ) - // .arg( m_nPatternStartTick ) - // .arg( m_nColumn ) - // .arg( getFrames() ) - // .arg( fTick, 0, 'f' ) ); - - // Update m_nPatternStartTick, m_nPatternTickPosition, and - // m_nPatternSize. if ( pHydrogen->getMode() == Song::Mode::Song ) { - updateSongTransportPosition( fTick ); + updateSongTransportPosition( fTick, nFrame, pPos ); } - else if ( pHydrogen->getMode() == Song::Mode::Pattern ) { - - // If the transport is rolling, pattern tick variables were - // already updated in the call to updateNoteQueue. - if ( getState() != State::Playing ) { - updatePatternTransportPosition( fTick ); - } + else { // Song::Mode::Pattern + updatePatternTransportPosition( fTick, nFrame, pPos ); } + + updateBpmAndTickSize( pPos ); - setTick( fTick ); - - updateBpmAndTickSize(); - - // WARNINGLOG( QString( "[After] frame: %5, tick: %1, pTickPos: %2, pStartPos: %3, column: %4, provided ticks: %6" ) - // .arg( getDoubleTick(), 0, 'f' ) - // .arg( m_nPatternTickPosition ) - // .arg( m_nPatternStartTick ) - // .arg( m_nColumn ) - // .arg( getFrames() ) - // .arg( fTick, 0, 'f' ) ); + // WARNINGLOG( QString( "[After] fTick: %1, nFrame: %2, pos: %3, frame: %4" ) + // .arg( fTick, 0, 'f' ) + // .arg( nFrame ) + // .arg( pPos->toQString( "", true ) ) + // .arg( pPos->getFrame() ) ); } -void AudioEngine::updatePatternTransportPosition( double fTick ) { +void AudioEngine::updatePatternTransportPosition( double fTick, long long nFrame, std::shared_ptr pPos ) { auto pHydrogen = Hydrogen::get_instance(); - // In selected pattern mode we update the pattern size _before_ - // checking the whether transport reached the end of a - // pattern. This way when switching from a shorter to one double - // its size transport will just continue till the end of the new, - // longer one. If we would not update pattern size, transport - // would be looped at half the length of the newly selected - // pattern, which looks like a glitch. - // - // The update of the playing pattern is done asynchronous in - // Hydrogen::setSelectedPatternNumber() and does not have to - // queried in here in each run. - // if ( pHydrogen->getPatternMode() == Song::PatternMode::Selected ) { - // updatePlayingPatterns( 0, fTick ); - // } - - // Transport went past the end of the pattern or Pattern mode - // was just activated. - if ( fTick >= static_cast(m_nPatternStartTick + m_nPatternSize) || - fTick < static_cast(m_nPatternStartTick) ) { - m_nPatternStartTick += - static_cast(std::floor( ( fTick - - static_cast(m_nPatternStartTick) ) / - static_cast(m_nPatternSize) )) * - m_nPatternSize; + pPos->setTick( fTick ); + pPos->setFrame( nFrame ); + + const double fPatternStartTick = + static_cast(pPos->getPatternStartTick()); + const int nPatternSize = pPos->getPatternSize(); + + if ( fTick >= fPatternStartTick + static_cast(nPatternSize) || + fTick < fPatternStartTick ) { + // Transport went past the end of the pattern or Pattern mode + // was just activated. + pPos->setPatternStartTick( pPos->getPatternStartTick() + + static_cast(std::floor( ( fTick - fPatternStartTick ) / + static_cast(nPatternSize) )) * + nPatternSize ); // In stacked pattern mode we will only update the playing - // patterns if the transport of the original pattern is - // looped. This way all patterns start fresh at the beginning. + // patterns if the transport of the original pattern is looped + // back to the beginning. This way all patterns start fresh. + // + // In selected pattern mode pattern change does occur + // asynchonically by user interaction. if ( pHydrogen->getPatternMode() == Song::PatternMode::Stacked ) { - // Updates m_nPatternSize. - updatePlayingPatterns( 0, fTick ); + updatePlayingPatternsPos( pPos ); } } - m_nPatternTickPosition = static_cast(std::floor( fTick )) - - m_nPatternStartTick; - if ( m_nPatternTickPosition > m_nPatternSize ) { - m_nPatternTickPosition = ( static_cast(std::floor( fTick )) - - m_nPatternStartTick ) % - m_nPatternSize; + long nPatternTickPosition = static_cast(std::floor( fTick )) - + pPos->getPatternStartTick(); + if ( nPatternTickPosition > nPatternSize ) { + nPatternTickPosition = ( static_cast(std::floor( fTick )) + - pPos->getPatternStartTick() ) % + nPatternSize; } + pPos->setPatternTickPosition( nPatternTickPosition ); } -void AudioEngine::updateSongTransportPosition( double fTick ) { +void AudioEngine::updateSongTransportPosition( double fTick, long long nFrame, std::shared_ptr pPos ) { - auto pHydrogen = Hydrogen::get_instance(); + // WARNINGLOG( QString( "[Before] fTick: %1, nFrame: %2, m_fSongSizeInTicks: %3, pos: %4" ) + // .arg( fTick, 0, 'f' ) + // .arg( nFrame ) + // .arg( m_fSongSizeInTicks, 0, 'f' ) + // .arg( pPos->toQString( "", true ) ) ); + + const auto pHydrogen = Hydrogen::get_instance(); const auto pSong = pHydrogen->getSong(); + pPos->setTick( fTick ); + pPos->setFrame( nFrame ); + if ( fTick < 0 ) { - ERRORLOG( QString( "Provided tick [%1] is negative!" ) + ERRORLOG( QString( "[%1] Provided tick [%2] is negative!" ) + .arg( pPos->getLabel() ) .arg( fTick, 0, 'f' ) ); return; } - - int nNewColumn = pHydrogen->getColumnForTick( std::floor( fTick ), - pSong->isLoopEnabled(), - &m_nPatternStartTick ); - - // While the current tick position is constantly increasing, - // m_nPatternStartTick is only defined between 0 and - // m_fSongSizeInTicks. We will take care of the looping next. - if ( fTick >= m_fSongSizeInTicks && - m_fSongSizeInTicks != 0 ) { - - m_nPatternTickPosition = - std::fmod( std::floor( fTick ) - m_nPatternStartTick, - m_fSongSizeInTicks ); + + int nNewColumn; + if ( pSong->getPatternGroupVector()->size() == 0 ) { + // There are no patterns in song. + pPos->setPatternStartTick( 0 ); + pPos->setPatternTickPosition( 0 ); + nNewColumn = 0; } else { - m_nPatternTickPosition = std::floor( fTick ) - m_nPatternStartTick; + long nPatternStartTick; + nNewColumn = pHydrogen->getColumnForTick( + std::floor( fTick ), pSong->isLoopEnabled(), &nPatternStartTick ); + pPos->setPatternStartTick( nPatternStartTick ); + + // While the current tick position is constantly increasing, + // m_nPatternStartTick is only defined between 0 and + // m_fSongSizeInTicks. We will take care of the looping next. + if ( fTick >= m_fSongSizeInTicks && m_fSongSizeInTicks != 0 ) { + pPos->setPatternTickPosition( + std::fmod( std::floor( fTick ) - nPatternStartTick, + m_fSongSizeInTicks ) ); + } + else { + pPos->setPatternTickPosition( std::floor( fTick ) - nPatternStartTick ); + } } - if ( m_nColumn != nNewColumn ) { - setColumn( nNewColumn ); - updatePlayingPatterns( nNewColumn, 0 ); + if ( pPos->getColumn() != nNewColumn ) { + pPos->setColumn( nNewColumn ); + + updatePlayingPatternsPos( pPos ); handleSelectedPattern(); } + + // WARNINGLOG( QString( "[After] fTick: %1, nFrame: %2, m_fSongSizeInTicks: %3, pos: %4, frame: %5" ) + // .arg( fTick, 0, 'f' ) + // .arg( nFrame ) + // .arg( m_fSongSizeInTicks, 0, 'f' ) + // .arg( pPos->toQString( "", true ) ) + // .arg( pPos->getFrame() ) ); + } -void AudioEngine::updateBpmAndTickSize() { +void AudioEngine::updateBpmAndTickSize( std::shared_ptr pPos ) { if ( ! ( m_state == State::Playing || m_state == State::Ready || m_state == State::Testing ) ) { return; } + auto pHydrogen = Hydrogen::get_instance(); auto pSong = pHydrogen->getSong(); - float fOldBpm = getBpm(); - float fNewBpm = getBpmAtColumn( pHydrogen->getAudioEngine()->getColumn() ); - if ( fNewBpm != getBpm() ) { - setBpm( fNewBpm ); + const float fOldBpm = pPos->getBpm(); + + const float fNewBpm = getBpmAtColumn( pPos->getColumn() ); + if ( fNewBpm != fOldBpm ) { + pPos->setBpm( fNewBpm ); EventQueue::get_instance()->push_event( EVENT_TEMPO_CHANGED, 0 ); } - float fOldTickSize = getTickSize(); - float fNewTickSize = AudioEngine::computeTickSize( static_cast(m_pAudioDriver->getSampleRate()), - getBpm(), pSong->getResolution() ); - - // DEBUGLOG(QString( "sample rate: %1, tick size: %2 -> %3, bpm: %4 -> %5" ) - // .arg( static_cast(m_pAudioDriver->getSampleRate())) - // .arg( fOldTickSize, 0, 'f' ) - // .arg( fNewTickSize, 0, 'f' ) - // .arg( fOldBpm, 0, 'f' ) - // .arg( getBpm(), 0, 'f' ) ); - + const float fOldTickSize = pPos->getTickSize(); + const float fNewTickSize = + AudioEngine::computeTickSize( static_cast(m_pAudioDriver->getSampleRate()), + fNewBpm, pSong->getResolution() ); // Nothing changed - avoid recomputing if ( fNewTickSize == fOldTickSize ) { return; } if ( fNewTickSize == 0 ) { - ERRORLOG( QString( "Something went wrong while calculating the tick size. [oldTS: %1, newTS: %2]" ) + ERRORLOG( QString( "[%1] Something went wrong while calculating the tick size. [oldTS: %2, newTS: %3]" ) + .arg( pPos->getLabel() ) .arg( fOldTickSize, 0, 'f' ).arg( fNewTickSize, 0, 'f' ) ); return; } - - setTickSize( fNewTickSize ); - - if ( ! pHydrogen->isTimelineEnabled() ) { - // If we deal with a single speed for the whole song, the frames - // since the beginning of the song are tempo-dependent and have to - // be recalculated. - long long nNewFrames = computeFrameFromTick( getDoubleTick(), &m_fTickMismatch ); - m_nFrameOffset = nNewFrames - getFrames() + m_nFrameOffset; - - // DEBUGLOG( QString( "old frame: %1, new frame: %2, tick: %3, old tick size: %4, new tick size: %5" ) - // .arg( getFrames() ).arg( nNewFrames ).arg( getDoubleTick(), 0, 'f' ) - // .arg( fOldTickSize, 0, 'f' ).arg( fNewTickSize, 0, 'f' ) ); - - setFrames( nNewFrames ); - - // In addition, all currently processed notes have to be - // updated to be still valid. - handleTempoChange(); - - } else if ( m_nFrameOffset != 0 ) { - // In case the frame offset was already set, we have to update - // it too in order to prevent inconsistencies in the transport - // and audio rendering when switching off the Timeline during - // playback, relocating, switching it on again, alter song - // size or sample rate, and switching it off again. I know, - // quite an edge case but still. - // If we deal with a single speed for the whole song, the frames - // since the beginning of the song are tempo-dependent and have to - // be recalculated. - long long nNewFrames = computeFrameFromTick( getDoubleTick(), - &m_fTickMismatch ); - m_nFrameOffset = nNewFrames - getFrames() + m_nFrameOffset; - } -} - -// This function uses the assumption that sample rate and resolution -// are constant over the whole song. -long long AudioEngine::computeFrameFromTick( const double fTick, double* fTickMismatch, int nSampleRate ) const { - - const auto pHydrogen = Hydrogen::get_instance(); - const auto pSong = pHydrogen->getSong(); - const auto pTimeline = pHydrogen->getTimeline(); - assert( pSong ); - - if ( nSampleRate == 0 ) { - nSampleRate = pHydrogen->getAudioOutput()->getSampleRate(); - } - const int nResolution = pSong->getResolution(); - - const double fTickSize = AudioEngine::computeDoubleTickSize( nSampleRate, - getBpm(), - nResolution ); - - if ( nSampleRate == 0 || nResolution == 0 ) { - ERRORLOG( "Not properly initialized yet" ); - *fTickMismatch = 0; - return 0; - } - - if ( fTick == 0 ) { - *fTickMismatch = 0; - return 0; - } - - const auto tempoMarkers = pTimeline->getAllTempoMarkers(); - - long long nNewFrames = 0; - if ( pHydrogen->isTimelineEnabled() && - ! ( tempoMarkers.size() == 1 && - pTimeline->isFirstTempoMarkerSpecial() ) ) { - - double fNewTick = fTick; - double fRemainingTicks = fTick; - double fNextTick, fPassedTicks = 0; - double fNextTickSize; - double fNewFrames = 0; - - int nColumns = pSong->getPatternGroupVector()->size(); - - while ( fRemainingTicks > 0 ) { - - for ( int ii = 1; ii <= tempoMarkers.size(); ++ii ) { - if ( ii == tempoMarkers.size() || - tempoMarkers[ ii ]->nColumn >= nColumns ) { - fNextTick = m_fSongSizeInTicks; - } else { - fNextTick = - static_cast(pHydrogen->getTickForColumn( tempoMarkers[ ii ]->nColumn ) ); - } - fNextTickSize = - AudioEngine::computeDoubleTickSize( nSampleRate, - tempoMarkers[ ii - 1 ]->fBpm, - nResolution ); - - if ( fRemainingTicks > ( fNextTick - fPassedTicks ) ) { - // The whole segment of the timeline covered by tempo - // marker ii is left of the current transport position. - fNewFrames += ( fNextTick - fPassedTicks ) * fNextTickSize; - - // DEBUGLOG( QString( "[segment] fTick: %1, fNewFrames: %2, nNextTick: %3, nRemainingTicks: %4, nPassedTicks: %5, fNextTickSize: %6, col: %7, bpm: %8, tick increment: %9, frame increment: %10" ) - // .arg( fTick, 0, 'f' ) - // .arg( fNewFrames, 0, 'f' ) - // .arg( fNextTick, 0, 'f' ) - // .arg( fRemainingTicks, 0, 'f' ) - // .arg( fPassedTicks, 0, 'f' ) - // .arg( fNextTickSize, 0, 'f' ) - // .arg( tempoMarkers[ ii - 1 ]->nColumn ) - // .arg( tempoMarkers[ ii - 1 ]->fBpm ) - // .arg( fNextTick - fPassedTicks, 0, 'f' ) - // .arg( ( fNextTick - fPassedTicks ) * fNextTickSize, 0, 'g', 30 ) - // ); - - fRemainingTicks -= fNextTick - fPassedTicks; - - fPassedTicks = fNextTick; - - } else { - // We are within this segment. - fNewFrames += fRemainingTicks * fNextTickSize; - - nNewFrames = static_cast( std::round( fNewFrames ) ); - if ( fRemainingTicks != ( fNextTick - fPassedTicks ) ) { - *fTickMismatch = ( fNewFrames - static_cast( nNewFrames ) ) / - fNextTickSize; - } else { - // We ended at the very tick containing a - // tempo marker. If the mismatch is negative, - // we round the tick to a higher value, we - // need to use the tick size of the next tempo - // marker. For positive values, the mismatch - // resides on "this" side of the tempo marker. - double fMismatchInFrames = - fNewFrames - static_cast( nNewFrames ); - if ( fMismatchInFrames < 0 ) { - // Get the tick size of the next tempo - // marker. - if ( ii < tempoMarkers.size() ) { - - fNextTickSize = - AudioEngine::computeDoubleTickSize( nSampleRate, - tempoMarkers[ ii ]->fBpm, - nResolution ); - } else { - fNextTickSize = - AudioEngine::computeDoubleTickSize( nSampleRate, - tempoMarkers[ 0 ]->fBpm, - nResolution ); - } - } - *fTickMismatch = fMismatchInFrames / - fNextTickSize; - } - - // DEBUGLOG( QString( "[end] fTick: %1, fNewFrames: %2, nNewFrames: %9, fTickMismatch: %10, nNextTick: %3, nRemainingTicks: %4, nPassedTicks: %5, fNextTickSize: %6, col: %7, bpm: %8, tick increment: %11, frame increment: %12" ) - // .arg( fTick, 0, 'f' ) - // .arg( fNewFrames, 0, 'g', 30 ) - // .arg( fNextTick, 0, 'f' ) - // .arg( fRemainingTicks, 0, 'f' ) - // .arg( fPassedTicks, 0, 'f' ) - // .arg( fNextTickSize, 0, 'f' ) - // .arg( tempoMarkers[ ii - 1 ]->nColumn ) - // .arg( tempoMarkers[ ii - 1 ]->fBpm ) - // .arg( nNewFrames ) - // .arg( *fTickMismatch, 0, 'g', 30 ) - // .arg( fRemainingTicks, 0, 'f' ) - // .arg( fRemainingTicks * fNextTickSize, 0, 'g', 30 ) - // ); - - fRemainingTicks -= fNewTick - fPassedTicks; - break; - } - } + // The lookahead in updateNoteQueue is tempo dependent (since it + // contains both tick and frame components). By resetting this + // variable we allow that the next one calculated to have + // arbitrary values. + pPos->setLastLeadLagFactor( 0 ); - if ( fRemainingTicks != 0 ) { - // The provided fTick is larger than the song. But, - // luckily, we just calculated the song length in - // frames (fNewFrames). - int nRepetitions = std::floor(fTick / m_fSongSizeInTicks); - double fSongSizeInFrames = fNewFrames; - - fNewFrames *= static_cast(nRepetitions); - fNewTick = std::fmod( fTick, m_fSongSizeInTicks ); - fRemainingTicks = fNewTick; - fPassedTicks = 0; - - // DEBUGLOG( QString( "[repeat] frames covered: %1, ticks covered: %2, ticks remaining: %3, nRepetitions: %4, fSongSizeInFrames: %5" ) - // .arg( fNewFrames, 0, 'g', 30 ) - // .arg( fTick - fNewTick, 0, 'g', 30 ) - // .arg( fRemainingTicks, 0, 'g', 30 ) - // .arg( nRepetitions ) - // .arg( fSongSizeInFrames, 0, 'g', 30 ) - // ); - - if ( std::isinf( fNewFrames ) || - static_cast(fNewFrames) > - std::numeric_limits::max() ) { - ERRORLOG( QString( "Provided ticks [%1] are too large." ).arg( fTick ) ); - return 0; - } - } - } - } else { - - // No Timeline but a single tempo for the whole song. - double fNewFrames = static_cast(fTick) * - fTickSize; - nNewFrames = static_cast( std::round( fNewFrames ) ); - *fTickMismatch = ( fNewFrames - static_cast(nNewFrames ) ) / - fTickSize; - - // DEBUGLOG(QString("[no-timeline] nNewFrames: %1, fTick: %2, fTickSize: %3, fTickMismatch: %4" ) - // .arg( nNewFrames ).arg( fTick, 0, 'f' ).arg( fTickSize, 0, 'f' ) - // .arg( *fTickMismatch, 0, 'g', 30 )); + // DEBUGLOG(QString( "[%1] [%7,%8] sample rate: %2, tick size: %3 -> %4, bpm: %5 -> %6" ) + // .arg( pPos->getLabel() ) + // .arg( static_cast(m_pAudioDriver->getSampleRate())) + // .arg( fOldTickSize, 0, 'f' ) + // .arg( fNewTickSize, 0, 'f' ) + // .arg( fOldBpm, 0, 'f' ) + // .arg( pPos->getBpm(), 0, 'f' ) + // .arg( pPos->getFrame() ) + // .arg( pPos->getDoubleTick(), 0, 'f' ) ); + + pPos->setTickSize( fNewTickSize ); + + calculateTransportOffsetOnBpmChange( pPos ); +} + +void AudioEngine::calculateTransportOffsetOnBpmChange( std::shared_ptr pPos ) { + + // If we deal with a single speed for the whole song, the frames + // since the beginning of the song are tempo-dependent and have to + // be recalculated. + const long long nNewFrame = + TransportPosition::computeFrameFromTick( pPos->getDoubleTick(), + &pPos->m_fTickMismatch ); + pPos->setFrameOffsetTempo( nNewFrame - pPos->getFrame() + + pPos->getFrameOffsetTempo() ); + + if ( m_bLookaheadApplied ) { + // if ( m_fLastTickEnd != 0 ) { + const long long nNewLookahead = + getLeadLagInFrames( pPos->getDoubleTick() ) + + AudioEngine::nMaxTimeHumanize + 1; + const double fNewTickEnd = TransportPosition::computeTickFromFrame( + nNewFrame + nNewLookahead ) + pPos->getTickMismatch(); + pPos->setTickOffsetQueuing( fNewTickEnd - m_fLastTickEnd ); + + // DEBUGLOG( QString( "[%1 : [%2] timeline] old frame: %3, new frame: %4, tick: %5, nNewLookahead: %6, pPos->getFrameOffsetTempo(): %7, pPos->getTickOffsetQueuing(): %8, fNewTickEnd: %9, m_fLastTickEnd: %10" ) + // .arg( pPos->getLabel() ) + // .arg( Hydrogen::get_instance()->isTimelineEnabled() ) + // .arg( pPos->getFrame() ) + // .arg( nNewFrame ) + // .arg( pPos->getDoubleTick(), 0, 'f' ) + // .arg( nNewLookahead ) + // .arg( pPos->getFrameOffsetTempo() ) + // .arg( pPos->getTickOffsetQueuing(), 0, 'f' ) + // .arg( fNewTickEnd, 0, 'f' ) + // .arg( m_fLastTickEnd, 0, 'f' ) + // ); } - - return nNewFrames; -} - -double AudioEngine::computeTickFromFrame( const long long nFrame, int nSampleRate ) const { - const auto pHydrogen = Hydrogen::get_instance(); - - if ( nFrame < 0 ) { - ERRORLOG( QString( "Provided frame [%1] must be non-negative" ).arg( nFrame ) ); - } - - const auto pSong = pHydrogen->getSong(); - const auto pTimeline = pHydrogen->getTimeline(); - assert( pSong ); - - if ( nSampleRate == 0 ) { - nSampleRate = pHydrogen->getAudioOutput()->getSampleRate(); - } - const int nResolution = pSong->getResolution(); - double fTick = 0; - - const double fTickSize = - AudioEngine::computeDoubleTickSize( nSampleRate, - getBpm(), - nResolution ); - - if ( nSampleRate == 0 || nResolution == 0 ) { - ERRORLOG( "Not properly initialized yet" ); - return fTick; - } - if ( nFrame == 0 ) { - return fTick; + // Happens when the Timeline was either toggled or tempo + // changed while the former was deactivated. + if ( pPos->getFrame() != nNewFrame ) { + pPos->setFrame( nNewFrame ); } - - const auto tempoMarkers = pTimeline->getAllTempoMarkers(); - - if ( pHydrogen->isTimelineEnabled() && - ! ( tempoMarkers.size() == 1 && - pTimeline->isFirstTempoMarkerSpecial() ) ) { - - // We are using double precision in here to avoid rounding - // errors. - double fTargetFrames = static_cast(nFrame); - double fPassedFrames = 0; - double fNextFrames = 0; - double fNextTicks, fPassedTicks = 0; - double fNextTickSize; - long long nRemainingFrames; - - int nColumns = pSong->getPatternGroupVector()->size(); - - while ( fPassedFrames < fTargetFrames ) { - - for ( int ii = 1; ii <= tempoMarkers.size(); ++ii ) { - - fNextTickSize = - AudioEngine::computeDoubleTickSize( nSampleRate, - tempoMarkers[ ii - 1 ]->fBpm, - nResolution ); - if ( ii == tempoMarkers.size() || - tempoMarkers[ ii ]->nColumn >= nColumns ) { - fNextTicks = m_fSongSizeInTicks; - } else { - fNextTicks = - static_cast(pHydrogen->getTickForColumn( tempoMarkers[ ii ]->nColumn )); - } - fNextFrames = (fNextTicks - fPassedTicks) * fNextTickSize; - - if ( fNextFrames < ( fTargetFrames - - fPassedFrames ) ) { - - // DEBUGLOG(QString( "[segment] nFrame: %1, fTick: %11, sampleRate: %2, tickSize: %3, nNextTicks: %5, fNextFrames: %6, col: %7, bpm: %8, nPassedTicks: %9, fPassedFrames: %10, tick increment: %12, frame increment: %13" ) - // .arg( nFrame ) - // .arg( nSampleRate ) - // .arg( fNextTickSize, 0, 'f' ) - // .arg( fNextTicks, 0, 'f' ) - // .arg( fNextFrames, 0, 'f' ) - // .arg( tempoMarkers[ ii -1 ]->nColumn ) - // .arg( tempoMarkers[ ii -1 ]->fBpm ) - // .arg( fPassedTicks, 0, 'f' ) - // .arg( fPassedFrames, 0, 'f' ) - // .arg( fTick, 0, 'f' ) - // .arg( fNextTicks - fPassedTicks, 0, 'f' ) - // .arg( (fNextTicks - fPassedTicks) * fNextTickSize, 0, 'g', 30 ) - // ); - - // The whole segment of the timeline covered by tempo - // marker ii is left of the transport position. - fTick += fNextTicks - fPassedTicks; - - fPassedFrames += fNextFrames; - fPassedTicks = fNextTicks; - } else { - // We are within this segment. - // We use a floor in here because only integers - // frames are supported. - double fNewTick = (fTargetFrames - fPassedFrames ) / - fNextTickSize; - - fTick += fNewTick; - - fPassedFrames = fTargetFrames; - - // DEBUGLOG(QString( "[end] nFrame: %1, fTick: %11, sampleRate: %2, tickSize: %3, nNextTicks: %5, fNextFrames: %6, col: %7, bpm: %8, nPassedTicks: %9, fPassedFrames: %10, tick increment: %12, frame increment: %13" ) - // .arg( nFrame ) - // .arg( nSampleRate ) - // .arg( fNextTickSize, 0, 'f' ) - // .arg( fNextTicks, 0, 'f' ) - // .arg( fNextFrames, 0, 'f' ) - // .arg( tempoMarkers[ ii -1 ]->nColumn ) - // .arg( tempoMarkers[ ii -1 ]->fBpm ) - // .arg( fPassedTicks, 0, 'f' ) - // .arg( fPassedFrames, 0, 'f' ) - // .arg( fTick, 0, 'f' ) - // .arg( fNewTick, 0, 'f' ) - // .arg( fNewTick * fNextTickSize, 0, 'g', 30 ) - // ); - - break; - } - } - - if ( fPassedFrames != fTargetFrames ) { - // The provided nFrame is larger than the song. But, - // luckily, we just calculated the song length in - // frames. - double fSongSizeInFrames = fPassedFrames; - int nRepetitions = std::floor(fTargetFrames / fSongSizeInFrames); - if ( m_fSongSizeInTicks * nRepetitions > - std::numeric_limits::max() ) { - ERRORLOG( QString( "Provided frames [%1] are too large." ).arg( nFrame ) ); - return 0; - } - fTick = m_fSongSizeInTicks * nRepetitions; - - fPassedFrames = static_cast(nRepetitions) * - fSongSizeInFrames; - fPassedTicks = 0; - - // DEBUGLOG( QString( "[repeat] frames covered: %1, frames remaining: %2, ticks covered: %3, nRepetitions: %4, fSongSizeInFrames: %5" ) - // .arg( fPassedFrames, 0, 'g', 30 ) - // .arg( fTargetFrames - fPassedFrames, 0, 'g', 30 ) - // .arg( fTick, 0, 'g', 30 ) - // .arg( nRepetitions ) - // .arg( fSongSizeInFrames, 0, 'g', 30 ) - // ); - - } - } - } else { - - // No Timeline. Constant tempo/tick size for the whole song. - fTick = static_cast(nFrame) / fTickSize; - - // DEBUGLOG(QString( "[no timeline] nFrame: %1, sampleRate: %2, tickSize: %3" ) - // .arg( nFrame ).arg( nSampleRate ).arg( fTickSize, 0, 'f' ) ); - - } - - return fTick; + handleTempoChange(); } void AudioEngine::clearAudioBuffers( uint32_t nFrames ) @@ -1028,7 +687,7 @@ void AudioEngine::clearAudioBuffers( uint32_t nFrames ) float *pBuffer_L, *pBuffer_R; // clear main out Left and Right - if ( m_pAudioDriver ) { + if ( m_pAudioDriver != nullptr ) { pBuffer_L = m_pAudioDriver->getOut_L(); pBuffer_R = m_pAudioDriver->getOut_R(); assert( pBuffer_L != nullptr && pBuffer_R != nullptr ); @@ -1040,7 +699,7 @@ void AudioEngine::clearAudioBuffers( uint32_t nFrames ) if ( Hydrogen::get_instance()->hasJackAudioDriver() ) { JackAudioDriver* pJackAudioDriver = static_cast(m_pAudioDriver); - if( pJackAudioDriver ) { + if ( pJackAudioDriver != nullptr ) { pJackAudioDriver->clearPerTrackAudioBuffers( nFrames ); } } @@ -1055,7 +714,7 @@ void AudioEngine::clearAudioBuffers( uint32_t nFrames ) Effects* pEffects = Effects::get_instance(); for ( unsigned i = 0; i < MAX_FX; ++i ) { // clear FX buffers LadspaFX* pFX = pEffects->getLadspaFX( i ); - if ( pFX ) { + if ( pFX != nullptr ) { assert( pFX->m_pBuffer_L ); assert( pFX->m_pBuffer_R ); memset( pFX->m_pBuffer_L, 0, nFrames * sizeof( float ) ); @@ -1136,7 +795,6 @@ AudioOutput* AudioEngine::createAudioDriver( const QString& sDriver ) // AudioEngine while being connected. m_pAudioDriver = pAudioDriver; - // change the current audio engine state if ( pSong != nullptr ) { setState( State::Ready ); } else { @@ -1186,14 +844,13 @@ void AudioEngine::startAudioDrivers() INFOLOG(""); Preferences *pPref = Preferences::get_instance(); - // check current state if ( getState() != State::Initialized ) { ERRORLOG( QString( "Audio engine is not in State::Initialized but [%1]" ) .arg( static_cast( getState() ) ) ); return; } - if ( m_pAudioDriver ) { // check if the audio m_pAudioDriver is still alive + if ( m_pAudioDriver ) { // check if audio driver is still alive ERRORLOG( "The audio driver is still alive" ); } if ( m_pMidiDriver ) { // check if midi driver is still alive @@ -1220,21 +877,17 @@ void AudioEngine::startAudioDrivers() } } - // If the audio driver could not be created, we resort to the - // NullDriver. if ( m_pAudioDriver == nullptr ) { ERRORLOG( QString( "Couldn't start audio driver [%1], falling back to NullDriver" ) .arg( sAudioDriver ) ); createAudioDriver( "NullDriver" ); } - // Lock both the AudioEngine and the audio output buffers. this->lock( RIGHT_HERE ); QMutexLocker mx(&m_MutexOutputPointer); if ( pPref->m_sMidiDriver == "ALSA" ) { #ifdef H2CORE_HAVE_ALSA - // Create MIDI driver AlsaMidiDriver *alsaMidiDriver = new AlsaMidiDriver(); m_pMidiDriverOut = alsaMidiDriver; m_pMidiDriver = alsaMidiDriver; @@ -1275,7 +928,6 @@ void AudioEngine::stopAudioDrivers() { INFOLOG( "" ); - // check current state if ( m_state == State::Playing ) { this->stopPlayback(); } @@ -1289,10 +941,8 @@ void AudioEngine::stopAudioDrivers() this->lock( RIGHT_HERE ); - // change the current audio engine state setState( State::Initialized ); - // delete MIDI driver if ( m_pMidiDriver != nullptr ) { m_pMidiDriver->close(); delete m_pMidiDriver; @@ -1300,7 +950,6 @@ void AudioEngine::stopAudioDrivers() m_pMidiDriverOut = nullptr; } - // delete audio driver if ( m_pAudioDriver != nullptr ) { m_pAudioDriver->disconnect(); QMutexLocker mx( &m_MutexOutputPointer ); @@ -1312,9 +961,6 @@ void AudioEngine::stopAudioDrivers() this->unlock(); } -/** - * Restart all audio and midi drivers. - */ void AudioEngine::restartAudioDrivers() { if ( m_pAudioDriver != nullptr ) { @@ -1337,30 +983,36 @@ float AudioEngine::getBpmAtColumn( int nColumn ) { auto pHydrogen = Hydrogen::get_instance(); auto pAudioEngine = pHydrogen->getAudioEngine(); + auto pSong = pHydrogen->getSong(); + + if ( pSong == nullptr ) { + WARNINGLOG( "no song set yet" ); + return MIN_BPM; + } - float fBpm = pAudioEngine->getBpm(); + float fBpm = pAudioEngine->getTransportPosition()->getBpm(); - // Check for a change in the current BPM. if ( pHydrogen->getJackTimebaseState() == JackAudioDriver::Timebase::Slave && pHydrogen->getMode() == Song::Mode::Song ) { - // Hydrogen is using the BPM broadcast by the JACK + // Hydrogen is using the BPM broadcasted by the JACK // server. This one does solely depend on external // applications and will NOT be stored in the Song. - float fJackMasterBpm = pHydrogen->getMasterBpm(); + const float fJackMasterBpm = pHydrogen->getMasterBpm(); if ( ! std::isnan( fJackMasterBpm ) && fBpm != fJackMasterBpm ) { fBpm = fJackMasterBpm; // DEBUGLOG( QString( "Tempo update by the JACK server [%1]").arg( fJackMasterBpm ) ); } - } else if ( pHydrogen->getSong()->getIsTimelineActivated() && + } + else if ( pSong->getIsTimelineActivated() && pHydrogen->getMode() == Song::Mode::Song ) { - float fTimelineBpm = pHydrogen->getTimeline()->getTempoAtColumn( nColumn ); + const float fTimelineBpm = pHydrogen->getTimeline()->getTempoAtColumn( nColumn ); if ( fTimelineBpm != fBpm ) { // DEBUGLOG( QString( "Set tempo to timeline value [%1]").arg( fTimelineBpm ) ); fBpm = fTimelineBpm; } - - } else { + } + else { // Change in speed due to user interaction with the BPM widget // or corresponding MIDI or OSC events. if ( pAudioEngine->getNextBpm() != fBpm ) { @@ -1406,30 +1058,34 @@ void AudioEngine::raiseError( unsigned nErrorCode ) } void AudioEngine::handleSelectedPattern() { - // Expects the AudioEngine being locked. - auto pHydrogen = Hydrogen::get_instance(); - auto pSong = pHydrogen->getSong(); + const auto pHydrogen = Hydrogen::get_instance(); + const auto pSong = pHydrogen->getSong(); + if ( pHydrogen->isPatternEditorLocked() && ( m_state == State::Playing || m_state == State::Testing ) ) { - int nColumn = m_nColumn; - if ( m_nColumn == -1 ) { - nColumn = 0; - } - - auto pPatternList = pSong->getPatternList(); - auto pColumn = ( *pSong->getPatternGroupVector() )[ nColumn ]; - + // Default value is used to deselect the current pattern in + // case none was found. int nPatternNumber = -1; - int nIndex; - for ( const auto& pattern : *pColumn ) { - nIndex = pPatternList->index( pattern ); + const int nColumn = std::max( m_pTransportPosition->getColumn(), 0 ); + if ( nColumn < (*pSong->getPatternGroupVector()).size() ) { + + const auto pPatternList = pSong->getPatternList(); + if ( pPatternList != nullptr ) { - if ( nIndex > nPatternNumber ) { - nPatternNumber = nIndex; + const auto pColumn = ( *pSong->getPatternGroupVector() )[ nColumn ]; + + int nIndex; + for ( const auto& pattern : *pColumn ) { + nIndex = pPatternList->index( pattern ); + + if ( nIndex > nPatternNumber ) { + nPatternNumber = nIndex; + } + } } } @@ -1442,35 +1098,36 @@ void AudioEngine::processPlayNotes( unsigned long nframes ) Hydrogen* pHydrogen = Hydrogen::get_instance(); std::shared_ptr pSong = pHydrogen->getSong(); - long long nFrames; + long long nFrame; if ( getState() == State::Playing || getState() == State::Testing ) { // Current transport position. - nFrames = getFrames(); + nFrame = m_pTransportPosition->getFrame(); } else { - // In case the playback is stopped and all realtime events, - // by e.g. MIDI or Hydrogen's virtual keyboard, we disregard - // tempo changes in the Timeline and pretend the current tick - // size is valid for all future notes. - nFrames = getRealtimeFrames(); + // In case the playback is stopped we pretend it is still + // rolling using the realtime ticks while disregarding tempo + // changes in the Timeline. This is important as we want to + // continue playing back notes in the sampler and process + // realtime events, by e.g. MIDI or Hydrogen's virtual + // keyboard. + nFrame = getRealtimeFrame(); } - // reading from m_songNoteQueue while ( !m_songNoteQueue.empty() ) { Note *pNote = m_songNoteQueue.top(); - long long nNoteStartInFrames = pNote->getNoteStart(); + const long long nNoteStartInFrames = pNote->getNoteStart(); - // DEBUGLOG( QString( "getDoubleTick(): %1, getFrames(): %2, nframes: %3, " ) - // .arg( getDoubleTick() ).arg( getFrames() ) - // .arg( nframes ).append( pNote->toQString( "", true ) ) ); + // DEBUGLOG( QString( "m_pTransportPosition->getDoubleTick(): %1, m_pTransportPosition->getFrame(): %2, nframes: %3, " ) + // .arg( m_pTransportPosition->getDoubleTick() ) + // .arg( m_pTransportPosition->getFrame() ) + // .arg( nframes ) + // .append( pNote->toQString( "", true ) ) ); + + if ( nNoteStartInFrames < nFrame + static_cast(nframes) ) { - if ( nNoteStartInFrames < - nFrames + static_cast(nframes) ) { - /* Check if the current note has probability != 1 - * If yes remove call random function to dequeue or not the note - */ float fNoteProbability = pNote->get_probability(); if ( fNoteProbability != 1. ) { + // Current note is skipped with a certain probability. if ( fNoteProbability < (float) rand() / (float) RAND_MAX ) { m_songNoteQueue.pop(); pNote->get_instrument()->dequeue(); @@ -1478,53 +1135,23 @@ void AudioEngine::processPlayNotes( unsigned long nframes ) } } - if ( pSong->getHumanizeVelocityValue() != 0 ) { - float random = pSong->getHumanizeVelocityValue() * getGaussian( 0.2 ); - pNote->set_velocity( - pNote->get_velocity() - + ( random - - ( pSong->getHumanizeVelocityValue() / 2.0 ) ) - ); - if ( pNote->get_velocity() > 1.0 ) { - pNote->set_velocity( 1.0 ); - } else if ( pNote->get_velocity() < 0.0 ) { - pNote->set_velocity( 0.0 ); - } - } - - // Offset + Random Pitch ;) - float fPitch = pNote->get_pitch() + pNote->get_instrument()->get_pitch_offset(); - /* Check if the current instrument has random pitch factor != 0. - * If yes add a gaussian perturbation to the pitch - */ - float fRandomPitchFactor = pNote->get_instrument()->get_random_pitch_factor(); - if ( fRandomPitchFactor != 0. ) { - fPitch += getGaussian( 0.4 ) * fRandomPitchFactor; - } - pNote->set_pitch( fPitch ); - /* * Check if the current instrument has the property "Stop-Note" set. * If yes, a NoteOff note is generated automatically after each note. */ - auto noteInstrument = pNote->get_instrument(); - if ( noteInstrument->is_stop_notes() ){ - Note *pOffNote = new Note( noteInstrument, - 0.0, - 0.0, - 0.0, - -1, - 0 ); + auto pNoteInstrument = pNote->get_instrument(); + if ( pNoteInstrument->is_stop_notes() ){ + Note *pOffNote = new Note( pNoteInstrument ); pOffNote->set_note_off( true ); - pHydrogen->getAudioEngine()->getSampler()->noteOn( pOffNote ); + m_pSampler->noteOn( pOffNote ); delete pOffNote; } m_pSampler->noteOn( pNote ); - m_songNoteQueue.pop(); // rimuovo la nota dalla lista di note + m_songNoteQueue.pop(); pNote->get_instrument()->dequeue(); - // raise noteOn event - int nInstrument = pSong->getInstrumentList()->index( pNote->get_instrument() ); + + const int nInstrument = pSong->getInstrumentList()->index( pNote->get_instrument() ); if( pNote->get_note_off() ){ delete pNote; } @@ -1542,16 +1169,15 @@ void AudioEngine::processPlayNotes( unsigned long nframes ) } } -void AudioEngine::clearNoteQueue() +void AudioEngine::clearNoteQueues() { - // delete all copied notes in the song notes queue - while (!m_songNoteQueue.empty()) { + // delete all copied notes in the note queues + while ( !m_songNoteQueue.empty() ) { m_songNoteQueue.top()->get_instrument()->dequeue(); delete m_songNoteQueue.top(); m_songNoteQueue.pop(); } - // delete all copied notes in the midi notes queue for ( unsigned i = 0; i < m_midiNoteQueue.size(); ++i ) { delete m_midiNoteQueue[i]; } @@ -1563,14 +1189,11 @@ int AudioEngine::audioEngine_process( uint32_t nframes, void* /*arg*/ ) AudioEngine* pAudioEngine = Hydrogen::get_instance()->getAudioEngine(); timeval startTimeval = currentTime2(); - // Resetting all audio output buffers with zeros. pAudioEngine->clearAudioBuffers( nframes ); // Calculate maximum time to wait for audio engine lock. Using the // last calculated processing time as an estimate of the expected - // processing time for this frame, the amount of slack time that - // we can afford to wait is: m_fMaxProcessTime - m_fProcessTime. - + // processing time for this frame. float sampleRate = static_cast(pAudioEngine->m_pAudioDriver->getSampleRate()); pAudioEngine->m_fMaxProcessTime = 1000.0 / ( sampleRate / nframes ); float fSlackTime = pAudioEngine->m_fMaxProcessTime - pAudioEngine->m_fProcessTime; @@ -1614,17 +1237,13 @@ int AudioEngine::audioEngine_process( uint32_t nframes, void* /*arg*/ ) // designed that way) #ifdef H2CORE_HAVE_JACK if ( Hydrogen::get_instance()->hasJackTransport() ) { - // Compares the current transport state, speed in bpm, and - // transport position with a query request to the JACK - // server. It will only overwrite the transport state, if - // the transport position was changed by the user by - // e.g. clicking on the timeline. - static_cast( pHydrogen->getAudioOutput() )->updateTransportInfo(); + static_cast( pHydrogen->getAudioOutput() )->updateTransportPosition(); } #endif // Check whether the tempo was changed. - pAudioEngine->updateBpmAndTickSize(); + pAudioEngine->updateBpmAndTickSize( pAudioEngine->m_pTransportPosition ); + pAudioEngine->updateBpmAndTickSize( pAudioEngine->m_pQueuingPosition ); // Update the state of the audio engine depending on whether it // was started or stopped by the user. @@ -1633,7 +1252,7 @@ int AudioEngine::audioEngine_process( uint32_t nframes, void* /*arg*/ ) pAudioEngine->startPlayback(); } - pAudioEngine->setRealtimeFrames( pAudioEngine->getFrames() ); + pAudioEngine->setRealtimeFrame( pAudioEngine->m_pTransportPosition->getFrame() ); } else { if ( pAudioEngine->getState() == State::Playing ) { pAudioEngine->stopPlayback(); @@ -1641,10 +1260,10 @@ int AudioEngine::audioEngine_process( uint32_t nframes, void* /*arg*/ ) // go ahead and increment the realtimeframes by nFrames // to support our realtime keyboard and midi event timing - pAudioEngine->setRealtimeFrames( pAudioEngine->getRealtimeFrames() + + pAudioEngine->setRealtimeFrame( pAudioEngine->getRealtimeFrame() + static_cast(nframes) ); } - + // always update note queue.. could come from pattern or realtime input // (midi, keyboard) int nResNoteQueue = pAudioEngine->updateNoteQueue( nframes ); @@ -1656,14 +1275,16 @@ int AudioEngine::audioEngine_process( uint32_t nframes, void* /*arg*/ ) if ( dynamic_cast(pAudioEngine->m_pAudioDriver) != nullptr ) { ___INFOLOG( "End of song." ); - + + // TODO: This part of the code might not be reached + // anymore. + pAudioEngine->unlock(); return 1; // kill the audio AudioDriver thread } } pAudioEngine->processAudio( nframes ); - // increment the transport position if ( pAudioEngine->getState() == AudioEngine::State::Playing ) { pAudioEngine->incrementTransportPosition( nframes ); } @@ -1683,7 +1304,7 @@ int AudioEngine::audioEngine_process( uint32_t nframes, void* /*arg*/ ) ___WARNINGLOG( QString( "Ladspa process time = %1" ).arg( fLadspaTime ) ); ___WARNINGLOG( "------------" ); ___WARNINGLOG( "" ); - // raise xRun event + EventQueue::get_instance()->push_event( EVENT_XRUN, -1 ); } #endif @@ -1697,15 +1318,13 @@ void AudioEngine::processAudio( uint32_t nFrames ) { auto pSong = Hydrogen::get_instance()->getSong(); - // play all notes processPlayNotes( nFrames ); float *pBuffer_L = m_pAudioDriver->getOut_L(), *pBuffer_R = m_pAudioDriver->getOut_R(); assert( pBuffer_L != nullptr && pBuffer_R != nullptr ); - // SAMPLER - getSampler()->process( nFrames, pSong ); + getSampler()->process( nFrames ); float* out_L = getSampler()->m_pMainOut_L; float* out_R = getSampler()->m_pMainOut_R; for ( unsigned i = 0; i < nFrames; ++i ) { @@ -1713,7 +1332,6 @@ void AudioEngine::processAudio( uint32_t nFrames ) { pBuffer_R[ i ] += out_R[ i ]; } - // SYNTH getSynth()->process( nFrames ); out_L = getSynth()->m_pOut_L; out_R = getSynth()->m_pOut_R; @@ -1725,7 +1343,6 @@ void AudioEngine::processAudio( uint32_t nFrames ) { timeval ladspaTime_start = currentTime2(); #ifdef H2CORE_HAVE_LADSPA - // Process LADSPA FX for ( unsigned nFX = 0; nFX < MAX_FX; ++nFX ) { LadspaFX *pFX = Effects::get_instance()->getLadspaFX( nFX ); if ( ( pFX ) && ( pFX->isEnabled() ) ) { @@ -1759,7 +1376,6 @@ void AudioEngine::processAudio( uint32_t nFrames ) { ( ladspaTime_end.tv_sec - ladspaTime_start.tv_sec ) * 1000.0 + ( ladspaTime_end.tv_usec - ladspaTime_start.tv_usec ) / 1000.0; - // update master peaks float val_L, val_R; for ( unsigned i = 0; i < nFrames; ++i ) { val_L = pBuffer_L[i]; @@ -1813,43 +1429,36 @@ void AudioEngine::setNextBpm( float fNextBpm ) { void AudioEngine::setSong( std::shared_ptr pNewSong ) { + auto pHydrogen = Hydrogen::get_instance(); + INFOLOG( QString( "Set song: %1" ).arg( pNewSong->getName() ) ); this->lock( RIGHT_HERE ); - // check current state - // should be set by removeSong called earlier if ( getState() != State::Prepared ) { ERRORLOG( QString( "Error the audio engine is not in State::Prepared but [%1]" ) .arg( static_cast( getState() ) ) ); } - // setup LADSPA FX if ( m_pAudioDriver != nullptr ) { setupLadspaFX(); } - // find the first pattern and set as current since we start in - // pattern mode. - if ( pNewSong->getPatternList()->size() > 0 ) { - m_pPlayingPatterns->add( pNewSong->getPatternList()->get( 0 ) ); - m_nPatternSize = m_pPlayingPatterns->longest_pattern_length(); - } else { - m_nPatternSize = MAX_NOTES; - } + // Reset (among other things) the transport position. This causes + // the locate() call below to update the playing patterns. + reset( false ); - Hydrogen::get_instance()->renameJackPorts( pNewSong ); + pHydrogen->renameJackPorts( pNewSong ); m_fSongSizeInTicks = static_cast( pNewSong->lengthInTicks() ); - // change the current audio engine state setState( State::Ready ); setNextBpm( pNewSong->getBpm() ); - // Will adapt the audio engine to the song's BPM. + // Will also adapt the audio engine to the new song's BPM. locate( 0 ); - Hydrogen::get_instance()->setTimeline( pNewSong->getTimeline() ); - Hydrogen::get_instance()->getTimeline()->activate(); + pHydrogen->setTimeline( pNewSong->getTimeline() ); + pHydrogen->getTimeline()->activate(); this->unlock(); } @@ -1863,7 +1472,6 @@ void AudioEngine::removeSong() this->stopPlayback(); } - // check current state if ( getState() != State::Ready ) { ERRORLOG( QString( "Error the audio engine is not in State::Ready but [%1]" ) .arg( static_cast( getState() ) ) ); @@ -1871,12 +1479,9 @@ void AudioEngine::removeSong() return; } - m_pPlayingPatterns->clear(); - m_pNextPatterns->clear(); - clearNoteQueue(); m_pSampler->stopPlayingNotes(); + reset(); - // change the current audio engine state setState( State::Prepared ); this->unlock(); } @@ -1891,226 +1496,343 @@ void AudioEngine::updateSongSize() { return; } - if ( m_pPlayingPatterns->size() > 0 ) { - m_nPatternSize = m_pPlayingPatterns->longest_pattern_length(); - } else { - m_nPatternSize = MAX_NOTES; - } - - EventQueue::get_instance()->push_event( EVENT_SONG_SIZE_CHANGED, 0 ); + auto updatePatternSize = []( std::shared_ptr pPos ) { + if ( pPos->getPlayingPatterns()->size() > 0 ) { + pPos->setPatternSize( pPos->getPlayingPatterns()->longest_pattern_length() ); + } else { + pPos->setPatternSize( MAX_NOTES ); + } + }; + updatePatternSize( m_pTransportPosition ); + updatePatternSize( m_pQueuingPosition ); if ( pHydrogen->getMode() == Song::Mode::Pattern ) { + m_fSongSizeInTicks = static_cast( pSong->lengthInTicks() ); + + EventQueue::get_instance()->push_event( EVENT_SONG_SIZE_CHANGED, 0 ); return; } - bool bEndOfSongReached = false; - - double fNewSongSizeInTicks = static_cast( pSong->lengthInTicks() ); + // Expected behavior: + // - changing any part of the song except of the pattern currently + // playing shouldn't affect transport position + // - the current transport position is defined as current column + + // current pattern tick position + // - there shouldn't be a difference in behavior whether the song + // was already looped or not + const double fNewSongSizeInTicks = static_cast( pSong->lengthInTicks() ); + + // Indicates that the song contains no patterns (before or after + // song size did change). + const bool bEmptySong = + m_fSongSizeInTicks == 0 || fNewSongSizeInTicks == 0; + + double fNewStrippedTick, fRepetitions; + if ( m_fSongSizeInTicks != 0 ) { + // Strip away all repetitions when in loop mode but keep their + // number. nPatternStartTick and nColumn are only defined + // between 0 and fSongSizeInTicks. + fNewStrippedTick = std::fmod( m_pTransportPosition->getDoubleTick(), + m_fSongSizeInTicks ); + fRepetitions = + std::floor( m_pTransportPosition->getDoubleTick() / m_fSongSizeInTicks ); + } + else { + // No patterns in song prior to song size change. + fNewStrippedTick = m_pTransportPosition->getDoubleTick(); + fRepetitions = 0; + } + + const int nOldColumn = m_pTransportPosition->getColumn(); - // WARNINGLOG( QString( "[Before] frame: %1, bpm: %2, tickSize: %3, column: %4, tick: %5, mod(tick): %6, pTickPos: %7, pStartPos: %8, m_fLastTickIntervalEnd: %9, m_fSongSizeInTicks: %10" ) - // .arg( getFrames() ).arg( getBpm() ) - // .arg( getTickSize(), 0, 'f' ) - // .arg( m_nColumn ) - // .arg( getDoubleTick(), 0, 'g', 30 ) - // .arg( std::fmod( getDoubleTick(), m_fSongSizeInTicks ), 0, 'g', 30 ) - // .arg( m_nPatternTickPosition ) - // .arg( m_nPatternStartTick ) - // .arg( m_fLastTickIntervalEnd ) + // WARNINGLOG( QString( "[Before] fNewStrippedTick: %1, fRepetitions: %2, m_fSongSizeInTicks: %3, fNewSongSizeInTicks: %4, transport: %5, queuing: %6" ) + // .arg( fNewStrippedTick, 0, 'f' ) + // .arg( fRepetitions ) // .arg( m_fSongSizeInTicks ) + // .arg( fNewSongSizeInTicks ) + // .arg( m_pTransportPosition->toQString( "", true ) ) + // .arg( m_pQueuingPosition->toQString( "", true ) ) // ); - // Strip away all repetitions when in loop mode but keep their - // number. m_nPatternStartTick and m_nColumn are only defined - // between 0 and m_fSongSizeInTicks. - double fNewTick = std::fmod( getDoubleTick(), m_fSongSizeInTicks ); - double fRepetitions = - std::floor( getDoubleTick() / m_fSongSizeInTicks ); - m_fSongSizeInTicks = fNewSongSizeInTicks; - // Expected behavior: - // - changing any part of the song except of the pattern currently - // play shouldn't affect the transport position - // - there shouldn't be a difference in behavior when the song was - // looped at least once - // - this internal compensation in the transport position will - // only be propagated to external audio servers, like JACK, once - // a relocation takes place. This temporal loss of sync is done - // to avoid audible glitches when e.g. toggling a pattern or - // scrolling the pattern length spin boxes. A general intuition - // for a loss of synchronization when just changing song parts - // in one application can probably be expected. - // - // We strive for consistency in audio playback and make both the - // current column/pattern and the transport position within the - // pattern invariant in this transformation. - long nNewPatternStartTick = pHydrogen->getTickForColumn( getColumn() ); - - if ( nNewPatternStartTick == -1 ) { - bEndOfSongReached = true; + auto endOfSongReached = [&](){ + if ( getState() == State::Playing ) { + stop(); + stopPlayback(); + } + locate( 0 ); + + // WARNINGLOG( QString( "[End of song reached] fNewStrippedTick: %1, fRepetitions: %2, m_fSongSizeInTicks: %3, fNewSongSizeInTicks: %4, transport: %5, queuing: %6" ) + // .arg( fNewStrippedTick, 0, 'f' ) + // .arg( fRepetitions ) + // .arg( m_fSongSizeInTicks ) + // .arg( fNewSongSizeInTicks ) + // .arg( m_pTransportPosition->toQString( "", true ) ) + // .arg( m_pQueuingPosition->toQString( "", true ) ) + // ); + + EventQueue::get_instance()->push_event( EVENT_SONG_SIZE_CHANGED, 0 ); + }; + + if ( nOldColumn >= pSong->getPatternGroupVector()->size() && + pSong->getLoopMode() != Song::LoopMode::Enabled ) { + // Old column exceeds the new song size. + endOfSongReached(); + return; + } + + + const long nNewPatternStartTick = pHydrogen->getTickForColumn( nOldColumn ); + + if ( nNewPatternStartTick == -1 && + pSong->getLoopMode() != Song::LoopMode::Enabled ) { + // Failsave in case old column exceeds the new song size. + endOfSongReached(); + return; } - if ( nNewPatternStartTick != m_nPatternStartTick ) { + if ( nNewPatternStartTick != m_pTransportPosition->getPatternStartTick() && + ! bEmptySong ) { + // A pattern prior to the current position was toggled, + // enlarged, or shrunk. We need to compensate this in order to + // keep the current pattern tick position constant. - // DEBUGLOG( QString( "[start tick mismatch] old: %1, new: %2" ) - // .arg( m_nPatternStartTick ) + // DEBUGLOG( QString( "[nPatternStartTick mismatch] old: %1, new: %2" ) + // .arg( m_pTransportPosition->getPatternStartTick() ) // .arg( nNewPatternStartTick ) ); - fNewTick += - static_cast(nNewPatternStartTick - m_nPatternStartTick); + fNewStrippedTick += + static_cast(nNewPatternStartTick - + m_pTransportPosition->getPatternStartTick()); + } + +#ifdef H2CORE_HAVE_DEBUG + const long nNewPatternTickPosition = + static_cast(std::floor( fNewStrippedTick )) - nNewPatternStartTick; + if ( nNewPatternTickPosition != m_pTransportPosition->getPatternTickPosition() && + ! bEmptySong ) { + ERRORLOG( QString( "[nPatternTickPosition mismatch] old: %1, new: %2" ) + .arg( m_pTransportPosition->getPatternTickPosition() ) + .arg( nNewPatternTickPosition ) ); } +#endif // Incorporate the looped transport again - fNewTick += fRepetitions * fNewSongSizeInTicks; - - // Ensure transport state is consistent - long long nNewFrames = computeFrameFromTick( fNewTick, &m_fTickMismatch ); + const double fNewTick = fNewStrippedTick + fRepetitions * fNewSongSizeInTicks; + const long long nNewFrame = TransportPosition::computeFrameFromTick( + fNewTick, &m_pTransportPosition->m_fTickMismatch ); + + double fTickOffset = fNewTick - m_pTransportPosition->getDoubleTick(); - m_nFrameOffset = nNewFrames - getFrames() + m_nFrameOffset; - m_fTickOffset = fNewTick - getDoubleTick(); + // The tick interval end covered in updateNoteQueue() is stored as + // double and needs to be more precise (hence updated before + // rounding). + m_fLastTickEnd += fTickOffset; - // Small rounding noise introduced in the calculation might spoil + // Small rounding noise introduced in the calculation does spoil // things as we floor the resulting tick offset later on. Hence, // we round it to a specific precision. - m_fTickOffset *= 1e8; - m_fTickOffset = std::round( m_fTickOffset ); - m_fTickOffset *= 1e-8; - - // INFOLOG(QString( "[update] nNewFrame: %1, old frame: %2, frame offset: %3, new tick: %4, old tick: %5, tick offset : %6, tick offset (without rounding): %7, fNewSongSizeInTicks: %8, fRepetitions: %9") - // .arg( nNewFrames ).arg( getFrames() ) - // .arg( m_nFrameOffset ) - // .arg( fNewTick, 0, 'g', 30 ) - // .arg( getDoubleTick(), 0, 'g', 30 ) - // .arg( m_fTickOffset, 0, 'g', 30 ) - // .arg( fNewTick - getDoubleTick(), 0, 'g', 30 ) - // .arg( fNewSongSizeInTicks, 0, 'g', 30 ) - // .arg( fRepetitions, 0, 'g', 30 ) - // ); - - setFrames( nNewFrames ); - setTick( fNewTick ); - - m_fLastTickIntervalEnd += m_fTickOffset; + fTickOffset *= 1e8; + fTickOffset = std::round( fTickOffset ); + fTickOffset *= 1e-8; + m_pTransportPosition->setTickOffsetSongSize( fTickOffset ); // Moves all notes currently processed by Hydrogen with respect to // the offsets calculated above. handleSongSizeChange(); - // After tick and frame information as well as notes are updated - // we will make the remainder of the transport information - // consistent. - updateTransportPosition( getDoubleTick() ); + m_pTransportPosition->setFrameOffsetTempo( + nNewFrame - m_pTransportPosition->getFrame() + + m_pTransportPosition->getFrameOffsetTempo() ); + + // INFOLOG(QString( "[update] nNewFrame: %1, m_pTransportPosition->getFrame() (old): %2, m_pTransportPosition->getFrameOffsetTempo(): %3, fNewTick: %4, m_pTransportPosition->getDoubleTick() (old): %5, m_pTransportPosition->getTickOffsetSongSize() : %6, tick offset (without rounding): %7, fNewSongSizeInTicks: %8, fRepetitions: %9, fNewStrippedTick: %10, nNewPatternStartTick: %11") + // .arg( nNewFrame ) + // .arg( m_pTransportPosition->getFrame() ) + // .arg( m_pTransportPosition->getFrameOffsetTempo() ) + // .arg( fNewTick, 0, 'g', 30 ) + // .arg( m_pTransportPosition->getDoubleTick(), 0, 'g', 30 ) + // .arg( m_pTransportPosition->getTickOffsetSongSize(), 0, 'g', 30 ) + // .arg( fNewTick - m_pTransportPosition->getDoubleTick(), 0, 'g', 30 ) + // .arg( fNewSongSizeInTicks, 0, 'g', 30 ) + // .arg( fRepetitions, 0, 'f' ) + // .arg( fNewStrippedTick, 0, 'f' ) + // .arg( nNewPatternStartTick ) + // ); + + const auto fOldTickSize = m_pTransportPosition->getTickSize(); + updateTransportPosition( fNewTick, nNewFrame, m_pTransportPosition ); + + // Ensure the tick offset is calculated as well (we do not expect + // the tempo to change hence the following call is most likely not + // executed during updateTransportPosition()). + if ( fOldTickSize == m_pTransportPosition->getTickSize() ) { + calculateTransportOffsetOnBpmChange( m_pTransportPosition ); + } + + // Updating the queuing position by the same offset to keep them + // approximately in sync. + const double fNewTickQueuing = m_pQueuingPosition->getDoubleTick() + + fTickOffset; + const long long nNewFrameQueuing = TransportPosition::computeFrameFromTick( + fNewTickQueuing, &m_pQueuingPosition->m_fTickMismatch ); + // Use offsets calculated above. + m_pQueuingPosition->set( m_pTransportPosition ); + updateTransportPosition( fNewTickQueuing, nNewFrameQueuing, + m_pQueuingPosition ); + + updatePlayingPatterns(); + +#ifdef H2CORE_HAVE_DEBUG + if ( nOldColumn != m_pTransportPosition->getColumn() && ! bEmptySong && + nOldColumn != -1 && m_pTransportPosition->getColumn() != -1 ) { + ERRORLOG( QString( "[nColumn mismatch] old: %1, new: %2" ) + .arg( nOldColumn ) + .arg( m_pTransportPosition->getColumn() ) ); + } +#endif - if ( m_nColumn == -1 || - ( bEndOfSongReached && - pSong->getLoopMode() != Song::LoopMode::Enabled ) ) { - stop(); - stopPlayback(); - locate( 0 ); + if ( m_pQueuingPosition->getColumn() == -1 && + pSong->getLoopMode() != Song::LoopMode::Enabled ) { + endOfSongReached(); + return; } - // WARNINGLOG( QString( "[After] frame: %1, bpm: %2, tickSize: %3, column: %4, tick: %5, pTickPos: %6, pStartPos: %7, m_fLastTickIntervalEnd: %8" ) - // .arg( getFrames() ).arg( getBpm() ) - // .arg( getTickSize(), 0, 'f' ) - // .arg( m_nColumn ).arg( getDoubleTick(), 0, 'f' ) - // .arg( m_nPatternTickPosition ) - // .arg( m_nPatternStartTick ) - // .arg( m_fLastTickIntervalEnd ) ); + // WARNINGLOG( QString( "[After] fNewTick: %1, fRepetitions: %2, m_fSongSizeInTicks: %3, fNewSongSizeInTicks: %4, transport: %5, queuing: %6" ) + // .arg( fNewTick, 0, 'g', 30 ) + // .arg( fRepetitions, 0, 'f' ) + // .arg( m_fSongSizeInTicks ) + // .arg( fNewSongSizeInTicks ) + // .arg( m_pTransportPosition->toQString( "", true ) ) + // .arg( m_pQueuingPosition->toQString( "", true ) ) + // ); + EventQueue::get_instance()->push_event( EVENT_SONG_SIZE_CHANGED, 0 ); } -void AudioEngine::removePlayingPattern( int nIndex ) { - m_pPlayingPatterns->del( nIndex ); +void AudioEngine::removePlayingPattern( Pattern* pPattern ) { + auto removePattern = [&]( std::shared_ptr pPos ) { + auto pPlayingPatterns = pPos->getPlayingPatterns(); + + for ( int ii = 0; ii < pPlayingPatterns->size(); ++ii ) { + if ( pPlayingPatterns->get( ii ) == pPattern ) { + pPlayingPatterns->del( ii ); + break; + } + } + }; + + removePattern( m_pTransportPosition ); + removePattern( m_pQueuingPosition ); } -void AudioEngine::updatePlayingPatterns( int nColumn, long nTick ) { +void AudioEngine::updatePlayingPatterns() { + updatePlayingPatternsPos( m_pTransportPosition ); + updatePlayingPatternsPos( m_pQueuingPosition ); +} + +void AudioEngine::updatePlayingPatternsPos( std::shared_ptr pPos ) { auto pHydrogen = Hydrogen::get_instance(); auto pSong = pHydrogen->getSong(); + auto pPlayingPatterns = pPos->getPlayingPatterns(); + + // DEBUGLOG( QString( "pre: %1" ).arg( pPos->toQString() ) ); if ( pHydrogen->getMode() == Song::Mode::Song ) { - // Called when transport enteres a new column. - m_pPlayingPatterns->clear(); - if ( nColumn < 0 || nColumn >= pSong->getPatternGroupVector()->size() ) { + const auto nPrevPatternNumber = pPlayingPatterns->size(); + + pPlayingPatterns->clear(); + + if ( pSong->getPatternGroupVector()->size() == 0 ) { + // No patterns in current song. + if ( nPrevPatternNumber > 0 ) { + EventQueue::get_instance()->push_event( EVENT_PLAYING_PATTERNS_CHANGED, 0 ); + } return; } + auto nColumn = std::max( pPos->getColumn(), 0 ); + if ( nColumn >= pSong->getPatternGroupVector()->size() ) { + ERRORLOG( QString( "Provided column [%1] exceeds allowed range [0,%2]. Using 0 as fallback." ) + .arg( nColumn ).arg( pSong->getPatternGroupVector()->size() - 1 ) ); + nColumn = 0; + } + for ( const auto& ppattern : *( *( pSong->getPatternGroupVector() ) )[ nColumn ] ) { if ( ppattern != nullptr ) { - m_pPlayingPatterns->add( ppattern ); - ppattern->addFlattenedVirtualPatterns( m_pPlayingPatterns ); + pPlayingPatterns->add( ppattern ); + ppattern->addFlattenedVirtualPatterns( pPlayingPatterns ); } } - if ( m_pPlayingPatterns->size() > 0 ) { - m_nPatternSize = m_pPlayingPatterns->longest_pattern_length(); - } else { - m_nPatternSize = MAX_NOTES; + // GUI does not care about the internals of the audio engine + // and just moves along the transport position. + // We omit the event when passing from one empty column to the + // next. + if ( pPos == m_pTransportPosition && + ( nPrevPatternNumber != 0 && pPlayingPatterns->size() != 0 ) ) { + EventQueue::get_instance()->push_event( EVENT_PLAYING_PATTERNS_CHANGED, 0 ); } - - EventQueue::get_instance()->push_event( EVENT_PATTERN_CHANGED, 0 ); } else if ( pHydrogen->getPatternMode() == Song::PatternMode::Selected ) { - // Called asynchronous when a different pattern number - // gets selected or the user switches from stacked into - // selected pattern mode. - + auto pSelectedPattern = pSong->getPatternList()->get( pHydrogen->getSelectedPatternNumber() ); - if ( m_pPlayingPatterns->size() != 1 || - ( m_pPlayingPatterns->size() == 1 && - m_pPlayingPatterns->get( 0 ) != pSelectedPattern ) ) { - m_pPlayingPatterns->clear(); - - if ( pSelectedPattern != nullptr ) { - m_pPlayingPatterns->add( pSelectedPattern ); - pSelectedPattern->addFlattenedVirtualPatterns( m_pPlayingPatterns ); - } - - if ( m_pPlayingPatterns->size() > 0 ) { - m_nPatternSize = m_pPlayingPatterns->longest_pattern_length(); - } else { - m_nPatternSize = MAX_NOTES; + if ( pSelectedPattern != nullptr && + ! ( pPlayingPatterns->size() == 1 && + pPlayingPatterns->get( 0 ) == pSelectedPattern ) ) { + pPlayingPatterns->clear(); + pPlayingPatterns->add( pSelectedPattern ); + pSelectedPattern->addFlattenedVirtualPatterns( pPlayingPatterns ); + + // GUI does not care about the internals of the audio + // engine and just moves along the transport position. + if ( pPos == m_pTransportPosition ) { + EventQueue::get_instance()->push_event( EVENT_PLAYING_PATTERNS_CHANGED, 0 ); } - - EventQueue::get_instance()->push_event( EVENT_PATTERN_CHANGED, 0 ); } } else if ( pHydrogen->getPatternMode() == Song::PatternMode::Stacked ) { - if ( m_pNextPatterns->size() > 0 ) { - - for ( const auto& ppattern : *m_pNextPatterns ) { - // If provided pattern is not part of the - // list, a nullptr will be returned. Else, a - // pointer to the deleted pattern will be - // returned. + auto pNextPatterns = pPos->getNextPatterns(); + + if ( pNextPatterns->size() > 0 ) { + for ( const auto& ppattern : *pNextPatterns ) { if ( ppattern == nullptr ) { continue; } - if ( ( m_pPlayingPatterns->del( ppattern ) ) == nullptr ) { + if ( ( pPlayingPatterns->del( ppattern ) ) == nullptr ) { // pPattern was not present yet. It will // be added. - m_pPlayingPatterns->add( ppattern ); - ppattern->addFlattenedVirtualPatterns( m_pPlayingPatterns ); + pPlayingPatterns->add( ppattern ); + ppattern->addFlattenedVirtualPatterns( pPlayingPatterns ); } else { // pPattern was already present. It will // be deleted. - ppattern->removeFlattenedVirtualPatterns( m_pPlayingPatterns ); + ppattern->removeFlattenedVirtualPatterns( pPlayingPatterns ); + } + + // GUI does not care about the internals of the audio + // engine and just moves along the transport position. + if ( pPos == m_pTransportPosition ) { + EventQueue::get_instance()->push_event( EVENT_PLAYING_PATTERNS_CHANGED, 0 ); } - EventQueue::get_instance()->push_event( EVENT_PATTERN_CHANGED, 0 ); - } - m_pNextPatterns->clear(); - - if ( m_pPlayingPatterns->size() != 0 ) { - m_nPatternSize = m_pPlayingPatterns->longest_pattern_length(); - } else { - m_nPatternSize = MAX_NOTES; } + pNextPatterns->clear(); } } + + if ( pPlayingPatterns->size() > 0 ) { + pPos->setPatternSize( pPlayingPatterns->longest_pattern_length() ); + } else { + pPos->setPatternSize( MAX_NOTES ); + } + + // DEBUGLOG( QString( "post: %1" ).arg( pPos->toQString() ) ); + } void AudioEngine::toggleNextPattern( int nPatternNumber ) { @@ -2118,15 +1840,21 @@ void AudioEngine::toggleNextPattern( int nPatternNumber ) { auto pSong = pHydrogen->getSong(); auto pPatternList = pSong->getPatternList(); auto pPattern = pPatternList->get( nPatternNumber ); - if ( pPattern != nullptr ) { - if ( m_pNextPatterns->del( pPattern ) == nullptr ) { - m_pNextPatterns->add( pPattern ); - } + if ( pPattern == nullptr ) { + return; + } + + if ( m_pTransportPosition->getNextPatterns()->del( pPattern ) == nullptr ) { + m_pTransportPosition->getNextPatterns()->add( pPattern ); + } + if ( m_pQueuingPosition->getNextPatterns()->del( pPattern ) == nullptr ) { + m_pQueuingPosition->getNextPatterns()->add( pPattern ); } } void AudioEngine::clearNextPatterns() { - m_pNextPatterns->clear(); + m_pTransportPosition->getNextPatterns()->clear(); + m_pQueuingPosition->getNextPatterns()->clear(); } void AudioEngine::flushAndAddNextPattern( int nPatternNumber ) { @@ -2134,276 +1862,347 @@ void AudioEngine::flushAndAddNextPattern( int nPatternNumber ) { auto pSong = pHydrogen->getSong(); auto pPatternList = pSong->getPatternList(); - m_pNextPatterns->clear(); bool bAlreadyPlaying = false; // Note: we will not perform a bound check on the provided pattern // number. This way the user can use the SELECT_ONLY_NEXT_PATTERN // MIDI or OSC command to flush all playing patterns. auto pRequestedPattern = pPatternList->get( nPatternNumber ); - - for ( int ii = 0; ii < m_pPlayingPatterns->size(); ++ii ) { - auto pPlayingPattern = m_pPlayingPatterns->get( ii ); - if ( pPlayingPattern != pRequestedPattern ) { - m_pNextPatterns->add( pPlayingPattern ); - } - else if ( pRequestedPattern != nullptr ) { - bAlreadyPlaying = true; + auto flushAndAddNext = [&]( std::shared_ptr pPos ) { + + auto pNextPatterns = pPos->getNextPatterns(); + auto pPlayingPatterns = pPos->getPlayingPatterns(); + + pNextPatterns->clear(); + for ( int ii = 0; ii < pPlayingPatterns->size(); ++ii ) { + + auto pPlayingPattern = pPlayingPatterns->get( ii ); + if ( pPlayingPattern != pRequestedPattern ) { + pNextPatterns->add( pPlayingPattern ); + } + else if ( pRequestedPattern != nullptr ) { + bAlreadyPlaying = true; + } } - } - // Appending the requested pattern. - if ( ! bAlreadyPlaying && pRequestedPattern != nullptr ) { - m_pNextPatterns->add( pRequestedPattern ); - } + // Appending the requested pattern. + if ( ! bAlreadyPlaying && pRequestedPattern != nullptr ) { + pNextPatterns->add( pRequestedPattern ); + } + }; + + flushAndAddNext( m_pTransportPosition ); + flushAndAddNext( m_pQueuingPosition ); } void AudioEngine::handleTimelineChange() { - setFrames( computeFrameFromTick( getDoubleTick(), &m_fTickMismatch ) ); - updateBpmAndTickSize(); + // INFOLOG( QString( "before:\n%1\n%2" ) + // .arg( m_pTransportPosition->toQString() ) + // .arg( m_pQueuingPosition->toQString() ) ); - if ( ! Hydrogen::get_instance()->isTimelineEnabled() ) { - // In case the Timeline was turned off, the - // handleTempoChange() function will take over and update all - // notes currently processed. - return; + const auto fOldTickSize = m_pTransportPosition->getTickSize(); + updateBpmAndTickSize( m_pTransportPosition ); + updateBpmAndTickSize( m_pQueuingPosition ); + + if ( fOldTickSize == m_pTransportPosition->getTickSize() ) { + // As tempo did not change during the Timeline activation, no + // update of the offsets took place. This, however, is not + // good, as it makes a significant difference to be located at + // tick X with e.g. 120 bpm tempo and at X with a 120 bpm + // tempo marker active but several others located prior to X. + calculateTransportOffsetOnBpmChange( m_pTransportPosition ); } + + // INFOLOG( QString( "after:\n%1\n%2" ) + // .arg( m_pTransportPosition->toQString() ) + // .arg( m_pQueuingPosition->toQString() ) ); +} + +void AudioEngine::handleTempoChange() { + if ( m_songNoteQueue.size() != 0 ) { - // Recalculate the note start in frames for all notes currently - // processed by the AudioEngine. - if ( m_songNoteQueue.size() > 0 ) { std::vector notes; for ( ; ! m_songNoteQueue.empty(); m_songNoteQueue.pop() ) { notes.push_back( m_songNoteQueue.top() ); } - for ( auto nnote : notes ) { - nnote->computeNoteStart(); - m_songNoteQueue.push( nnote ); + if ( notes.size() > 0 ) { + for ( auto nnote : notes ) { + nnote->computeNoteStart(); + m_songNoteQueue.push( nnote ); + } + } + + notes.clear(); + while ( m_midiNoteQueue.size() > 0 ) { + notes.push_back( m_midiNoteQueue[ 0 ] ); + m_midiNoteQueue.pop_front(); + } + + if ( notes.size() > 0 ) { + for ( auto nnote : notes ) { + nnote->computeNoteStart(); + m_midiNoteQueue.push_back( nnote ); + } } } getSampler()->handleTimelineOrTempoChange(); } -void AudioEngine::handleTempoChange() { - if ( m_songNoteQueue.size() == 0 ) { - return; - } - - // All notes share the same ticksize state (or things have gone - // wrong at some point). - if ( m_songNoteQueue.top()->getUsedTickSize() != - getTickSize() ) { +void AudioEngine::handleSongSizeChange() { + if ( m_songNoteQueue.size() != 0 ) { std::vector notes; for ( ; ! m_songNoteQueue.empty(); m_songNoteQueue.pop() ) { notes.push_back( m_songNoteQueue.top() ); } - // All notes share the same ticksize state (or things have gone - // wrong at some point). - for ( auto nnote : notes ) { - nnote->computeNoteStart(); - m_songNoteQueue.push( nnote ); - } - - getSampler()->handleTimelineOrTempoChange(); - } -} + const long nTickOffset = + static_cast(std::floor(m_pTransportPosition->getTickOffsetSongSize())); -void AudioEngine::handleSongSizeChange() { - if ( m_songNoteQueue.size() == 0 ) { - return; - } + if ( notes.size() > 0 ) { + for ( auto nnote : notes ) { - std::vector notes; - for ( ; ! m_songNoteQueue.empty(); m_songNoteQueue.pop() ) { - notes.push_back( m_songNoteQueue.top() ); - } + // DEBUGLOG( QString( "[song queue] name: %1, pos: %2 -> %3, tick offset: %4, tick offset floored: %5" ) + // .arg( nnote->get_instrument()->get_name() ) + // .arg( nnote->get_position() ) + // .arg( std::max( nnote->get_position() + nTickOffset, + // static_cast(0) ) ) + // .arg( m_pTransportPosition->getTickOffsetSongSize(), 0, 'f' ) + // .arg( nTickOffset ) ); + + nnote->set_position( std::max( nnote->get_position() + nTickOffset, + static_cast(0) ) ); + nnote->computeNoteStart(); + m_songNoteQueue.push( nnote ); + } + } + + notes.clear(); + while ( m_midiNoteQueue.size() > 0 ) { + notes.push_back( m_midiNoteQueue[ 0 ] ); + m_midiNoteQueue.pop_front(); + } - for ( auto nnote : notes ) { + if ( notes.size() > 0 ) { + for ( auto nnote : notes ) { - // DEBUGLOG( QString( "name: %1, pos: %2, new pos: %3, tick offset: %4, tick offset floored: %5" ) - // .arg( nnote->get_instrument()->get_name() ) - // .arg( nnote->get_position() ) - // .arg( std::max( nnote->get_position() + - // static_cast(std::floor(getTickOffset())), - // static_cast(0) ) ) - // .arg( getTickOffset() ) - // .arg( std::floor(getTickOffset()) ) ); + // DEBUGLOG( QString( "[midi queue] name: %1, pos: %2 -> %3, tick offset: %4, tick offset floored: %5" ) + // .arg( nnote->get_instrument()->get_name() ) + // .arg( nnote->get_position() ) + // .arg( std::max( nnote->get_position() + nTickOffset, + // static_cast(0) ) ) + // .arg( m_pTransportPosition->getTickOffsetSongSize(), 0, 'f' ) + // .arg( nTickOffset ) ); - nnote->set_position( std::max( nnote->get_position() + - static_cast(std::floor(getTickOffset())), - static_cast(0) ) ); - nnote->computeNoteStart(); - m_songNoteQueue.push( nnote ); + nnote->set_position( std::max( nnote->get_position() + nTickOffset, + static_cast(0) ) ); + nnote->computeNoteStart(); + m_midiNoteQueue.push_back( nnote ); + } + } } getSampler()->handleSongSizeChange(); } -long long AudioEngine::computeTickInterval( double* fTickStart, double* fTickEnd, unsigned nFrames ) { +long long AudioEngine::computeTickInterval( double* fTickStart, double* fTickEnd, unsigned nIntervalLengthInFrames ) { const auto pHydrogen = Hydrogen::get_instance(); const auto pTimeline = pHydrogen->getTimeline(); + auto pPos = m_pTransportPosition; long long nFrameStart, nFrameEnd; if ( getState() == State::Ready ) { - // In case the playback is stopped and all realtime events, - // by e.g. MIDI or Hydrogen's virtual keyboard, we disregard - // tempo changes in the Timeline and pretend the current tick - // size is valid for all future notes. - nFrameStart = getRealtimeFrames(); + // In case the playback is stopped we pretend it is still + // rolling using the realtime ticks while disregarding tempo + // changes in the Timeline. This is important as we want to + // continue playing back notes in the sampler and process + // realtime events, by e.g. MIDI or Hydrogen's virtual + // keyboard. + nFrameStart = getRealtimeFrame(); } else { - // Enters here both when the transport is rolling and - // State::Playing is set as well as with State::Prepared - // during testing. - nFrameStart = getFrames(); + // Enters here when either transport is rolling or the unit + // tests are run. + nFrameStart = pPos->getFrame(); } // We don't use the getLookaheadInFrames() function directly // because the lookahead contains both a frame-based and a - // tick-based component and would be a twice as expensive to + // tick-based component and would be twice as expensive to // calculate using the mentioned call. - long long nLeadLagFactor = getLeadLagInFrames( getDoubleTick() ); - long long nLookahead = nLeadLagFactor + + long long nLeadLagFactor = getLeadLagInFrames( pPos->getDoubleTick() ); + + // Timeline disabled: + // Due to rounding errors in tick<->frame conversions the leadlag + // factor in frames can differ by +/-1 even if the corresponding + // lead lag in ticks is exactly the same. + // + // Timeline enabled: + // With Tempo markers being present the lookahead is not constant + // anymore. As it determines the position X frames and Y ticks + // into the future, imagine it being process cycle after cycle + // moved across a marker. The amount of frames covered by the + // first and the second tick size will always change and so does + // the resulting lookahead. + // + // This, however, would result in holes and overlaps in tick + // coverage for the queuing position and note enqueuing in + // updateNoteQueue(). That's why we stick to a single lead lag + // factor invalidated each time the tempo of the song does change. + if ( pPos->getLastLeadLagFactor() != 0 ) { + if ( pPos->getLastLeadLagFactor() != nLeadLagFactor ) { + nLeadLagFactor = pPos->getLastLeadLagFactor(); + } + } else { + pPos->setLastLeadLagFactor( nLeadLagFactor ); + } + + const long long nLookahead = nLeadLagFactor + AudioEngine::nMaxTimeHumanize + 1; nFrameEnd = nFrameStart + nLookahead + - static_cast(nFrames); + static_cast(nIntervalLengthInFrames); - if ( m_fLastTickIntervalEnd != -1 ) { + // Checking whether transport and queuing position are identical + // is not enough in here. For specific audio driver parameters and + // very tiny buffersizes used by drivers with dynamic buffer sizes + // they both can be identical. + if ( m_bLookaheadApplied ) { nFrameStart += nLookahead; } - - *fTickStart = computeTickFromFrame( nFrameStart ); - *fTickEnd = computeTickFromFrame( nFrameEnd ); - - // INFOLOG( QString( "nFrameStart: %1, nFrameEnd: %2, fTickStart: %3, fTickEnd: %4" ) - // .arg( nFrameStart ).arg( nFrameEnd ) - // .arg( *fTickStart, 0, 'f' ).arg( *fTickEnd, 0, 'f' ) ); - if ( getState() == State::Playing || getState() == State::Testing ) { - // If there was a change in ticksize, account for the last used - // lookahead to ensure the tick intervals are aligned. - if ( m_fLastTickIntervalEnd != -1 && - m_fLastTickIntervalEnd != *fTickStart ) { - if ( m_fLastTickIntervalEnd > *fTickEnd ) { - // The last lookahead was larger than the end of the - // current interval would reach. We will remain at the - // former interval end until the lookahead was eaten up in - // future calls to updateNoteQueue() to not produce - // glitches by non-aligned tick intervals. - *fTickStart = m_fLastTickIntervalEnd; - *fTickEnd = m_fLastTickIntervalEnd; - } else { - *fTickStart = m_fLastTickIntervalEnd; - } - } + *fTickStart = ( TransportPosition::computeTickFromFrame( nFrameStart ) + + pPos->getTickMismatch() ) - pPos->getTickOffsetQueuing() ; + *fTickEnd = TransportPosition::computeTickFromFrame( nFrameEnd ) - + pPos->getTickOffsetQueuing(); + + // INFOLOG( QString( "nFrame: [%1,%2], fTick: [%3, %4], fTick (without offset): [%5,%6], m_pTransportPosition->getTickOffsetQueuing(): %7, nLookahead: %8, nIntervalLengthInFrames: %9, m_pTransportPosition: %10, m_pQueuingPosition: %11,_bLookaheadApplied: %12" ) + // .arg( nFrameStart ) + // .arg( nFrameEnd ) + // .arg( *fTickStart, 0, 'f' ) + // .arg( *fTickEnd, 0, 'f' ) + // .arg( TransportPosition::computeTickFromFrame( nFrameStart ), 0, 'f' ) + // .arg( TransportPosition::computeTickFromFrame( nFrameEnd ), 0, 'f' ) + // .arg( pPos->getTickOffsetQueuing(), 0, 'f' ) + // .arg( nLookahead ) + // .arg( nIntervalLengthInFrames ) + // .arg( pPos->toQString() ) + // .arg( m_pQueuingPosition->toQString() ) + // .arg( m_bLookaheadApplied ) + // ); - // DEBUGLOG( QString( "tick: [%1,%2], curr tick: %5, curr frame: %4, nFrames: %3, realtime: %6, m_fTickOffset: %7, ticksize: %8, leadlag: %9, nlookahead: %10, m_fLastTickIntervalEnd: %11" ) - // .arg( *fTickStart, 0, 'f' ) - // .arg( *fTickEnd, 0, 'f' ) - // .arg( nFrames ) - // .arg( getFrames() ) - // .arg( getDoubleTick(), 0, 'f' ) - // .arg( getRealtimeFrames() ) - // .arg( m_fTickOffset, 0, 'f' ) - // .arg( getTickSize(), 0, 'f' ) - // .arg( nLeadLagFactor ) - // .arg( nLookahead ) - // .arg( m_fLastTickIntervalEnd, 0, 'f' ) - // ); + return nLeadLagFactor; +} - if ( m_fLastTickIntervalEnd < *fTickEnd ) { - m_fLastTickIntervalEnd = *fTickEnd; + // Ideally we just floor the provided tick. When relocating to + // a specific tick, it's converted counterpart is stored as the + // transport position in frames, which is then used to calculate + // the tick start again. These conversions back and forth can + // introduce rounding error that get larger for larger tick + // numbers and could result in a computed start tick of + // 86753.999999934 when transport was relocated to 86754. As we do + // not want to cover notes prior to our current transport + // position, we have to account for such rounding errors. +double AudioEngine::coarseGrainTick( double fTick ) { + if ( std::ceil( fTick ) - fTick > 0 && + std::ceil( fTick ) - fTick < 1E-6 ) { + return std::floor( fTick ) + 1; + } + else { + return std::floor( fTick ); } } - return nLeadLagFactor; -} - -int AudioEngine::updateNoteQueue( unsigned nFrames ) +int AudioEngine::updateNoteQueue( unsigned nIntervalLengthInFrames ) { Hydrogen* pHydrogen = Hydrogen::get_instance(); std::shared_ptr pSong = pHydrogen->getSong(); - double fTickStart, fTickEnd; + double fTickStartComp, fTickEndComp; long long nLeadLagFactor = - computeTickInterval( &fTickStart, &fTickEnd, nFrames ); + computeTickInterval( &fTickStartComp, &fTickEndComp, nIntervalLengthInFrames ); - // Get initial timestamp for first tick - gettimeofday( &m_currentTickTime, nullptr ); - - // MIDI events now get put into the `m_songNoteQueue` as well, - // based on their timestamp (which is given in terms of its - // transport position and not in terms of the date-time as above). + // MIDI events get put into the `m_songNoteQueue` as well. while ( m_midiNoteQueue.size() > 0 ) { Note *pNote = m_midiNoteQueue[0]; - - // DEBUGLOG( QString( "getDoubleTick(): %1, getFrames(): %2, " ) - // .arg( getDoubleTick() ).arg( getFrames() ) - // .append( pNote->toQString( "", true ) ) ); - if ( pNote->get_position() > - static_cast(std::floor( fTickEnd )) ) { + static_cast(coarseGrainTick( fTickEndComp )) ) { break; } m_midiNoteQueue.pop_front(); pNote->get_instrument()->enqueue(); pNote->computeNoteStart(); + pNote->humanize(); m_songNoteQueue.push( pNote ); } if ( getState() != State::Playing && getState() != State::Testing ) { - // only keep going if we're playing return 0; } - long long nNoteStart; - float fUsedTickSize; + AutomationPath* pAutomationPath = pSong->getVelocityAutomationPath(); - double fTickMismatch; + // computeTickInterval() is always called regardless whether + // transport is rolling or not. But we only mark the lookahead + // consumed if the associated tick interval was actually traversed + // by the queuing position. + if ( ! m_bLookaheadApplied ) { + m_bLookaheadApplied = true; + } + + const long nTickStart = static_cast(coarseGrainTick( fTickStartComp )); + const long nTickEnd = static_cast(coarseGrainTick( fTickEndComp )); - AutomationPath* pAutomationPath = pSong->getVelocityAutomationPath(); - - // DEBUGLOG( QString( "tick interval: [%1 : %2], curr tick: %3, curr frame: %4") - // .arg( fTickStart, 0, 'f' ).arg( fTickEnd, 0, 'f' ) - // .arg( getDoubleTick(), 0, 'f' ).arg( getFrames() ) ); + // Only store the last tick interval end if transport is + // rolling. Else the realtime frame processing will mess things + // up. + m_fLastTickEnd = fTickEndComp; + + // WARNINGLOG( QString( "tick interval (floor): [%1,%2], tick interval (computed): [%3,%4], nLeadLagFactor: %5, m_fSongSizeInTicks: %6, m_pTransportPosition: %7, m_pQueuingPosition: %8") + // .arg( nTickStart ).arg( nTickEnd ) + // .arg( fTickStartComp, 0, 'f' ).arg( fTickEndComp, 0, 'f' ) + // .arg( nLeadLagFactor ) + // .arg( m_fSongSizeInTicks, 0, 'f' ) + // .arg( m_pTransportPosition->toQString() ) + // .arg( m_pQueuingPosition->toQString() ) ); // We loop over integer ticks to ensure that all notes encountered // between two iterations belong to the same pattern. - for ( long nnTick = static_cast(std::floor(fTickStart)); - nnTick < static_cast(std::floor(fTickEnd)); nnTick++ ) { - + for ( long nnTick = nTickStart; nnTick < nTickEnd; ++nnTick ) { + ////////////////////////////////////////////////////////////// - // SONG MODE + // Update queuing position and playing patterns. if ( pHydrogen->getMode() == Song::Mode::Song ) { - if ( pSong->getPatternGroupVector()->size() == 0 ) { - // there's no song!! - ERRORLOG( "no patterns in song." ); - stop(); - return -1; - } - - int nOldColumn = m_nColumn; - updateSongTransportPosition( static_cast(nnTick) ); - - // If no pattern list could not be found, either choose - // the first one if loop mode is activate or the - // function returns indicating that the end of the song is - // reached. - if ( m_nColumn == -1 || - ( pSong->getLoopMode() == Song::LoopMode::Finishing && - m_nColumn < nOldColumn ) ) { - INFOLOG( "End of Song" ); + const long nPreviousPosition = m_pQueuingPosition->getPatternStartTick() + + m_pQueuingPosition->getPatternTickPosition(); + + const long long nNewFrame = TransportPosition::computeFrameFromTick( + static_cast(nnTick), + &m_pQueuingPosition->m_fTickMismatch ); + updateSongTransportPosition( static_cast(nnTick), + nNewFrame, m_pQueuingPosition ); + + if ( ( pSong->getLoopMode() != Song::LoopMode::Enabled ) && + ( ( nPreviousPosition > m_pQueuingPosition->getPatternStartTick() + + m_pQueuingPosition->getPatternTickPosition() ) || + pSong->getPatternGroupVector()->size() == 0 ) ) { + + // DEBUGLOG( QString( "nPreviousPosition: %1, curr: %2, transport pos: %3, queuing pos: %4" ) + // .arg( nPreviousPosition ) + // .arg( m_pQueuingPosition->getPatternStartTick() + + // m_pQueuingPosition->getPatternTickPosition() ) + // .arg( m_pTransportPosition->toQString() ) + // .arg( m_pQueuingPosition->toQString() ) ); + + INFOLOG( "End of song reached." ); if( pHydrogen->getMidiOutput() != nullptr ){ pHydrogen->getMidiOutput()->handleQueueAllNoteOff(); @@ -2412,30 +2211,33 @@ int AudioEngine::updateNoteQueue( unsigned nFrames ) return -1; } } - - ////////////////////////////////////////////////////////////// - // PATTERN MODE else if ( pHydrogen->getMode() == Song::Mode::Pattern ) { - updatePatternTransportPosition( nnTick ); - - // DEBUGLOG( QString( "[post] nnTick: %1, m_nPatternTickPosition: %2, m_nPatternStartTick: %3, m_nPatternSize: %4" ) - // .arg( nnTick ).arg( m_nPatternTickPosition ) - // .arg( m_nPatternStartTick ).arg( m_nPatternSize ) ); - + const long long nNewFrame = TransportPosition::computeFrameFromTick( + static_cast(nnTick), + &m_pQueuingPosition->m_fTickMismatch ); + updatePatternTransportPosition( static_cast(nnTick), + nNewFrame, m_pQueuingPosition ); } ////////////////////////////////////////////////////////////// // Metronome // Only trigger the metronome at a predefined rate. - if ( m_nPatternTickPosition % 48 == 0 ) { + int nMetronomeTickPosition; + if ( pSong->getPatternGroupVector()->size() == 0 ) { + nMetronomeTickPosition = nnTick; + } else { + nMetronomeTickPosition = m_pQueuingPosition->getPatternTickPosition(); + } + + if ( nMetronomeTickPosition % 48 == 0 ) { float fPitch; float fVelocity; // Depending on whether the metronome beat will be issued // at the beginning or in the remainder of the pattern, // two different sounds and events will be used. - if ( m_nPatternTickPosition == 0 ) { + if ( nMetronomeTickPosition == 0 ) { fPitch = 3; fVelocity = 1.0; EventQueue::get_instance()->push_event( EVENT_METRONOME, 1 ); @@ -2448,22 +2250,28 @@ int AudioEngine::updateNoteQueue( unsigned nFrames ) // Only trigger the sounds if the user enabled the // metronome. if ( Preferences::get_instance()->m_bUseMetronome ) { - m_pMetronomeInstrument->set_volume( - Preferences::get_instance()->m_fMetronomeVolume - ); Note *pMetronomeNote = new Note( m_pMetronomeInstrument, nnTick, fVelocity, 0.f, // pan -1, - fPitch - ); + fPitch ); m_pMetronomeInstrument->enqueue(); pMetronomeNote->computeNoteStart(); m_songNoteQueue.push( pMetronomeNote ); } } - + + if ( pHydrogen->getMode() == Song::Mode::Song && + pSong->getPatternGroupVector()->size() == 0 ) { + // No patterns in song. We let transport roll in case + // patterns will be added again and still use metronome. + if ( Preferences::get_instance()->m_bUseMetronome ) { + continue; + } else { + return 0; + } + } ////////////////////////////////////////////////////////////// // Update the notes queue. // @@ -2473,115 +2281,68 @@ int AudioEngine::updateNoteQueue( unsigned nFrames ) // - add remainder of pNote->get_position() % 1 when setting // nnTick as new position. // - if ( m_pPlayingPatterns->size() != 0 ) { - for ( unsigned nPat = 0 ; - nPat < m_pPlayingPatterns->size() ; - ++nPat ) { - Pattern *pPattern = m_pPlayingPatterns->get( nPat ); + const auto pPlayingPatterns = m_pQueuingPosition->getPlayingPatterns(); + if ( pPlayingPatterns->size() != 0 ) { + for ( auto nPat = 0; nPat < pPlayingPatterns->size(); ++nPat ) { + Pattern *pPattern = pPlayingPatterns->get( nPat ); assert( pPattern != nullptr ); Pattern::notes_t* notes = (Pattern::notes_t*)pPattern->get_notes(); // Loop over all notes at tick nPatternTickPosition // (associated tick is determined by Note::__position // at the time of insertion into the Pattern). - FOREACH_NOTE_CST_IT_BOUND(notes, it, m_nPatternTickPosition) { + FOREACH_NOTE_CST_IT_BOUND(notes, it, + m_pQueuingPosition->getPatternTickPosition()) { Note *pNote = it->second; - if ( pNote ) { + if ( pNote != nullptr ) { pNote->set_just_recorded( false ); - /** Time Offset in frames (relative to sample rate) - * Sum of 3 components: swing, humanized timing, lead_lag - */ - int nOffset = 0; - - /** Swing 16ths // - * delay the upbeat 16th-notes by a constant (manual) offset - */ - if ( ( ( m_nPatternTickPosition % ( MAX_NOTES / 16 ) ) == 0 ) - && ( ( m_nPatternTickPosition % ( MAX_NOTES / 8 ) ) != 0 ) - && pSong->getSwingFactor() > 0 ) { - /* TODO: incorporate the factor MAX_NOTES / 32. either in Song::m_fSwingFactor - * or make it a member variable. - * comment by oddtime: - * 32 depends on the fact that the swing is applied to the upbeat 16th-notes. - * (not to upbeat 8th-notes as in jazz swing!). - * however 32 could be changed but must be >16, otherwise the max delay is too long and - * the swing note could be played after the next downbeat! - */ - // If the Timeline is activated, the tick - // size may change at any - // point. Therefore, the length in frames - // of a 16-th note offset has to be - // calculated for a particular transport - // position and is not generally applicable. - nOffset += - computeFrameFromTick( nnTick + MAX_NOTES / 32., &fTickMismatch ) * - pSong->getSwingFactor() - - computeFrameFromTick( nnTick, &fTickMismatch ); - } - - /* Humanize - Time parameter // - * Add a random offset to each note. Due to - * the nature of the Gaussian distribution, - * the factor Song::__humanize_time_value will - * also scale the variance of the generated - * random variable. - */ - if ( pSong->getHumanizeTimeValue() != 0 ) { - nOffset += ( int )( - getGaussian( 0.3 ) - * pSong->getHumanizeTimeValue() - * AudioEngine::nMaxTimeHumanize - ); - } - - // Lead or Lag - timing parameter // - // Add a constant offset to all notes. - nOffset += (int) ( pNote->get_lead_lag() * nLeadLagFactor ); - - // Lower bound of the offset. No note is - // allowed to start prior to the beginning of - // the song. - if( nNoteStart + nOffset < 0 ){ - nOffset = -nNoteStart; - } - - if ( nOffset > AudioEngine::nMaxTimeHumanize ) { - nOffset = AudioEngine::nMaxTimeHumanize; - } else if ( nOffset < -1 * AudioEngine::nMaxTimeHumanize ) { - nOffset = -AudioEngine::nMaxTimeHumanize; - } - - // Generate a copy of the current note, assign - // it the new offset, and push it to the list - // of all notes, which are about to be played - // back. - // - // Why a copy? because it has the new offset - // (including swing and random timing) in its - // humanized delay, and tick position is - // expressed referring to start time (and not - // pattern). Note *pCopiedNote = new Note( pNote ); - pCopiedNote->set_humanize_delay( nOffset ); - // DEBUGLOG( QString( "getDoubleTick(): %1, getFrames(): %2, getColumn(): %3, nnTick: %4, " ) - // .arg( getDoubleTick() ).arg( getFrames() ) - // .arg( getColumn() ).arg( nnTick ) - // .append( pCopiedNote->toQString("", true ) ) ); + // Lead or Lag. + // This property is set within the + // NotePropertiesRuler and only applies to + // notes picked up from patterns within + // Hydrogen during transport. + pCopiedNote->set_humanize_delay( + pCopiedNote->get_humanize_delay() + + static_cast( + static_cast(pNote->get_lead_lag()) * + static_cast(nLeadLagFactor) )); pCopiedNote->set_position( nnTick ); - // Important: this call has to be done _after_ - // setting the position and the humanize_delay. + pCopiedNote->humanize(); + + /** Swing 16ths + * delay the upbeat 16th-notes by a constant + * (manual) offset. + * + * This must done _after_ setting the position + * of the note. + */ + if ( ( ( m_pQueuingPosition->getPatternTickPosition() % + ( MAX_NOTES / 16 ) ) == 0 ) && + ( ( m_pQueuingPosition->getPatternTickPosition() % + ( MAX_NOTES / 8 ) ) != 0 ) ) { + pCopiedNote->swing(); + } + + // This must be done _after_ setting the + // position, humanization, and swing. pCopiedNote->computeNoteStart(); if ( pHydrogen->getMode() == Song::Mode::Song ) { - float fPos = static_cast( m_nColumn ) + + const float fPos = static_cast( m_pQueuingPosition->getColumn() ) + pCopiedNote->get_position() % 192 / 192.f; - pCopiedNote->set_velocity( pNote->get_velocity() * + pCopiedNote->set_velocity( pCopiedNote->get_velocity() * pAutomationPath->get_value( fPos ) ); } - pNote->get_instrument()->enqueue(); + + // DEBUGLOG( QString( "m_pQueuingPosition: %1, new note: %2" ) + // .arg( m_pQueuingPosition->toQString() ) + // .arg( pCopiedNote->toQString() ) ); + + pCopiedNote->get_instrument()->enqueue(); m_songNoteQueue.push( pCopiedNote ); } } @@ -2594,7 +2355,6 @@ int AudioEngine::updateNoteQueue( unsigned nFrames ) void AudioEngine::noteOn( Note *note ) { - // check current state if ( ! ( getState() == State::Playing || getState() == State::Ready || getState() == State::Testing ) ) { @@ -2607,13 +2367,8 @@ void AudioEngine::noteOn( Note *note ) m_midiNoteQueue.push_back( note ); } -bool AudioEngine::compare_pNotes::operator()(Note* pNote1, Note* pNote2) -{ - float fTickSize = Hydrogen::get_instance()->getAudioEngine()->getTickSize(); - return (pNote1->get_humanize_delay() + - AudioEngine::computeFrame( pNote1->get_position(), fTickSize ) ) > - (pNote2->get_humanize_delay() + - AudioEngine::computeFrame( pNote2->get_position(), fTickSize ) ); +bool AudioEngine::compare_pNotes::operator()(Note* pNote1, Note* pNote2) { + return pNote1->getNoteStart() > pNote2->getNoteStart(); } void AudioEngine::play() { @@ -2656,1873 +2411,130 @@ double AudioEngine::getLeadLagInTicks() { } long long AudioEngine::getLeadLagInFrames( double fTick ) { - double fTickMismatch; - long long nFrameStart = computeFrameFromTick( fTick, &fTickMismatch ); - long long nFrameEnd = computeFrameFromTick( fTick + AudioEngine::getLeadLagInTicks(), - &fTickMismatch ); + double fTmp; + const long long nFrameStart = + TransportPosition::computeFrameFromTick( fTick, &fTmp ); + const long long nFrameEnd = + TransportPosition::computeFrameFromTick( fTick + + AudioEngine::getLeadLagInTicks(), + &fTmp ); + + // WARNINGLOG( QString( "nFrameStart: %1, nFrameEnd: %2, diff: %3, fTick: %4" ) + // .arg( nFrameStart ).arg( nFrameEnd ) + // .arg( nFrameEnd - nFrameStart ).arg( fTick, 0, 'f' ) ); return nFrameEnd - nFrameStart; } -long long AudioEngine::getLookaheadInFrames( double fTick ) { - return getLeadLagInFrames( fTick ) + +long long AudioEngine::getLookaheadInFrames() { + return getLeadLagInFrames( m_pTransportPosition->getDoubleTick() ) + AudioEngine::nMaxTimeHumanize + 1; } -double AudioEngine::getDoubleTick() const { - return TransportInfo::getTick(); -} - -long AudioEngine::getTick() const { - return static_cast(std::floor( getDoubleTick() )); -} - -bool AudioEngine::testFrameToTickConversion() { - auto pHydrogen = Hydrogen::get_instance(); - auto pCoreActionController = pHydrogen->getCoreActionController(); - - bool bNoMismatch = true; - - pCoreActionController->activateTimeline( true ); - pCoreActionController->addTempoMarker( 0, 120 ); - pCoreActionController->addTempoMarker( 3, 100 ); - pCoreActionController->addTempoMarker( 5, 40 ); - pCoreActionController->addTempoMarker( 7, 200 ); - - double fFrameOffset1, fFrameOffset2, fFrameOffset3, - fFrameOffset4, fFrameOffset5, fFrameOffset6; - - long long nFrame1 = 342732; - long long nFrame2 = 1037223; - long long nFrame3 = 453610333722; - double fTick1 = computeTickFromFrame( nFrame1 ); - long long nFrame1Computed = computeFrameFromTick( fTick1, &fFrameOffset1 ); - double fTick2 = computeTickFromFrame( nFrame2 ); - long long nFrame2Computed = computeFrameFromTick( fTick2, &fFrameOffset2 ); - double fTick3 = computeTickFromFrame( nFrame3 ); - long long nFrame3Computed = computeFrameFromTick( fTick3, &fFrameOffset3 ); - - if ( nFrame1Computed != nFrame1 || std::abs( fFrameOffset1 ) > 1e-10 ) { - qDebug() << QString( "[testFrameToTickConversion] [1] nFrame: %1, fTick: %2, nFrameComputed: %3, fFrameOffset: %4, frame diff: %5" ) - .arg( nFrame1 ).arg( fTick1, 0, 'f' ).arg( nFrame1Computed ) - .arg( fFrameOffset1, 0, 'E', -1 ) - .arg( nFrame1Computed - nFrame1 ) - .toLocal8Bit().data(); - bNoMismatch = false; - } - if ( nFrame2Computed != nFrame2 || std::abs( fFrameOffset2 ) > 1e-10 ) { - qDebug() << QString( "[testFrameToTickConversion] [2] nFrame: %1, fTick: %2, nFrameComputed: %3, fFrameOffset: %4, frame diff: %5" ) - .arg( nFrame2 ).arg( fTick2, 0, 'f' ).arg( nFrame2Computed ) - .arg( fFrameOffset2, 0, 'E', -1 ) - .arg( nFrame2Computed - nFrame2 ).toLocal8Bit().data(); - bNoMismatch = false; - } - if ( nFrame3Computed != nFrame3 || std::abs( fFrameOffset3 ) > 1e-6 ) { - qDebug() << QString( "[testFrameToTickConversion] [3] nFrame: %1, fTick: %2, nFrameComputed: %3, fFrameOffset: %4, frame diff: %5" ) - .arg( nFrame3 ).arg( fTick3, 0, 'f' ).arg( nFrame3Computed ) - .arg( fFrameOffset3, 0, 'E', -1 ) - .arg( nFrame3Computed - nFrame3 ).toLocal8Bit().data(); - bNoMismatch = false; - } - - double fTick4 = 552; - double fTick5 = 1939; - double fTick6 = 534623409; - long long nFrame4 = computeFrameFromTick( fTick4, &fFrameOffset4 ); - double fTick4Computed = computeTickFromFrame( nFrame4 ) + - fFrameOffset4; - long long nFrame5 = computeFrameFromTick( fTick5, &fFrameOffset5 ); - double fTick5Computed = computeTickFromFrame( nFrame5 ) + - fFrameOffset5; - long long nFrame6 = computeFrameFromTick( fTick6, &fFrameOffset6 ); - double fTick6Computed = computeTickFromFrame( nFrame6 ) + - fFrameOffset6; - - - if ( abs( fTick4Computed - fTick4 ) > 1e-9 ) { - qDebug() << QString( "[testFrameToTickConversion] [4] nFrame: %1, fTick: %2, fTickComputed: %3, fFrameOffset: %4, tick diff: %5" ) - .arg( nFrame4 ).arg( fTick4, 0, 'f' ).arg( fTick4Computed, 0, 'f' ) - .arg( fFrameOffset4, 0, 'E' ) - .arg( fTick4Computed - fTick4 ).toLocal8Bit().data(); - bNoMismatch = false; - } - - if ( abs( fTick5Computed - fTick5 ) > 1e-9 ) { - qDebug() << QString( "[testFrameToTickConversion] [5] nFrame: %1, fTick: %2, fTickComputed: %3, fFrameOffset: %4, tick diff: %5" ) - .arg( nFrame5 ).arg( fTick5, 0, 'f' ).arg( fTick5Computed, 0, 'f' ) - .arg( fFrameOffset5, 0, 'E' ) - .arg( fTick5Computed - fTick5 ).toLocal8Bit().data(); - bNoMismatch = false; +const PatternList* AudioEngine::getPlayingPatterns() const { + if ( m_pTransportPosition != nullptr ) { + return m_pTransportPosition->getPlayingPatterns(); } + return nullptr; +} - if ( abs( fTick6Computed - fTick6 ) > 1e-6 ) { - qDebug() << QString( "[testFrameToTickConversion] [6] nFrame: %1, fTick: %2, fTickComputed: %3, fFrameOffset: %4, tick diff: %5" ) - .arg( nFrame6 ).arg( fTick6, 0, 'f' ).arg( fTick6Computed, 0, 'f' ) - .arg( fFrameOffset6, 0, 'E' ) - .arg( fTick6Computed - fTick6 ).toLocal8Bit().data(); - bNoMismatch = false; +const PatternList* AudioEngine::getNextPatterns() const { + if ( m_pTransportPosition != nullptr ) { + return m_pTransportPosition->getNextPatterns(); } - - return bNoMismatch; + return nullptr; } -bool AudioEngine::testTransportProcessing() { - auto pHydrogen = Hydrogen::get_instance(); - auto pPref = Preferences::get_instance(); - auto pCoreActionController = pHydrogen->getCoreActionController(); - - pCoreActionController->activateTimeline( false ); - pCoreActionController->activateLoopMode( true ); - - lock( RIGHT_HERE ); - - // Seed with a real random value, if available - std::random_device randomSeed; - - // Choose a random mean between 1 and 6 - std::default_random_engine randomEngine( randomSeed() ); - std::uniform_int_distribution frameDist( 1, pPref->m_nBufferSize ); - std::uniform_real_distribution tempoDist( MIN_BPM, MAX_BPM ); - - // For this call the AudioEngine still needs to be in state - // Playing or Ready. - reset( false ); - - 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; - double fCheckTick; - long long nCheckFrame, nLastFrame = 0; - - bool bNoMismatch = true; - - // 2112 is the number of ticks within the test song. - int nMaxCycles = - std::max( std::ceil( 2112.0 / - static_cast(pPref->m_nBufferSize) * - getTickSize() * 4.0 ), - 2112.0 ); - int nn = 0; - - while ( getDoubleTick() < m_fSongSizeInTicks ) { - - nFrames = frameDist( randomEngine ); - - incrementTransportPosition( nFrames ); +QString AudioEngine::toQString( const QString& sPrefix, bool bShort ) const { + QString s = Base::sPrintIndention; - if ( ! testCheckTransportPosition( "[testTransportProcessing] constant tempo" ) ) { - bNoMismatch = false; - break; + QString sOutput; + if ( ! bShort ) { + sOutput = QString( "%1[AudioEngine]\n" ).arg( sPrefix ) + .append( "%1%2m_pTransportPosition:\n").arg( sPrefix ).arg( s ); + if ( m_pTransportPosition != nullptr ) { + sOutput.append( QString( "%1" ) + .arg( m_pTransportPosition->toQString( sPrefix + s, bShort ) ) ); + } else { + sOutput.append( QString( "nullptr\n" ) ); } - - if ( getFrames() - nFrames != nLastFrame ) { - qDebug() << QString( "[testTransportProcessing] [constant tempo] inconsistent frame update. getFrames(): %1, nFrames: %2, nLastFrame: %3" ) - .arg( getFrames() ).arg( nFrames ).arg( nLastFrame ); - bNoMismatch = false; - break; + sOutput.append( QString( "%1%2m_pQueuingPosition:\n").arg( sPrefix ).arg( s ) ); + if ( m_pQueuingPosition != nullptr ) { + sOutput.append( QString( "%1" ) + .arg( m_pQueuingPosition->toQString( sPrefix + s, bShort ) ) ); + } else { + sOutput.append( QString( "nullptr\n" ) ); } - nLastFrame = getFrames(); - - nn++; - - if ( nn > nMaxCycles ) { - qDebug() << QString( "[testTransportProcessing] [constant tempo] end of the song wasn't reached in time. getFrames(): %1, ticks: %2, getTickSize(): %3, m_fSongSizeInTicks: %4, nMaxCycles: %5" ) - .arg( getFrames() ) - .arg( getDoubleTick(), 0, 'f' ) - .arg( getTickSize(), 0, 'f' ) - .arg( m_fSongSizeInTicks, 0, 'f' ) - .arg( nMaxCycles ); - bNoMismatch = false; - break; + sOutput.append( QString( "%1%2m_fNextBpm: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fNextBpm, 0, 'f' ) ) + .append( QString( "%1%2m_state: %3\n" ).arg( sPrefix ).arg( s ).arg( static_cast(m_state) ) ) + .append( QString( "%1%2m_nextState: %3\n" ).arg( sPrefix ).arg( s ).arg( static_cast(m_nextState) ) ) + .append( QString( "%1%2m_fSongSizeInTicks: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fSongSizeInTicks, 0, 'f' ) ) + .append( QString( "%1%2m_fLastTickEnd: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fLastTickEnd, 0, 'f' ) ) + .append( QString( "%1%2m_bLookaheadApplied: %3\n" ).arg( sPrefix ).arg( s ).arg( m_bLookaheadApplied ) ) + .append( QString( "%1%2m_pSampler: stringification not implemented\n" ).arg( sPrefix ).arg( s ) ) + .append( QString( "%1%2m_pSynth: stringification not implemented\n" ).arg( sPrefix ).arg( s ) ) + .append( QString( "%1%2m_pAudioDriver: stringification not implemented\n" ).arg( sPrefix ).arg( s ) ) + .append( QString( "%1%2m_pMidiDriver: stringification not implemented\n" ).arg( sPrefix ).arg( s ) ) + .append( QString( "%1%2m_pMidiDriverOut: stringification not implemented\n" ).arg( sPrefix ).arg( s ) ) + .append( QString( "%1%2m_pEventQueue: stringification not implemented\n" ).arg( sPrefix ).arg( s ) ); +#ifdef H2CORE_HAVE_LADSPA + sOutput.append( QString( "%1%2m_fFXPeak_L: [" ).arg( sPrefix ).arg( s ) ); + for ( auto ii : m_fFXPeak_L ) { + sOutput.append( QString( " %1" ).arg( ii ) ); } - } - - reset( false ); - nLastFrame = 0; - - float fBpm; - float fLastBpm = getBpm(); - int nCyclesPerTempo = 5; - int nPrevLastFrame = 0; - - long long nTotalFrames = 0; - - nn = 0; - - while ( getDoubleTick() < m_fSongSizeInTicks ) { - - fBpm = tempoDist( randomEngine ); - - nPrevLastFrame = nLastFrame; - nLastFrame = - static_cast(std::round( static_cast(nLastFrame) * - static_cast(fLastBpm) / - static_cast(fBpm) )); - - setNextBpm( fBpm ); - updateBpmAndTickSize(); - - for ( int cc = 0; cc < nCyclesPerTempo; ++cc ) { - nFrames = frameDist( randomEngine ); - - incrementTransportPosition( nFrames ); - - if ( ! testCheckTransportPosition( "[testTransportProcessing] variable tempo" ) ) { - setState( AudioEngine::State::Ready ); - unlock(); - return bNoMismatch; - } - - if ( ( cc > 0 && getFrames() - nFrames != nLastFrame ) || - // errors in the rescaling of nLastFrame are omitted. - ( cc == 0 && - abs( ( getFrames() - nFrames - nLastFrame ) / - getFrames() ) > 1e-8 ) ) { - qDebug() << QString( "[testTransportProcessing] [variable tempo] inconsistent frame update. getFrames(): %1, nFrames: %2, nLastFrame: %3, cc: %4, fLastBpm: %5, fBpm: %6, nPrevLastFrame: %7" ) - .arg( getFrames() ).arg( nFrames ) - .arg( nLastFrame ).arg( cc ) - .arg( fLastBpm, 0, 'f' ).arg( fBpm, 0, 'f' ) - .arg( nPrevLastFrame ); - bNoMismatch = false; - setState( AudioEngine::State::Ready ); - unlock(); - return bNoMismatch; - } - - nLastFrame = getFrames(); - - // 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 ( getFrames() - m_nFrameOffset != nTotalFrames ) { - qDebug() << QString( "[testTransportProcessing] [variable tempo] frame offset incorrect. getFrames(): %1, m_nFrameOffset: %2, nTotalFrames: %3" ) - .arg( getFrames() ).arg( m_nFrameOffset ).arg( nTotalFrames ); - bNoMismatch = false; - setState( AudioEngine::State::Ready ); - unlock(); - return bNoMismatch; - } + sOutput.append( QString( "]\n%1%2m_fFXPeak_R: [" ).arg( sPrefix ).arg( s ) ); + for ( auto ii : m_fFXPeak_R ) { + sOutput.append( QString( " %1" ).arg( ii ) ); } - - fLastBpm = fBpm; - - nn++; - - if ( nn > nMaxCycles ) { - qDebug() << "[testTransportProcessing] [variable tempo] end of the song wasn't reached in time."; - bNoMismatch = false; - break; + sOutput.append( QString( " ]\n" ) ); +#endif + sOutput.append( QString( "%1%2m_fMasterPeak_L: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fMasterPeak_L ) ) + .append( QString( "%1%2m_fMasterPeak_R: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fMasterPeak_R ) ) + .append( QString( "%1%2m_fProcessTime: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fProcessTime ) ) + .append( QString( "%1%2m_fMaxProcessTime: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fMaxProcessTime ) ) + .append( QString( "%1%2m_fLadspaTime: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fLadspaTime ) ) + .append( QString( "%1%2m_nRealtimeFrame: %3\n" ).arg( sPrefix ).arg( s ).arg( m_nRealtimeFrame ) ) + .append( QString( "%1%2m_AudioProcessCallback: stringification not implemented\n" ).arg( sPrefix ).arg( s ) ) + .append( QString( "%1%2m_songNoteQueue: length = %3\n" ).arg( sPrefix ).arg( s ).arg( m_songNoteQueue.size() ) ); + sOutput.append( QString( "%1%2m_midiNoteQueue: [\n" ).arg( sPrefix ).arg( s ) ); + for ( const auto& nn : m_midiNoteQueue ) { + sOutput.append( nn->toQString( sPrefix + s, bShort ) ); } + sOutput.append( QString( "]\n%1%2m_pMetronomeInstrument: %3\n" ).arg( sPrefix ).arg( s ).arg( m_pMetronomeInstrument->toQString( sPrefix + s, bShort ) ) ) + .append( QString( "%1%2nMaxTimeHumanize: %3\n" ).arg( sPrefix ).arg( s ).arg( AudioEngine::nMaxTimeHumanize ) ) + .append( QString( "%1%2fHumanizeVelocitySD: %3\n" ).arg( sPrefix ).arg( s ).arg( AudioEngine::fHumanizeVelocitySD ) ) + .append( QString( "%1%2fHumanizePitchSD: %3\n" ).arg( sPrefix ).arg( s ).arg( AudioEngine::fHumanizePitchSD ) ) + .append( QString( "%1%2fHumanizeTimingSD: %3\n" ).arg( sPrefix ).arg( s ).arg( AudioEngine::fHumanizeTimingSD ) ); + } - - setState( AudioEngine::State::Ready ); - - unlock(); - - pCoreActionController->activateTimeline( true ); - pCoreActionController->addTempoMarker( 0, 120 ); - pCoreActionController->addTempoMarker( 1, 100 ); - pCoreActionController->addTempoMarker( 2, 20 ); - pCoreActionController->addTempoMarker( 3, 13.4 ); - pCoreActionController->addTempoMarker( 4, 383.2 ); - pCoreActionController->addTempoMarker( 5, 64.38372 ); - pCoreActionController->addTempoMarker( 6, 96.3 ); - pCoreActionController->addTempoMarker( 7, 240.46 ); - pCoreActionController->addTempoMarker( 8, 200.1 ); - - lock( RIGHT_HERE ); - setState( AudioEngine::State::Testing ); - - // Check consistency after switching on the Timeline - if ( ! testCheckTransportPosition( "[testTransportProcessing] timeline: off" ) ) { - bNoMismatch = false; - } - - nn = 0; - nLastFrame = 0; - - while ( getDoubleTick() < m_fSongSizeInTicks ) { - - nFrames = frameDist( randomEngine ); - - incrementTransportPosition( nFrames ); - - if ( ! testCheckTransportPosition( "[testTransportProcessing] timeline" ) ) { - bNoMismatch = false; - break; + else { + sOutput = QString( "%1[AudioEngine]" ).arg( sPrefix ) + .append( ", m_pTransportPosition:\n"); + if ( m_pTransportPosition != nullptr ) { + sOutput.append( QString( "%1" ) + .arg( m_pTransportPosition->toQString( sPrefix, bShort ) ) ); + } else { + sOutput.append( QString( "nullptr\n" ) ); } - - if ( getFrames() - nFrames != nLastFrame ) { - qDebug() << QString( "[testTransportProcessing] [timeline] inconsistent frame update. getFrames(): %1, nFrames: %2, nLastFrame: %3" ) - .arg( getFrames() ).arg( nFrames ).arg( nLastFrame ); - bNoMismatch = false; - break; + sOutput.append( ", m_pQueuingPosition:\n"); + if ( m_pQueuingPosition != nullptr ) { + sOutput.append( QString( "%1" ) + .arg( m_pQueuingPosition->toQString( sPrefix, bShort ) ) ); + } else { + sOutput.append( QString( "nullptr\n" ) ); } - nLastFrame = getFrames(); - - nn++; - - if ( nn > nMaxCycles ) { - qDebug() << "[testTransportProcessing] [timeline] end of the song wasn't reached in time."; - bNoMismatch = false; - break; - } - } - - setState( AudioEngine::State::Ready ); - - unlock(); - - // Check consistency after switching on the Timeline - pCoreActionController->activateTimeline( false ); - - lock( RIGHT_HERE ); - setState( AudioEngine::State::Testing ); - - if ( ! testCheckTransportPosition( "[testTransportProcessing] timeline: off" ) ) { - bNoMismatch = false; - } - - reset( false ); - - setState( AudioEngine::State::Ready ); - - unlock(); - - // Check consistency of playback in PatternMode - pCoreActionController->activateSongMode( false ); - - lock( RIGHT_HERE ); - setState( AudioEngine::State::Testing ); - - nLastFrame = 0; - fLastBpm = 0; - nTotalFrames = 0; - - int nDifferentTempos = 10; - - for ( int tt = 0; tt < nDifferentTempos; ++tt ) { - - fBpm = tempoDist( randomEngine ); - - nLastFrame = std::round( nLastFrame * fLastBpm / fBpm ); - - setNextBpm( fBpm ); - updateBpmAndTickSize(); - - fLastBpm = fBpm; - - for ( int cc = 0; cc < nCyclesPerTempo; ++cc ) { - nFrames = frameDist( randomEngine ); - - incrementTransportPosition( nFrames ); - - if ( ! testCheckTransportPosition( "[testTransportProcessing] pattern mode" ) ) { - setState( AudioEngine::State::Ready ); - unlock(); - pCoreActionController->activateSongMode( true ); - return bNoMismatch; - } - - if ( ( cc > 0 && getFrames() - nFrames != nLastFrame ) || - // errors in the rescaling of nLastFrame are omitted. - ( cc == 0 && abs( getFrames() - nFrames - nLastFrame ) > 1 ) ) { - qDebug() << QString( "[testTransportProcessing] [pattern mode] inconsistent frame update. getFrames(): %1, nFrames: %2, nLastFrame: %3" ) - .arg( getFrames() ).arg( nFrames ).arg( nLastFrame ); - bNoMismatch = false; - setState( AudioEngine::State::Ready ); - unlock(); - pCoreActionController->activateSongMode( true ); - return bNoMismatch; - } - - nLastFrame = getFrames(); - - // 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 ( getFrames() - m_nFrameOffset != nTotalFrames ) { - qDebug() << QString( "[testTransportProcessing] [pattern mode] frame offset incorrect. getFrames(): %1, m_nFrameOffset: %2, nTotalFrames: %3" ) - .arg( getFrames() ).arg( m_nFrameOffset ).arg( nTotalFrames ); - bNoMismatch = false; - setState( AudioEngine::State::Ready ); - unlock(); - pCoreActionController->activateSongMode( true ); - return bNoMismatch; - } - } - } - - reset( false ); - - setState( AudioEngine::State::Ready ); - - unlock(); - pCoreActionController->activateSongMode( true ); - - return bNoMismatch; -} - -bool AudioEngine::testTransportRelocation() { - auto pHydrogen = Hydrogen::get_instance(); - auto pPref = Preferences::get_instance(); - - lock( RIGHT_HERE ); - - // Seed with a real random value, if available - std::random_device randomSeed; - - // Choose a random mean between 1 and 6 - std::default_random_engine randomEngine( randomSeed() ); - std::uniform_real_distribution tickDist( 0, m_fSongSizeInTicks ); - std::uniform_int_distribution frameDist( 0, pPref->m_nBufferSize ); - - // For this call the AudioEngine still needs to be in state - // Playing or Ready. - reset( false ); - - setState( AudioEngine::State::Testing ); - - // Check consistency of updated frames and ticks while relocating - // transport. - double fNewTick; - long long nNewFrame; - - bool bNoMismatch = true; - - int nProcessCycles = 100; - for ( int nn = 0; nn < nProcessCycles; ++nn ) { - - if ( nn < nProcessCycles - 1 ) { - fNewTick = tickDist( randomEngine ); - } else { - // There was a rounding error at this particular tick. - fNewTick = 960; - } - - locate( fNewTick, false ); - - if ( ! testCheckTransportPosition( "[testTransportRelocation] mismatch tick-based" ) ) { - bNoMismatch = false; - break; - } - - // Frame-based relocation - nNewFrame = frameDist( randomEngine ); - locateToFrame( nNewFrame ); - - if ( ! testCheckTransportPosition( "[testTransportRelocation] mismatch frame-based" ) ) { - bNoMismatch = false; - break; - } - } - - reset( false ); - - setState( AudioEngine::State::Ready ); - - unlock(); - - - return bNoMismatch; -} - -bool AudioEngine::testComputeTickInterval() { - auto pHydrogen = Hydrogen::get_instance(); - auto pPref = Preferences::get_instance(); - - lock( RIGHT_HERE ); - - // Seed with a real random value, if available - std::random_device randomSeed; - - // Choose a random mean between 1 and 6 - std::default_random_engine randomEngine( randomSeed() ); - std::uniform_real_distribution frameDist( 1, pPref->m_nBufferSize ); - std::uniform_real_distribution tempoDist( MIN_BPM, MAX_BPM ); - - // For this call the AudioEngine still needs to be in state - // Playing or Ready. - reset( false ); - - setState( AudioEngine::State::Testing ); - - // Check consistency of tick intervals processed in - // updateNoteQueue() (no overlap and no holes). We pretend to - // receive transport position updates of random size (as e.g. used - // in PulseAudio). - - double fTickStart, fTickEnd; - double fLastTickStart = 0; - double fLastTickEnd = 0; - long long nLeadLagFactor; - long long nLastLeadLagFactor = 0; - int nFrames; - - bool bNoMismatch = true; - - int nProcessCycles = 100; - for ( int nn = 0; nn < nProcessCycles; ++nn ) { - - nFrames = frameDist( randomEngine ); - - nLeadLagFactor = computeTickInterval( &fTickStart, &fTickEnd, - nFrames ); - - if ( nLastLeadLagFactor != 0 && - // Since we move a region on two mismatching grids (frame - // and tick), it's okay if the calculated is not - // perfectly constant. For certain tick ranges more - // frames are enclosed than for others (Moire effect). - std::abs( nLastLeadLagFactor - nLeadLagFactor ) > 1 ) { - qDebug() << QString( "[testComputeTickInterval] [constant tempo] There should not be altering lead lag with constant tempo [new: %1, prev: %2].") - .arg( nLeadLagFactor ).arg( nLastLeadLagFactor ); - bNoMismatch = false; - } - nLastLeadLagFactor = nLeadLagFactor; - - if ( nn == 0 && fTickStart != 0 ){ - qDebug() << QString( "[testComputeTickInterval] [constant tempo] First interval [%1,%2] does not start at 0.") - .arg( fTickStart, 0, 'f' ).arg( fTickEnd, 0, 'f' ); - bNoMismatch = false; - } - - if ( fTickStart != fLastTickEnd ) { - qDebug() << QString( "[testComputeTickInterval] [variable tempo] Interval [%1,%2] does not align with previous one [%3,%4]. nFrames: %5, curr tick: %6, curr frames: %7, bpm: %8, tick size: %9, nLeadLagFactor: %10") - .arg( fTickStart, 0, 'f' ) - .arg( fTickEnd, 0, 'f' ) - .arg( fLastTickStart, 0, 'f' ) - .arg( fLastTickEnd, 0, 'f' ) - .arg( nFrames ) - .arg( getDoubleTick(), 0, 'f' ) - .arg( getFrames() ) - .arg( getBpm(), 0, 'f' ) - .arg( getTickSize(), 0, 'f' ) - .arg( nLeadLagFactor ); - bNoMismatch = false; - } - - fLastTickStart = fTickStart; - fLastTickEnd = fTickEnd; - - incrementTransportPosition( nFrames ); - } - - reset( false ); - - fLastTickStart = 0; - fLastTickEnd = 0; - - float fBpm; - - int nTempoChanges = 20; - int nProcessCyclesPerTempo = 5; - for ( int tt = 0; tt < nTempoChanges; ++tt ) { - - fBpm = tempoDist( randomEngine ); - setNextBpm( fBpm ); - - for ( int cc = 0; cc < nProcessCyclesPerTempo; ++cc ) { - - nFrames = frameDist( randomEngine ); - - nLeadLagFactor = computeTickInterval( &fTickStart, &fTickEnd, - nFrames ); - - if ( cc == 0 && tt == 0 && fTickStart != 0 ){ - qDebug() << QString( "[testComputeTickInterval] [variable tempo] First interval [%1,%2] does not start at 0.") - .arg( fTickStart, 0, 'f' ) - .arg( fTickEnd, 0, 'f' ); - bNoMismatch = false; - break; - } - - if ( fTickStart != fLastTickEnd ) { - qDebug() << QString( "[variable tempo] Interval [%1,%2] does not align with previous one [%3,%4]. nFrames: %5, curr tick: %6, curr frames: %7, bpm: %8, tick size: %9, nLeadLagFactor: %10") - .arg( fTickStart, 0, 'f' ) - .arg( fTickEnd, 0, 'f' ) - .arg( fLastTickStart, 0, 'f' ) - .arg( fLastTickEnd, 0, 'f' ) - .arg( nFrames ) - .arg( getDoubleTick(), 0, 'f' ) - .arg( getFrames() ) - .arg( getBpm(), 0, 'f' ) - .arg( getTickSize(), 0, 'f' ) - .arg( nLeadLagFactor ); - bNoMismatch = false; - break; - } - - fLastTickStart = fTickStart; - fLastTickEnd = fTickEnd; - - incrementTransportPosition( nFrames ); - } - - if ( ! bNoMismatch ) { - break; - } - } - - reset( false ); - - setState( AudioEngine::State::Ready ); - - unlock(); - - - return bNoMismatch; -} - -bool AudioEngine::testSongSizeChange() { - - auto pHydrogen = Hydrogen::get_instance(); - auto pCoreActionController = pHydrogen->getCoreActionController(); - auto pSong = pHydrogen->getSong(); - - lock( RIGHT_HERE ); - reset( false ); - setState( AudioEngine::State::Testing ); - - unlock(); - pCoreActionController->locateToColumn( 4 ); - lock( RIGHT_HERE ); - - if ( ! testCheckConsistency( 1, 1, "[testSongSizeChange] prior" ) ) { - setState( AudioEngine::State::Ready ); - unlock(); - return false; - } - - // Toggle a grid cell after to the current transport position - if ( ! testCheckConsistency( 6, 6, "[testSongSizeChange] after" ) ) { - setState( AudioEngine::State::Ready ); - unlock(); - return false; - } - - // Now we head to the "same" position inside the song but with the - // transport being looped once. - int nTestColumn = 4; - long nNextTick = pHydrogen->getTickForColumn( nTestColumn ); - if ( nNextTick == -1 ) { - qDebug() << QString( "[testSongSizeChange] Bad test design: there is no column [%1]" ) - .arg( nTestColumn ); - setState( AudioEngine::State::Ready ); - unlock(); - return false; - } - - nNextTick += pSong->lengthInTicks(); - - unlock(); - pCoreActionController->activateLoopMode( true ); - pCoreActionController->locateToTick( nNextTick ); - lock( RIGHT_HERE ); - - if ( ! testCheckConsistency( 1, 1, "[testSongSizeChange] looped:prior" ) ) { - setState( AudioEngine::State::Ready ); - unlock(); - return false; - } - - // Toggle a grid cell after to the current transport position - if ( ! testCheckConsistency( 6, 6, "[testSongSizeChange] looped:after" ) ) { - setState( AudioEngine::State::Ready ); - unlock(); - return false; - } - - setState( AudioEngine::State::Ready ); - unlock(); - - return true; -} - -bool AudioEngine::testSongSizeChangeInLoopMode() { - auto pHydrogen = Hydrogen::get_instance(); - auto pCoreActionController = pHydrogen->getCoreActionController(); - auto pPref = Preferences::get_instance(); - - pCoreActionController->activateTimeline( false ); - pCoreActionController->activateLoopMode( true ); - - lock( RIGHT_HERE ); - - int nColumns = pHydrogen->getSong()->getPatternGroupVector()->size(); - - // Seed with a real random value, if available - std::random_device randomSeed; - - // Choose a random mean between 1 and 6 - std::default_random_engine randomEngine( randomSeed() ); - std::uniform_real_distribution frameDist( 1, pPref->m_nBufferSize ); - std::uniform_int_distribution columnDist( nColumns, nColumns + 100 ); - - // For this call the AudioEngine still needs to be in state - // Playing or Ready. - reset( false ); - - setState( AudioEngine::State::Testing ); - - uint32_t nFrames = 500; - double fInitialSongSize = m_fSongSizeInTicks; - int nNewColumn; - - bool bNoMismatch = true; - - int nNumberOfTogglings = 1; - - for ( int nn = 0; nn < nNumberOfTogglings; ++nn ) { - - locate( fInitialSongSize + frameDist( randomEngine ) ); - - if ( ! testCheckTransportPosition( "[testSongSizeChangeInLoopMode] relocation" ) ) { - bNoMismatch = false; - break; - } - - incrementTransportPosition( nFrames ); - - if ( ! testCheckTransportPosition( "[testSongSizeChangeInLoopMode] first increment" ) ) { - bNoMismatch = false; - break; - } - - nNewColumn = columnDist( randomEngine ); - - unlock(); - pCoreActionController->toggleGridCell( nNewColumn, 0 ); - lock( RIGHT_HERE ); - - if ( ! testCheckTransportPosition( "[testSongSizeChangeInLoopMode] first toggling" ) ) { - bNoMismatch = false; - break; - } - - if ( fInitialSongSize == m_fSongSizeInTicks ) { - qDebug() << QString( "[testSongSizeChangeInLoopMode] [first toggling] no song enlargement %1") - .arg( m_fSongSizeInTicks ); - bNoMismatch = false; - break; - } - - incrementTransportPosition( nFrames ); - - if ( ! testCheckTransportPosition( "[testSongSizeChange] second increment" ) ) { - bNoMismatch = false; - break; - } - - unlock(); - pCoreActionController->toggleGridCell( nNewColumn, 0 ); - lock( RIGHT_HERE ); - - if ( ! testCheckTransportPosition( "[testSongSizeChange] second toggling" ) ) { - bNoMismatch = false; - break; - } - - if ( fInitialSongSize != m_fSongSizeInTicks ) { - qDebug() << QString( "[testSongSizeChange] [second toggling] song size mismatch original: %1, new: %2" ) - .arg( fInitialSongSize ).arg( m_fSongSizeInTicks ); - bNoMismatch = false; - break; - } - - incrementTransportPosition( nFrames ); - - if ( ! testCheckTransportPosition( "[testSongSizeChange] third increment" ) ) { - bNoMismatch = false; - break; - } - } - - setState( AudioEngine::State::Ready ); - - unlock(); - - return bNoMismatch; -} - -bool AudioEngine::testNoteEnqueuing() { - auto pHydrogen = Hydrogen::get_instance(); - auto pSong = pHydrogen->getSong(); - auto pCoreActionController = pHydrogen->getCoreActionController(); - auto pPref = Preferences::get_instance(); - - pCoreActionController->activateTimeline( false ); - pCoreActionController->activateLoopMode( false ); - pCoreActionController->activateSongMode( true ); - lock( RIGHT_HERE ); - - // Seed with a real random value, if available - std::random_device randomSeed; - - // Choose a random mean between 1 and 6 - std::default_random_engine randomEngine( randomSeed() ); - std::uniform_int_distribution frameDist( pPref->m_nBufferSize / 2, - pPref->m_nBufferSize ); - - // For this call the AudioEngine still needs to be in state - // Playing or Ready. - reset( false ); - - 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; - double fCheckTick; - long long nCheckFrame, nLastFrame = 0; - - bool bNoMismatch = true; - - // 2112 is the number of ticks within the test song. - int nMaxCycles = - std::max( std::ceil( 2112.0 / - static_cast(pPref->m_nBufferSize) * - getTickSize() * 4.0 ), - 2112.0 ); - - // Larger number to account for both small buffer sizes and long - // samples. - int nMaxCleaningCycles = 5000; - int nn = 0; - - // Ensure the sampler is clean. - while ( getSampler()->isRenderingNotes() ) { - processAudio( pPref->m_nBufferSize ); - incrementTransportPosition( pPref->m_nBufferSize ); - ++nn; - - // {//DEBUG - // QString msg = QString( "[song mode] nn: %1, note:" ).arg( nn ); - // auto pNoteQueue = getSampler()->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 ) { - qDebug() << "[testNoteEnqueuing] [song mode] Sampler is in weird state"; - return false; - } - } - locate( 0 ); - - nn = 0; - - bool bEndOfSongReached = false; - - auto notesInSong = pSong->getAllNotes(); - - std::vector> notesInSongQueue; - std::vector> notesInSamplerQueue; - - while ( getDoubleTick() < m_fSongSizeInTicks ) { - - nFrames = frameDist( randomEngine ); - - if ( ! bEndOfSongReached ) { - if ( updateNoteQueue( nFrames ) == -1 ) { - bEndOfSongReached = true; - } - } - - // Add freshly enqueued notes. - testMergeQueues( ¬esInSongQueue, - testCopySongNoteQueue() ); - - processAudio( nFrames ); - - testMergeQueues( ¬esInSamplerQueue, - getSampler()->getPlayingNotesQueue() ); - - incrementTransportPosition( nFrames ); - - ++nn; - if ( nn > nMaxCycles ) { - qDebug() << QString( "[testNoteEnqueuing] end of the song wasn't reached in time. getFrames(): %1, ticks: %2, getTickSize(): %3, m_fSongSizeInTicks: %4, nMaxCycles: %5" ) - .arg( getFrames() ) - .arg( getDoubleTick(), 0, 'f' ) - .arg( getTickSize(), 0, 'f' ) - .arg( m_fSongSizeInTicks, 0, 'f' ) - .arg( nMaxCycles ); - bNoMismatch = false; - break; - } - } - - if ( notesInSongQueue.size() != - notesInSong.size() ) { - QString sMsg = QString( "[testNoteEnqueuing] [song mode] Mismatch between notes count in Song [%1] and NoteQueue [%2]. Song:\n" ) - .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() ) ); - } - - qDebug() << sMsg; - bNoMismatch = false; - } - - // 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] [song mode] Mismatch between notes count in Song [%1] and Sampler [%2]. Song:\n" ) - .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() ) ); - } - - qDebug() << sMsg; - bNoMismatch = false; - } - - setState( AudioEngine::State::Ready ); - - unlock(); - - if ( ! bNoMismatch ) { - return bNoMismatch; - } - - ////////////////////////////////////////////////////////////////// - // Perform the test in pattern mode - ////////////////////////////////////////////////////////////////// - - pCoreActionController->activateSongMode( false ); - pHydrogen->setPatternMode( Song::PatternMode::Selected ); - pHydrogen->setSelectedPatternNumber( 4 ); - - lock( RIGHT_HERE ); - - // For this call the AudioEngine still needs to be in state - // Playing or Ready. - reset( false ); - - setState( AudioEngine::State::Testing ); - - int nLoops = 5; - - nMaxCycles = MAX_NOTES * 2 * nLoops; - nn = 0; - - // Ensure the sampler is clean. - while ( getSampler()->isRenderingNotes() ) { - processAudio( pPref->m_nBufferSize ); - incrementTransportPosition( pPref->m_nBufferSize ); - ++nn; - - // {//DEBUG - // QString msg = QString( "[pattern mode] nn: %1, note:" ).arg( nn ); - // auto pNoteQueue = getSampler()->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 ) { - qDebug() << "[testNoteEnqueuing] [pattern mode] Sampler is in weird state"; - return false; - } - } - locate( 0 ); - - auto pPattern = - pSong->getPatternList()->get( pHydrogen->getSelectedPatternNumber() ); - if ( pPattern == nullptr ) { - qDebug() << QString( "[testNoteEnqueuing] null pattern selected [%1]" ) - .arg( pHydrogen->getSelectedPatternNumber() ); - return false; - } - - std::vector> notesInPattern; - for ( int ii = 0; ii < nLoops; ++ii ) { - FOREACH_NOTE_CST_IT_BEGIN_END( pPattern->get_notes(), it ) { - if ( it->second != nullptr ) { - auto note = std::make_shared( it->second ); - note->set_position( note->get_position() + - ii * pPattern->get_length() ); - notesInPattern.push_back( note ); - } - } - } - - notesInSongQueue.clear(); - notesInSamplerQueue.clear(); - - nMaxCycles = - static_cast(std::max( static_cast(pPattern->get_length()) * - static_cast(nLoops) * - getTickSize() * 4 / - static_cast(pPref->m_nBufferSize), - static_cast(MAX_NOTES) * - static_cast(nLoops) )); - nn = 0; - - while ( getDoubleTick() < pPattern->get_length() * nLoops ) { - - nFrames = frameDist( randomEngine ); - - updateNoteQueue( nFrames ); - - // Add freshly enqueued notes. - testMergeQueues( ¬esInSongQueue, - testCopySongNoteQueue() ); - - processAudio( nFrames ); - - testMergeQueues( ¬esInSamplerQueue, - getSampler()->getPlayingNotesQueue() ); - - incrementTransportPosition( nFrames ); - - ++nn; - if ( nn > nMaxCycles ) { - qDebug() << QString( "[testNoteEnqueuing] end of the pattern wasn't reached in time. getFrames(): %1, ticks: %2, getTickSize(): %3, pattern length: %4, nMaxCycles: %5, nLoops: %6" ) - .arg( getFrames() ) - .arg( getDoubleTick(), 0, 'f' ) - .arg( getTickSize(), 0, 'f' ) - .arg( pPattern->get_length() ) - .arg( nMaxCycles ) - .arg( nLoops ); - bNoMismatch = false; - break; - } - } - - // Transport in pattern mode is always looped. We have to pop the - // notes added during the second run due to the lookahead. - int nNoteNumber = notesInSongQueue.size(); - for( int ii = 0; ii < nNoteNumber; ++ii ) { - auto note = notesInSongQueue[ nNoteNumber - 1 - ii ]; - if ( note != nullptr && - note->get_position() >= pPattern->get_length() * nLoops ) { - notesInSongQueue.pop_back(); - } else { - break; - } - } - - nNoteNumber = notesInSamplerQueue.size(); - for( int ii = 0; ii < nNoteNumber; ++ii ) { - auto note = notesInSamplerQueue[ nNoteNumber - 1 - ii ]; - if ( note != nullptr && - note->get_position() >= pPattern->get_length() * nLoops ) { - notesInSamplerQueue.pop_back(); - } else { - break; - } - } - - if ( notesInSongQueue.size() != - notesInPattern.size() ) { - QString sMsg = QString( "[testNoteEnqueuing] [pattern mode] Mismatch between notes count in Pattern [%1] and NoteQueue [%2]. Pattern:\n" ) - .arg( notesInPattern.size() ).arg( notesInSongQueue.size() ); - for ( int ii = 0; ii < notesInPattern.size(); ++ii ) { - auto note = notesInPattern[ 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() ) ); - } - - qDebug() << sMsg; - bNoMismatch = false; - } - - // 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() != - notesInPattern.size() && - pPref->m_nBufferSize < 1024 ) { - QString sMsg = QString( "[testNoteEnqueuing] [pattern mode] Mismatch between notes count in Pattern [%1] and Sampler [%2]. Pattern:\n" ) - .arg( notesInPattern.size() ).arg( notesInSamplerQueue.size() ); - for ( int ii = 0; ii < notesInPattern.size(); ++ii ) { - auto note = notesInPattern[ 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() ) ); - } - - qDebug() << sMsg; - bNoMismatch = false; - } - - setState( AudioEngine::State::Ready ); - - unlock(); - - ////////////////////////////////////////////////////////////////// - // Perform the test in looped pattern 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 ); - - lock( RIGHT_HERE ); - - // For this call the AudioEngine still needs to be in state - // Playing or Ready. - reset( false ); - - setState( AudioEngine::State::Testing ); - - nLoops = 1; - nCheckFrame = 0; - nLastFrame = 0; - - // 2112 is the number of ticks within the test song. - nMaxCycles = - std::max( std::ceil( 2112.0 / - static_cast(pPref->m_nBufferSize) * - getTickSize() * 4.0 ), - 2112.0 ) * - ( nLoops + 1 ); - - nn = 0; - // Ensure the sampler is clean. - while ( getSampler()->isRenderingNotes() ) { - processAudio( pPref->m_nBufferSize ); - incrementTransportPosition( pPref->m_nBufferSize ); - ++nn; - - // {//DEBUG - // QString msg = QString( "[song mode] [loop mode] nn: %1, note:" ).arg( nn ); - // auto pNoteQueue = getSampler()->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 ) { - qDebug() << "[testNoteEnqueuing] [loop mode] Sampler is in weird state"; - return false; - } - } - locate( 0 ); - - nn = 0; - - bEndOfSongReached = false; - - notesInSong.clear(); - for ( int ii = 0; ii <= nLoops; ++ii ) { - auto notesVec = pSong->getAllNotes(); - for ( auto nnote : notesVec ) { - nnote->set_position( nnote->get_position() + - ii * m_fSongSizeInTicks ); - } - notesInSong.insert( notesInSong.end(), notesVec.begin(), notesVec.end() ); - } - - notesInSongQueue.clear(); - notesInSamplerQueue.clear(); - - while ( getDoubleTick() < m_fSongSizeInTicks * ( nLoops + 1 ) ) { - - nFrames = frameDist( randomEngine ); - - // Turn off loop mode once we entered the last loop cycle. - if ( ( getDoubleTick() > m_fSongSizeInTicks * nLoops + 100 ) && - pSong->getLoopMode() == Song::LoopMode::Enabled ) { - INFOLOG( QString( "\n\ndisabling loop mode\n\n" ) ); - pCoreActionController->activateLoopMode( false ); - } - - if ( ! bEndOfSongReached ) { - if ( updateNoteQueue( nFrames ) == -1 ) { - bEndOfSongReached = true; - } - } - - // Add freshly enqueued notes. - testMergeQueues( ¬esInSongQueue, - testCopySongNoteQueue() ); - - processAudio( nFrames ); - - testMergeQueues( ¬esInSamplerQueue, - getSampler()->getPlayingNotesQueue() ); - - incrementTransportPosition( nFrames ); - - ++nn; - if ( nn > nMaxCycles ) { - qDebug() << QString( "[testNoteEnqueuing] [loop mode] end of the song wasn't reached in time. getFrames(): %1, ticks: %2, getTickSize(): %3, m_fSongSizeInTicks: %4, nMaxCycles: %5" ) - .arg( getFrames() ) - .arg( getDoubleTick(), 0, 'f' ) - .arg( getTickSize(), 0, 'f' ) - .arg( m_fSongSizeInTicks, 0, 'f' ) - .arg( nMaxCycles ); - bNoMismatch = false; - break; - } - } - - if ( notesInSongQueue.size() != - notesInSong.size() ) { - QString sMsg = QString( "[testNoteEnqueuing] [loop mode] Mismatch between notes count in Song [%1] and NoteQueue [%2]. Song:\n" ) - .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() ) ); - } - - qDebug().noquote() << sMsg; - bNoMismatch = false; - } - - // 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] [loop mode] Mismatch between notes count in Song [%1] and Sampler [%2]. Song:\n" ) - .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() ) ); - } - - qDebug().noquote() << sMsg; - bNoMismatch = false; - } - - setState( AudioEngine::State::Ready ); - - unlock(); - - return bNoMismatch; -} - -void AudioEngine::testMergeQueues( std::vector>* noteList, std::vector> 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(newNote.get()) ); - } - } -} - -// Used for the Sampler note queue -void AudioEngine::testMergeQueues( std::vector>* noteList, std::vector 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(newNote) ); - } - } -} - -bool AudioEngine::testCheckTransportPosition( const QString& sContext) const { - - double fTickMismatch; - long long nCheckFrame = computeFrameFromTick( getDoubleTick(), &fTickMismatch ); - double fCheckTick = computeTickFromFrame( getFrames() );// + fTickMismatch ); - - if ( abs( fCheckTick + fTickMismatch - getDoubleTick() ) > 1e-9 || - abs( fTickMismatch - m_fTickMismatch ) > 1e-9 || - nCheckFrame != getFrames() ) { - qDebug() << QString( "[testCheckTransportPosition] [%9] mismatch. frame: %1, check frame: %2, tick: %3, check tick: %4, offset: %5, check offset: %6, tick size: %7, bpm: %8, fCheckTick + fTickMismatch - getDoubleTick(): %10, fTickMismatch - m_fTickMismatch: %11, nCheckFrame - getFrames(): %12" ) - .arg( getFrames() ) - .arg( nCheckFrame ) - .arg( getDoubleTick(), 0 , 'f', 9 ) - .arg( fCheckTick, 0 , 'f', 9 ) - .arg( m_fTickMismatch, 0 , 'f', 9 ) - .arg( fTickMismatch, 0 , 'f', 9 ) - .arg( getTickSize(), 0 , 'f' ) - .arg( getBpm(), 0 , 'f' ) - .arg( sContext ) - .arg( fCheckTick + fTickMismatch - getDoubleTick(), 0, 'E' ) - .arg( fTickMismatch - m_fTickMismatch, 0, 'E' ) - .arg( nCheckFrame - getFrames() ); - return false; -} - - return true; -} - -bool AudioEngine::testCheckAudioConsistency( const std::vector> oldNotes, - const std::vector> newNotes, - const QString& sContext, - int nPassedFrames, bool bTestAudio, - float fPassedTicks ) const { - - bool bNoMismatch = true; - double fPassedFrames = static_cast(nPassedFrames); - auto pSong = Hydrogen::get_instance()->getSong(); - - 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() != - Hydrogen::get_instance()->getAudioOutput()->getSampleRate() || - ppOldNote->get_total_pitch() != 0.0 ) { - // In here we assume the layer pitcyh is zero. - fPassedFrames = static_cast(nPassedFrames) * - Note::pitchToFrequency( ppOldNote->get_total_pitch() ) * - static_cast(ppOldNote->getSample( nn )->get_sample_rate()) / - static_cast(Hydrogen::get_instance()->getAudioOutput()->getSampleRate()); - } - - int nSampleFrames = ( ppNewNote->get_instrument()->get_component( nn ) - ->get_layer( pSelectedLayer->SelectedLayer )->get_sample()->get_frames() ); - double fExpectedFrames = - std::min( static_cast(pSelectedLayer->SamplePosition) + - fPassedFrames, - static_cast(nSampleFrames) ); - if ( std::abs( ppNewNote->get_layer_selected( nn )->SamplePosition - - fExpectedFrames ) > 1 ) { - qDebug() << QString( "[testCheckAudioConsistency] [%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( Hydrogen::get_instance()->getAudioOutput()->getSampleRate() ) - .arg( ppNewNote->get_layer_selected( nn )->SamplePosition - - fExpectedFrames, 0, 'g', 30 ); - bNoMismatch = false; - } - } - } else { - // Check whether changes in note start position - // were properly applied. - if ( ppNewNote->get_position() - fPassedTicks != - ppOldNote->get_position() ) { - qDebug() << QString( "[testCheckAudioConsistency] [%4] glitch in note queue.\n\nPre: %1 ;\n\nPost: %2 ; with passed ticks: %3\n" ) - .arg( ppOldNote->toQString( "", true ) ) - .arg( ppNewNote->toQString( "", true ) ) - .arg( fPassedTicks ) - .arg( sContext ); - bNoMismatch = false; - } - } - } - } - } - - // 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 ) { - qDebug() << QString( "[testCheckAudioConsistency] [%1] bad test design. No notes played back." ) - .arg( sContext ); - if ( oldNotes.size() != 0 ) { - qDebug() << "old notes:"; - for ( auto const& nnote : oldNotes ) { - qDebug() << nnote->toQString( " ", true ); - } - } - if ( newNotes.size() != 0 ) { - qDebug() << "new notes:"; - for ( auto const& nnote : newNotes ) { - qDebug() << nnote->toQString( " ", true ); - } - } - qDebug() << QString( "[testCheckAudioConsistency] curr tick: %1, curr frame: %2, nPassedFrames: %3, fPassedTicks: %4, fTickSize: %5" ) - .arg( getDoubleTick(), 0, 'f' ) - .arg( getFrames() ) - .arg( nPassedFrames ) - .arg( fPassedTicks, 0, 'f' ) - .arg( getTickSize(), 0, 'f' ); - qDebug() << "[testCheckAudioConsistency] notes in song:"; - for ( auto const& nnote : pSong->getAllNotes() ) { - qDebug() << nnote->toQString( " ", true ); - } - - bNoMismatch = false; - } - - return bNoMismatch; -} - -std::vector> AudioEngine::testCopySongNoteQueue() { - std::vector rawNotes; - std::vector> notes; - for ( ; ! m_songNoteQueue.empty(); m_songNoteQueue.pop() ) { - rawNotes.push_back( m_songNoteQueue.top() ); - notes.push_back( std::make_shared( m_songNoteQueue.top() ) ); - } - - for ( auto nnote : rawNotes ) { - m_songNoteQueue.push( nnote ); - } - - return notes; -} - -bool AudioEngine::testCheckConsistency( int nToggleColumn, int nToggleRow, const QString& sContext ) { - auto pHydrogen = Hydrogen::get_instance(); - auto pCoreActionController = pHydrogen->getCoreActionController(); - auto pSong = pHydrogen->getSong(); - - unsigned long nBufferSize = pHydrogen->getAudioOutput()->getBufferSize(); - - - updateNoteQueue( nBufferSize ); - - auto prevNotes = testCopySongNoteQueue(); - - processAudio( nBufferSize ); - - // Cache some stuff in order to compare it later on. - long nOldSongSize = pSong->lengthInTicks(); - float fPrevTempo = getBpm(); - float fPrevTickSize = getTickSize(); - double fPrevTickStart, fPrevTickEnd; - long long nPrevLeadLag; - - // We need to reset this variable in order for - // computeTickInterval() to behave like just after a relocation. - m_fLastTickIntervalEnd = -1; - nPrevLeadLag = computeTickInterval( &fPrevTickStart, &fPrevTickEnd, nBufferSize ); - - std::vector> notes1, notes2; - for ( const auto& ppNote : getSampler()->getPlayingNotesQueue() ) { - notes1.push_back( std::make_shared( ppNote ) ); - } - - ////// - // Toggle a grid cell prior to the current transport position - ////// - - unlock(); - pCoreActionController->toggleGridCell( nToggleColumn, nToggleRow ); - lock( RIGHT_HERE ); - - QString sFirstContext = QString( "[testCheckConsistency] %1 : 1. toggling" ).arg( sContext ); - - // Check whether there is a change in song size - long nNewSongSize = pSong->lengthInTicks(); - if ( nNewSongSize == nOldSongSize ) { - qDebug() << QString( "[%1] no change in song size" ) - .arg( sFirstContext ); - return false; - } - - // Check whether current frame and tick information are still - // consistent. - if ( ! testCheckTransportPosition( sFirstContext ) ) { - return false; - } - - // m_songNoteQueue have been updated properly. - auto afterNotes = testCopySongNoteQueue(); - - if ( ! testCheckAudioConsistency( prevNotes, afterNotes, sFirstContext, - 0, false, m_fTickOffset ) ) { - return false; - } - double fTickStart, fTickEnd; - long long nLeadLag; - - // We need to reset this variable in order for - // computeTickInterval() to behave like just after a relocation. - m_fLastTickIntervalEnd = -1; - nLeadLag = computeTickInterval( &fTickStart, &fTickEnd, nBufferSize ); - if ( std::abs( nLeadLag - nPrevLeadLag ) > 1 ) { - qDebug() << QString( "[%3] LeadLag should be constant since there should be change in tick size. old: %1, new: %2" ) - .arg( nPrevLeadLag ).arg( nLeadLag ).arg( sFirstContext ); - return false; - } - if ( std::abs( fTickStart - m_fTickOffset - fPrevTickStart ) > 4e-3 ) { - qDebug() << QString( "[%4] Mismatch in the start of the tick interval handled by updateNoteQueue new: %1, old: %2, old+offset: %3" ) - .arg( fTickStart, 0, 'f' ).arg( fPrevTickStart, 0, 'f' ) - .arg( fPrevTickStart + m_fTickOffset, 0, 'f' ) - .arg( sFirstContext ); - return false; - } - if ( std::abs( fTickEnd - m_fTickOffset - fPrevTickEnd ) > 4e-3 ) { - qDebug() << QString( "[%4] Mismatch in the end of the tick interval handled by updateNoteQueue new: %1, old: %2, old+offset: %3" ) - .arg( fTickEnd, 0, 'f' ).arg( fPrevTickEnd, 0, 'f' ) - .arg( fPrevTickEnd + m_fTickOffset, 0, 'f' ) - .arg( sFirstContext ); - return false; - } - - // 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. - incrementTransportPosition( nBufferSize ); - processAudio( nBufferSize ); - incrementTransportPosition( nBufferSize ); - processAudio( nBufferSize ); - - // Check whether tempo and tick size have not changed. - if ( fPrevTempo != getBpm() || fPrevTickSize != getTickSize() ) { - qDebug() << QString( "[%1] tempo and ticksize are affected" ) - .arg( sFirstContext ); - return false; - } - - for ( const auto& ppNote : getSampler()->getPlayingNotesQueue() ) { - notes2.push_back( std::make_shared( ppNote ) ); - } - - if ( ! testCheckAudioConsistency( notes1, notes2, sFirstContext, - nBufferSize * 2 ) ) { - return false; - } - - ////// - // Toggle the same grid cell again - ////// - - QString sSecondContext = QString( "[testCheckAudioConsistency] %1 : 2. toggling" ).arg( sContext ); - - notes1.clear(); - for ( const auto& ppNote : getSampler()->getPlayingNotesQueue() ) { - notes1.push_back( std::make_shared( ppNote ) ); - } - - // We deal with a slightly artificial situation regarding - // m_fLastTickIntervalEnd in here. Usually, in addition to - // incrementTransportPosition() and processAudio() - // updateNoteQueue() would have been called too. This would update - // m_fLastTickIntervalEnd which is not done in here. This way we - // emulate a situation that occurs when encountering a change in - // ticksize (passing a tempo marker or a user interaction with the - // BPM widget) just before the song size changed. - double fPrevLastTickIntervalEnd = m_fLastTickIntervalEnd; - nPrevLeadLag = computeTickInterval( &fPrevTickStart, &fPrevTickEnd, nBufferSize ); - m_fLastTickIntervalEnd = fPrevLastTickIntervalEnd; - - unlock(); - pCoreActionController->toggleGridCell( nToggleColumn, nToggleRow ); - lock( RIGHT_HERE ); - - // Check whether there is a change in song size - nOldSongSize = nNewSongSize; - nNewSongSize = pSong->lengthInTicks(); - if ( nNewSongSize == nOldSongSize ) { - qDebug() << QString( "[%1] no change in song size" ) - .arg( sSecondContext ); - return false; - } - - // Check whether current frame and tick information are still - // consistent. - if ( ! testCheckTransportPosition( sSecondContext ) ) { - return false; - } - - // Check whether the notes already enqueued into the - // m_songNoteQueue have been updated properly. - prevNotes.clear(); - prevNotes = testCopySongNoteQueue(); - if ( ! testCheckAudioConsistency( afterNotes, prevNotes, sSecondContext, - 0, false, m_fTickOffset ) ) { - return false; - } - - nLeadLag = computeTickInterval( &fTickStart, &fTickEnd, nBufferSize ); - if ( std::abs( nLeadLag - nPrevLeadLag ) > 1 ) { - qDebug() << QString( "[%3] LeadLag should be constant since there should be change in tick size. old: %1, new: %2" ) - .arg( nPrevLeadLag ).arg( nLeadLag ).arg( sSecondContext ); - return false; - } - if ( std::abs( fTickStart - m_fTickOffset - fPrevTickStart ) > 4e-3 ) { - qDebug() << QString( "[%4] Mismatch in the start of the tick interval handled by updateNoteQueue new: %1, old: %2, old+offset: %3" ) - .arg( fTickStart, 0, 'f' ).arg( fPrevTickStart, 0, 'f' ) - .arg( fPrevTickStart + m_fTickOffset, 0, 'f' ) - .arg( sSecondContext ); - return false; - } - if ( std::abs( fTickEnd - m_fTickOffset - fPrevTickEnd ) > 4e-3 ) { - qDebug() << QString( "[%4] Mismatch in the end of the tick interval handled by updateNoteQueue new: %1, old: %2, old+offset: %3" ) - .arg( fTickEnd, 0, 'f' ).arg( fPrevTickEnd, 0, 'f' ) - .arg( fPrevTickEnd + m_fTickOffset, 0, 'f' ) - .arg( sSecondContext ); - return false; - } - - // 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. - incrementTransportPosition( nBufferSize ); - processAudio( nBufferSize ); - incrementTransportPosition( nBufferSize ); - processAudio( nBufferSize ); - - // Check whether tempo and tick size have not changed. - if ( fPrevTempo != getBpm() || fPrevTickSize != getTickSize() ) { - qDebug() << QString( "[%1] tempo and ticksize are affected" ) - .arg( sSecondContext ); - return false; - } - - notes2.clear(); - for ( const auto& ppNote : getSampler()->getPlayingNotesQueue() ) { - notes2.push_back( std::make_shared( ppNote ) ); - } - - if ( ! testCheckAudioConsistency( notes1, notes2, sSecondContext, - nBufferSize * 2 ) ) { - return false; - } - - return true; -} - -QString AudioEngine::toQString( const QString& sPrefix, bool bShort ) const { - QString s = Base::sPrintIndention; - QString sOutput; - if ( ! bShort ) { - sOutput = QString( "%1[AudioEngine]\n" ).arg( sPrefix ) - .append( QString( "%1%2m_nFrames: %3\n" ).arg( sPrefix ).arg( s ).arg( getFrames() ) ) - .append( QString( "%1%2m_fTick: %3\n" ).arg( sPrefix ).arg( s ).arg( getDoubleTick(), 0, 'f' ) ) - .append( QString( "%1%2m_nFrameOffset: %3\n" ).arg( sPrefix ).arg( s ).arg( m_nFrameOffset ) ) - .append( QString( "%1%2m_fTickOffset: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fTickOffset, 0, 'f' ) ) - .append( QString( "%1%2m_fTickSize: %3\n" ).arg( sPrefix ).arg( s ).arg( getTickSize(), 0, 'f' ) ) - .append( QString( "%1%2m_fBpm: %3\n" ).arg( sPrefix ).arg( s ).arg( getBpm(), 0, 'f' ) ) - .append( QString( "%1%2m_fNextBpm: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fNextBpm, 0, 'f' ) ) - .append( QString( "%1%2m_state: %3\n" ).arg( sPrefix ).arg( s ).arg( static_cast(m_state) ) ) - .append( QString( "%1%2m_nextState: %3\n" ).arg( sPrefix ).arg( s ).arg( static_cast(m_nextState) ) ) - .append( QString( "%1%2m_currentTickTime: %3 ms\n" ).arg( sPrefix ).arg( s ).arg( m_currentTickTime.tv_sec * 1000 + m_currentTickTime.tv_usec / 1000) ) - .append( QString( "%1%2m_nPatternStartTick: %3\n" ).arg( sPrefix ).arg( s ).arg( m_nPatternStartTick ) ) - .append( QString( "%1%2m_nPatternTickPosition: %3\n" ).arg( sPrefix ).arg( s ).arg( m_nPatternTickPosition ) ) - .append( QString( "%1%2m_nColumn: %3\n" ).arg( sPrefix ).arg( s ).arg( m_nColumn ) ) - .append( QString( "%1%2m_fSongSizeInTicks: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fSongSizeInTicks, 0, 'f' ) ) - .append( QString( "%1%2m_fTickMismatch: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fTickMismatch, 0, 'f' ) ) - .append( QString( "%1%2m_fLastTickIntervalEnd: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fLastTickIntervalEnd ) ) - .append( QString( "%1%2m_pSampler: \n" ).arg( sPrefix ).arg( s ) ) - .append( QString( "%1%2m_pSynth: \n" ).arg( sPrefix ).arg( s ) ) - .append( QString( "%1%2m_pAudioDriver: \n" ).arg( sPrefix ).arg( s ) ) - .append( QString( "%1%2m_pMidiDriver: \n" ).arg( sPrefix ).arg( s ) ) - .append( QString( "%1%2m_pMidiDriverOut: \n" ).arg( sPrefix ).arg( s ) ) - .append( QString( "%1%2m_pEventQueue: \n" ).arg( sPrefix ).arg( s ) ); -#ifdef H2CORE_HAVE_LADSPA - sOutput.append( QString( "%1%2m_fFXPeak_L: [" ).arg( sPrefix ).arg( s ) ); - for ( auto ii : m_fFXPeak_L ) { - sOutput.append( QString( " %1" ).arg( ii ) ); - } - sOutput.append( QString( "]\n%1%2m_fFXPeak_R: [" ).arg( sPrefix ).arg( s ) ); - for ( auto ii : m_fFXPeak_R ) { - sOutput.append( QString( " %1" ).arg( ii ) ); - } - sOutput.append( QString( " ]\n" ) ); -#endif - sOutput.append( QString( "%1%2m_fMasterPeak_L: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fMasterPeak_L ) ) - .append( QString( "%1%2m_fMasterPeak_R: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fMasterPeak_R ) ) - .append( QString( "%1%2m_fProcessTime: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fProcessTime ) ) - .append( QString( "%1%2m_fMaxProcessTime: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fMaxProcessTime ) ) - .append( QString( "%1%2m_pNextPatterns: %3\n" ).arg( sPrefix ).arg( s ).arg( m_pNextPatterns->toQString( sPrefix + s ), bShort ) ) - .append( QString( "%1%2m_pPlayingPatterns: %3\n" ).arg( sPrefix ).arg( s ).arg( m_pPlayingPatterns->toQString( sPrefix + s ), bShort ) ) - .append( QString( "%1%2m_nRealtimeFrames: %3\n" ).arg( sPrefix ).arg( s ).arg( m_nRealtimeFrames ) ) - .append( QString( "%1%2m_AudioProcessCallback: \n" ).arg( sPrefix ).arg( s ) ) - .append( QString( "%1%2m_songNoteQueue: length = %3\n" ).arg( sPrefix ).arg( s ).arg( m_songNoteQueue.size() ) ); - sOutput.append( QString( "%1%2m_midiNoteQueue: [\n" ).arg( sPrefix ).arg( s ) ); - for ( const auto& nn : m_midiNoteQueue ) { - sOutput.append( nn->toQString( sPrefix + s, bShort ) ); - } - sOutput.append( QString( "]\n%1%2m_pMetronomeInstrument: %3\n" ).arg( sPrefix ).arg( s ).arg( m_pMetronomeInstrument->toQString( sPrefix + s, bShort ) ) ) - .append( QString( "%1%2nMaxTimeHumanize: %3\n" ).arg( sPrefix ).arg( s ).arg( AudioEngine::nMaxTimeHumanize ) ); - - } else { - sOutput = QString( "%1[AudioEngine]" ).arg( sPrefix ) - .append( QString( ", m_nFrames: %1" ).arg( getFrames() ) ) - .append( QString( ", m_fTick: %1" ).arg( getDoubleTick(), 0, 'f' ) ) - .append( QString( ", m_nFrameOffset: %1" ).arg( m_nFrameOffset ) ) - .append( QString( ", m_fTickOffset: %1" ).arg( m_fTickOffset, 0, 'f' ) ) - .append( QString( ", m_fTickSize: %1" ).arg( getTickSize(), 0, 'f' ) ) - .append( QString( ", m_fBpm: %1" ).arg( getBpm(), 0, 'f' ) ) - .append( QString( ", m_fNextBpm: %1" ).arg( m_fNextBpm, 0, 'f' ) ) + sOutput.append( QString( ", m_fNextBpm: %1" ).arg( m_fNextBpm, 0, 'f' ) ) .append( QString( ", m_state: %1" ).arg( static_cast(m_state) ) ) .append( QString( ", m_nextState: %1" ).arg( static_cast(m_nextState) ) ) - .append( QString( ", m_currentTickTime: %1 ms" ).arg( m_currentTickTime.tv_sec * 1000 + m_currentTickTime.tv_usec / 1000) ) - .append( QString( ", m_nPatternStartTick: %1" ).arg( m_nPatternStartTick ) ) - .append( QString( ", m_nPatternTickPosition: %1" ).arg( m_nPatternTickPosition ) ) - .append( QString( ", m_nColumn: %1" ).arg( m_nColumn ) ) .append( QString( ", m_fSongSizeInTicks: %1" ).arg( m_fSongSizeInTicks, 0, 'f' ) ) - .append( QString( ", m_fTickMismatch: %1" ).arg( m_fTickMismatch, 0, 'f' ) ) - .append( QString( ", m_fLastTickIntervalEnd: %1" ).arg( m_fLastTickIntervalEnd ) ) - .append( QString( ", m_pSampler:" ) ) - .append( QString( ", m_pSynth:" ) ) - .append( QString( ", m_pAudioDriver:" ) ) - .append( QString( ", m_pMidiDriver:" ) ) - .append( QString( ", m_pMidiDriverOut:" ) ) - .append( QString( ", m_pEventQueue:" ) ); + .append( QString( ", m_fLastTickEnd: %1" ).arg( m_fLastTickEnd, 0, 'f' ) ) + .append( QString( ", m_bLookaheadApplied: %1" ).arg( m_bLookaheadApplied ) ) + .append( QString( ", m_pSampler: ..." ) ) + .append( QString( ", m_pSynth: ..." ) ) + .append( QString( ", m_pAudioDriver: ..." ) ) + .append( QString( ", m_pMidiDriver: ..." ) ) + .append( QString( ", m_pMidiDriverOut: ..." ) ) + .append( QString( ", m_pEventQueue: ..." ) ); #ifdef H2CORE_HAVE_LADSPA sOutput.append( QString( ", m_fFXPeak_L: [" ) ); for ( auto ii : m_fFXPeak_L ) { @@ -4538,17 +2550,19 @@ QString AudioEngine::toQString( const QString& sPrefix, bool bShort ) const { .append( QString( ", m_fMasterPeak_R: %1" ).arg( m_fMasterPeak_R ) ) .append( QString( ", m_fProcessTime: %1" ).arg( m_fProcessTime ) ) .append( QString( ", m_fMaxProcessTime: %1" ).arg( m_fMaxProcessTime ) ) - .append( QString( ", m_pNextPatterns: %1" ).arg( m_pNextPatterns->toQString( sPrefix + s ), bShort ) ) - .append( QString( ", m_pPlayingPatterns: %1" ).arg( m_pPlayingPatterns->toQString( sPrefix + s ), bShort ) ) - .append( QString( ", m_nRealtimeFrames: %1" ).arg( m_nRealtimeFrames ) ) - .append( QString( ", m_AudioProcessCallback:" ) ) + .append( QString( ", m_fLadspaTime: %1" ).arg( m_fLadspaTime ) ) + .append( QString( ", m_nRealtimeFrame: %1" ).arg( m_nRealtimeFrame ) ) + .append( QString( ", m_AudioProcessCallback: ..." ) ) .append( QString( ", m_songNoteQueue: length = %1" ).arg( m_songNoteQueue.size() ) ); sOutput.append( QString( ", m_midiNoteQueue: [" ) ); for ( const auto& nn : m_midiNoteQueue ) { sOutput.append( nn->toQString( sPrefix + s, bShort ) ); } sOutput.append( QString( "], m_pMetronomeInstrument: id = %1" ).arg( m_pMetronomeInstrument->get_id() ) ) - .append( QString( ", nMaxTimeHumanize: id %1" ).arg( AudioEngine::nMaxTimeHumanize ) ); + .append( QString( ", nMaxTimeHumanize: id %1" ).arg( AudioEngine::nMaxTimeHumanize ) ) + .append( QString( ", fHumanizeVelocitySD: id %1" ).arg( AudioEngine::fHumanizeVelocitySD ) ) + .append( QString( ", fHumanizePitchSD: id %1" ).arg( AudioEngine::fHumanizePitchSD ) ) + .append( QString( ", fHumanizeTimingSD: id %1" ).arg( AudioEngine::fHumanizeTimingSD ) ); } return sOutput; diff --git a/src/core/AudioEngine/AudioEngine.h b/src/core/AudioEngine/AudioEngine.h index bb5be103bc..f03639e8f5 100644 --- a/src/core/AudioEngine/AudioEngine.h +++ b/src/core/AudioEngine/AudioEngine.h @@ -23,13 +23,14 @@ #ifndef AUDIO_ENGINE_H #define AUDIO_ENGINE_H +#include + #include #include #include #include #include #include -#include #include #include @@ -68,40 +69,35 @@ namespace H2Core class PatternList; class Drumkit; class Song; + class TransportPosition; /** - * Audio Engine main class. + * The audio engine deals with two distinct #TransportPosition. The + * first (and most important one) is #m_pTransportPosition which + * indicated the current position of the audio rendering and playhead + * as well as all patterns associated with it. This is also the one + * other parts of Hydrogen are concerned with. * - * It serves as a container for the Sampler and Synth stored in the - * #m_pSampler and #m_pSynth member objects, takes care of transport - * control and note processing, and provides a mutex #m_EngineMutex - * enabling the user to synchronize the access of the Song object and - * the AudioEngine itself. lock() and try_lock() can be called by a - * thread to lock the engine and unlock() to make it accessible for - * other threads once again. + * The second one is #m_pQueuingPosition which is only used + * internally. It is one lookahead ahead of #m_pTransportPosition, + * used for inserting notes into the song queue, and required in order + * to supported lead and lag of notes. Formerly, this second transport + * state was trimmed to a couple of variables making its update less + * expensive. However, this showed to be quite error prone as things + * tend to went out of sync. * - * The audio engine does not have one but two consistent states with - * respect it its member variables. #m_fTick, #m_nFrames, - * #m_fTickOffset, #m_fTickMismatch, #m_fBpm, #m_fTickSize, - * #m_nFrameOffset, #m_state, and #m_nRealtimeFrames are associated - * with the current transport position. #m_fLastTickIntervalEnd, - * #m_nColumn, #m_nPatternSize, #m_nPatternStartTick, and - * #m_nPatternTickPosition determine the current position - * updateNoteQueue() is adding notes from #m_pPlayingPatterns into - * #m_songNoteQueue. Since the latter is ahead of the current - * transport position by a non-constant (tempo-dependent) lookahead, - * both states are out of sync while in playback but in sync again - * once the transport gets relocated (which resets the lookahead). But - * within themselves both states are consistent. + * All tick information (apart from note handling in + * updateNoteQueue()) are handled as double internally. But due to + * historical reasons the GUI and the remainder of the core only + * access a version of the current tick rounded to integer. * * \ingroup docCore docAudioEngine */ -class AudioEngine : public H2Core::TransportInfo, public H2Core::Object +class AudioEngine : public H2Core::Object { H2_OBJECT(AudioEngine) public: - /** Audio Engine states.*/ enum class State { /** * Not even the constructors have been called. @@ -120,7 +116,7 @@ class AudioEngine : public H2Core::TransportInfo, public H2Core::Object pSong); - /* retrieve the midi (input) driver */ MidiInput* getMidiDriver() const; - /* retrieve the midi (output) driver */ MidiOutput* getMidiOutDriver() const; - + std::shared_ptr getMetronomeInstrument() const; void raiseError( unsigned nErrorCode ); @@ -349,24 +332,16 @@ class AudioEngine : public H2Core::TransportInfo, public H2Core::Object getTransportPosition() const; const PatternList* getNextPatterns() const; const PatternList* getPlayingPatterns() const; - long long getRealtimeFrames() const; + long long getRealtimeFrame() const; - const struct timeval& getCurrentTickTime() const; - /** Maximum lead lag factor in ticks. * - * During the humanization the onset of a Note will be moved + * During humanization the onset of a Note will be moved * Note::__lead_lag times the value calculated by this function. */ static double getLeadLagInTicks(); @@ -378,34 +353,25 @@ class AudioEngine : public H2Core::TransportInfo, public H2Core::Object pSong ); + friend void Hydrogen::setSong( std::shared_ptr pSong, bool bRelinking ); /** Is allowed to call removeSong().*/ friend void Hydrogen::removeSong(); /** Is allowed to use locate() to directly set the position in @@ -573,121 +451,81 @@ class AudioEngine : public H2Core::TransportInfo, public H2Core::Object pTransportPosition ); + void calculateTransportOffsetOnBpmChange( std::shared_ptr pTransportPosition ); - void updateBpmAndTickSize(); - - void setPatternTickPosition( long nTick ); - void setColumn( int nColumn ); - void setRealtimeFrames( long long nFrames ); + void setRealtimeFrame( long long nFrame ); + void updatePlayingPatternsPos( std::shared_ptr pPos ); - /** - * Updates the global objects of the audioEngine according to new - * Song. - * - * \param pNewSong Song to load. - */ void setSong( std::shared_ptrpNewSong ); - /** - * Does the necessary cleanup of the global objects in the audioEngine. - */ void removeSong(); void setState( State state ); void setNextState( State state ); State getNextState() const; - /** - * Resets a number of member variables and sets m_state to - * State::Playing. - */ void startPlayback(); - /** - * Resets a number of member variables and sets m_state to - * State::Ready. - */ void stopPlayback(); - /** Relocate using the audio driver. - * - * \param fTick Next transport position in ticks. - * \param bWithJackBroadcast Relocate not using the AudioEngine - * directly but using the JACK server. - */ void locate( const double fTick, bool bWithJackBroadcast = true ); /** * Version of the locate() function intended to be directly used @@ -699,18 +537,32 @@ class AudioEngine : public H2Core::TransportInfo, public H2Core::Object pPos ); + void updateSongTransportPosition( double fTick, long long nFrame, + std::shared_ptr pPos ); + void updatePatternTransportPosition( double fTick, long long nFrame, + std::shared_ptr pPos ); /** - * Updates all notes in #m_songNoteQueue to be still valid after a - * tempo change. - * - * This function will only be used with the #Timeline - * disabled. See handleTimelineChange(). + * Updates all notes in #m_songNoteQueue and #m_midiNoteQueue to + * be still valid after a tempo change. */ void handleTempoChange(); + + /** + * Updates the transport states and all notes in #m_songNoteQueue + * and #m_midiNoteQueue after adding or deleting a TempoMarker or + * enabling/disabling the #Timeline. + * + * If the #Timeline is activated, adding or removing a TempoMarker + * does effectively has the same effects as a relocation with + * respect to the transport position in frames. It's tick + * counterpart, however, is not affected. This function ensures + * they are in sync again. + */ + void handleTimelineChange(); + /** * Updates all notes in #m_songNoteQueue to be still valid after a * change in song size. @@ -723,57 +575,12 @@ class AudioEngine : public H2Core::TransportInfo, public H2Core::Object> oldNotes, - const std::vector> newNotes, - const QString& sContext, - int nPassedFrames, - bool bTestAudio = true, - float fPassedTicks = 0.0 ) const; - /** - * Toggles the grid cell defined by @a nToggleColumn and @a - * nToggleRow twice and checks whether the transport position and - * the audio processing remains consistent. - */ - bool testCheckConsistency( int nToggleColumn, int nToggleRow, const QString& sContext ); - - std::vector> testCopySongNoteQueue(); - /** - * Add every Note in @a newNotes not yet contained in @a noteList - * to the latter. - */ - void testMergeQueues( std::vector>* noteList, - std::vector> newNotes ); - void testMergeQueues( std::vector>* noteList, - std::vector newNotes ); - /** Local instance of the Sampler. */ Sampler* m_pSampler; - /** Local instance of the Synth. */ Synth* m_pSynth; - - /** - * Pointer to the current instance of the audio driver. - */ AudioOutput * m_pAudioDriver; - - /** - * MIDI input - */ MidiInput * m_pMidiDriver; - - /** - * MIDI output - */ MidiOutput * m_pMidiDriverOut; - EventQueue* m_pEventQueue; #if defined(H2CORE_HAVE_LADSPA) || _DOXYGEN_ @@ -819,87 +626,25 @@ class AudioEngine : public H2Core::TransportInfo, public H2Core::Object m_pTransportPosition; + std::shared_ptr m_pQueuingPosition; /** Set to the total number of ticks in a Song.*/ double m_fSongSizeInTicks; - /** - * Patterns to be played next in stacked Song::Mode::Pattern mode. - * - * See updatePlayingPatterns() for details. - */ - PatternList* m_pNextPatterns; - - /** - * PatternList containing all Patterns currently played back. - * - * See updatePlayingPatterns() for details. - */ - PatternList* m_pPlayingPatterns; - /** * Variable keeping track of the transport position in realtime. * * Even if the audio engine is stopped, the variable will be - * incremented (as audioEngine_process() would do at the beginning + * incremented (as audioEngine_process() would do at the beginning * of each cycle) to support realtime keyboard and MIDI event * timing. */ - long long m_nRealtimeFrames; + long long m_nRealtimeFrame; /** * Current state of the H2Core::AudioEngine. @@ -924,24 +669,12 @@ class AudioEngine : public H2Core::TransportInfo, public H2Core::Object m_pMetronomeInstrument; - /** - * Maximum time (in frames) a note's position can be off due to - * the humanization (lead-lag). - */ - static const int nMaxTimeHumanize; float m_fNextBpm; - /** Number of frames TransportInfo::m_nFrames is ahead of - TransportInfo::m_nTick. */ - double m_fTickMismatch; - double m_fTickOffset; - long long m_nFrameOffset; - double m_fLastTickIntervalEnd; - + double m_fLastTickEnd; + bool m_bLookaheadApplied; }; @@ -1017,10 +750,6 @@ inline float AudioEngine::getMaxProcessTime() const { return m_fMaxProcessTime; } -inline const struct timeval& AudioEngine::getCurrentTickTime() const { - return m_currentTickTime; -} - inline AudioEngine::State AudioEngine::getState() const { return m_state; } @@ -1044,52 +773,25 @@ inline MidiOutput* AudioEngine::getMidiOutDriver() const { return m_pMidiDriverOut; } -inline long AudioEngine::getPatternStartTick() const { - return m_nPatternStartTick; -} -inline void AudioEngine::setPatternTickPosition( long nTick ) { - m_nPatternTickPosition = nTick; -} - -inline long AudioEngine::getPatternTickPosition() const { - return m_nPatternTickPosition; -} - -inline void AudioEngine::setColumn( int songPos ) { - m_nColumn = songPos; -} - -inline int AudioEngine::getColumn() const { - return m_nColumn; -} - -inline const PatternList* AudioEngine::getPlayingPatterns() const { - return m_pPlayingPatterns; -} - -inline const PatternList* AudioEngine::getNextPatterns() const { - return m_pNextPatterns; -} - -inline long long AudioEngine::getRealtimeFrames() const { - return m_nRealtimeFrames; +inline long long AudioEngine::getRealtimeFrame() const { + return m_nRealtimeFrame; } -inline void AudioEngine::setRealtimeFrames( long long nFrames ) { - m_nRealtimeFrames = nFrames; +inline void AudioEngine::setRealtimeFrame( long long nFrame ) { + m_nRealtimeFrame = nFrame; } inline float AudioEngine::getNextBpm() const { return m_fNextBpm; } -inline long long AudioEngine::getFrameOffset() const { - return m_nFrameOffset; +inline const std::shared_ptr AudioEngine::getTransportPosition() const { + return m_pTransportPosition; } -inline void AudioEngine::setFrameOffset( long long nFrameOffset ) { - m_nFrameOffset = nFrameOffset; +inline double AudioEngine::getSongSizeInTicks() const { + return m_fSongSizeInTicks; } -inline double AudioEngine::getTickOffset() const { - return m_fTickOffset; +inline std::shared_ptr AudioEngine::getMetronomeInstrument() const { + return m_pMetronomeInstrument; } }; diff --git a/src/core/AudioEngine/AudioEngineTests.cpp b/src/core/AudioEngine/AudioEngineTests.cpp new file mode 100644 index 0000000000..91a8ac2688 --- /dev/null +++ b/src/core/AudioEngine/AudioEngineTests.cpp @@ -0,0 +1,1947 @@ +/* + * Hydrogen + * Copyright(c) 2002-2008 by Alex >Comix< Cominu [comix@users.sourceforge.net] + * Copyright(c) 2008-2021 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 +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 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 frameDist( 1, pPref->m_nBufferSize ); + std::uniform_real_distribution tempoDist( MIN_BPM, MAX_BPM ); + + // For this call the AudioEngine still needs to be in state + // Playing or Ready. + pAE->reset( false ); + 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(pAE->m_fSongSizeInTicks) / + static_cast(pPref->m_nBufferSize) * + static_cast(pTransportPos->getTickSize()) * 4.0 ), + static_cast(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 ); + 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 frameDist( 1, pPref->m_nBufferSize ); + std::uniform_real_distribution tempoDist( MIN_BPM, MAX_BPM ); + + // For this call the AudioEngine still needs to be in state + // Playing or Ready. + pAE->reset( false ); + 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(pAE->m_fSongSizeInTicks) / + static_cast(pPref->m_nBufferSize) * + static_cast(pTransportPos->getTickSize()) * 4.0 ), + static_cast(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 ); + 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 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->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(pPref->m_nBufferSize) * + static_cast(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( fTickEnd ) - static_cast( 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 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 tickDist( 0, pAE->m_fSongSizeInTicks ); + std::uniform_int_distribution frameDist( 0, pPref->m_nBufferSize ); + + // For this call the AudioEngine still needs to be in state + // Playing or Ready. + pAE->reset( false ); + 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->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->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 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 = pHydrogen->getSong()->getPatternGroupVector()->size(); + + std::random_device randomSeed; + std::default_random_engine randomEngine( randomSeed() ); + std::uniform_real_distribution tickDist( 1, pPref->m_nBufferSize ); + std::uniform_int_distribution columnDist( nColumns, nColumns + 100 ); + + // For this call the AudioEngine still needs to be in state + // Playing or Ready. + pAE->reset( false ); + 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 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->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(pAE->m_fSongSizeInTicks) / + static_cast(pPref->m_nBufferSize) * + static_cast(pTransportPos->getTickSize()) * 4.0 ), + static_cast(pAE->m_fSongSizeInTicks) ); + + + // Ensure the sampler is clean. + AudioEngineTests::resetSampler( "testNoteEnqueuing : song mode" ); + + auto notesInSong = pSong->getAllNotes(); + std::vector> notesInSongQueue; + std::vector> notesInSamplerQueue; + + auto retrieveNotes = [&]( const QString& sContext ) { + // Add freshly enqueued notes. + AudioEngineTests::mergeQueues( ¬esInSongQueue, + AudioEngineTests::copySongNoteQueue() ); + pAE->processAudio( nFrames ); + + AudioEngineTests::mergeQueues( ¬esInSamplerQueue, + 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_END( pPattern->get_notes(), it ) { + if ( it->second != nullptr ) { + auto note = std::make_shared( it->second ); + note->set_position( note->get_position() + + ii * pPattern->get_length() ); + notesInSong.push_back( note ); + } + } + } + + notesInSongQueue.clear(); + notesInSamplerQueue.clear(); + + nMaxCycles = + static_cast(std::max( static_cast(pPattern->get_length()) * + static_cast(nLoops) * + pTransportPos->getTickSize() * 4 / + static_cast(pPref->m_nBufferSize), + static_cast(MAX_NOTES) * + static_cast(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>* 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( ¬esInSongQueue ); + popSurplusNotes( ¬esInSamplerQueue ); + + 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->setState( AudioEngine::State::Testing ); + + nLoops = 1; + + nMaxCycles = + std::max( std::ceil( static_cast(pAE->m_fSongSizeInTicks) / + static_cast(pPref->m_nBufferSize) * + static_cast(pTransportPos->getTickSize()) * 4.0 ), + static_cast(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 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->setState( AudioEngine::State::Testing ); + AudioEngineTests::resetSampler( __PRETTY_FUNCTION__ ); + + uint32_t nFrames; + + const int nMaxCycles = + std::max( std::ceil( static_cast(pAE->m_fSongSizeInTicks) / + static_cast(pPref->m_nBufferSize) * + static_cast(pTransportPos->getTickSize()) * 4.0 ), + static_cast(pAE->m_fSongSizeInTicks) ); + + auto notesInSong = pSong->getAllNotes(); + std::vector> notesInSongQueue; + std::vector> 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( ¬esInSongQueue, + AudioEngineTests::copySongNoteQueue() ); + + pAE->processAudio( nFrames ); + + AudioEngineTests::mergeQueues( ¬esInSamplerQueue, + 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->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> *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(pAE->m_fSongSizeInTicks) / + static_cast(pPref->m_nBufferSize) * fStep * + static_cast(pTransportPos->getTickSize()) * 4.0 ), + static_cast(pAE->m_fSongSizeInTicks) ); + const uint32_t nFrames = static_cast( + std::round( static_cast(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 ) { + + 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->setHumanizeTimeValue( fValue ); + pSong->setHumanizeVelocityValue( fValue ); + 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> notesReference; + getNotes( ¬esReference ); + + 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> notesCustomized; + getNotes( ¬esCustomized ); + + 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>* 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* pDeviations, float fTargetSD, const QString& sContext ) { + + float fMean = std::accumulate( pDeviations->begin(), pDeviations->end(), + 0.0, std::plus() ) / + static_cast( 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( 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 deviationsPitch( notesReference.size() ); + std::vector deviationsVelocity( notesReference.size() ); + std::vector 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> notesHumanizedWeak; + getNotes( ¬esHumanizedWeak ); + checkHumanization( 0.2, ¬esHumanizedWeak ); + + setHumanization( 0.8 ); + std::vector> notesHumanizedStrong; + getNotes( ¬esHumanizedStrong ); + checkHumanization( 0.8, ¬esHumanizedStrong ); + + ////////////////////////////////////////////////////////////////// + // 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> notesSwing; + getNotes( ¬esSwing ); + + 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>* noteList, std::vector> 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(newNote.get()) ); + } + } +} + +// Used for the Sampler note queue +void AudioEngineTests::mergeQueues( std::vector>* noteList, std::vector 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(newNote) ); + } + } +} + +void AudioEngineTests::checkTransportPosition( std::shared_ptr 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(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> oldNotes, + const std::vector> 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(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(nPassedFrames) * + Note::pitchToFrequency( ppOldNote->get_total_pitch() ) * + static_cast(ppOldNote->getSample( nn )->get_sample_rate()) / + static_cast(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(pSelectedLayer->SamplePosition) + + fPassedFrames, + static_cast(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> AudioEngineTests::copySongNoteQueue() { + auto pAE = Hydrogen::get_instance()->getAudioEngine(); + std::vector rawNotes; + std::vector> notes; + for ( ; ! pAE->m_songNoteQueue.empty(); pAE->m_songNoteQueue.pop() ) { + rawNotes.push_back( pAE->m_songNoteQueue.top() ); + notes.push_back( std::make_shared( 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> 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( 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( 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( 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 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 ); +} + +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 diff --git a/src/core/AudioEngine/AudioEngineTests.h b/src/core/AudioEngine/AudioEngineTests.h new file mode 100644 index 0000000000..b49cc7631b --- /dev/null +++ b/src/core/AudioEngine/AudioEngineTests.h @@ -0,0 +1,149 @@ +/* + * Hydrogen + * Copyright(c) 2002-2008 by Alex >Comix< Cominu [comix@users.sourceforge.net] + * Copyright(c) 2008-2021 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 + * + */ + +#ifndef AUDIO_ENGINE_TESTS_H +#define AUDIO_ENGINE_TESTS_H + +#include +#include + +#include +#include + +namespace H2Core +{ + + class TransportPosition; + +/** + * Defined in here since it requires access to methods and + * variables private to the #AudioEngine class. + */ +class AudioEngineTests : public H2Core::Object +{ + H2_OBJECT(AudioEngineTests) +public: + /** + * Unit test checking for consistency when converting frames to + * ticks and back. + */ + static void testFrameToTickConversion(); + /** + * Unit test checking the incremental update of the transport + * position in audioEngine_process(). + */ + static void testTransportProcessing(); + /** + * More detailed test of the transport and playhead integrity in + * case Timeline/tempo markers are involved. + */ + static void testTransportProcessingTimeline(); + /** + * Unit test checking the relocation of the transport + * position in audioEngine_process(). + */ + static void testTransportRelocation(); + /** + * Unit test checking that loop mode as well as deactivating it + * works. + */ + static void testLoopMode(); + /** + * Unit test checking consistency of transport position when + * playback was looped at least once and the song size is changed + * by toggling a pattern. + */ + static void testSongSizeChange(); + /** + * Unit test checking consistency of transport position when + * playback was looped at least once and the song size is changed + * by toggling a pattern. + */ + static void testSongSizeChangeInLoopMode(); + /** + * Unit test checking that all notes in a song are picked up once. + */ + static void testNoteEnqueuing(); + + /** + * Checks whether the order of notes enqueued and processed by the + * Sampler is consistent on tempo change. + */ + static void testNoteEnqueuingTimeline(); + /** + * Unit test checking that custom note properties take effect and + * that humanization works as expected. + */ + static void testHumanization(); + +private: + static int processTransport( const QString& sContext, + int nFrames, + long long* nLastLookahead, + long long* nLastTransportFrame, + long long* nTotalFrames, + long* nLastPlayheadTick, + double* fLastTickIntervalEnd, + bool bCheckLookahead = true ); + /** + * Checks the consistency of the transport position @a pPos by + * converting the current tick, frame, column, pattern start tick + * etc. into each other and comparing the results. + * + * \param pPos Transport position to check + * \param sContext String identifying the calling function and + * used for logging + */ + static void checkTransportPosition( std::shared_ptr pPos, const QString& sContext ); + /** + * Takes two instances of Sampler::m_playingNotesQueue and checks + * whether matching notes have exactly @a nPassedFrames difference + * in their SelectedLayerInfo::SamplePosition. + */ + static void checkAudioConsistency( const std::vector> oldNotes, + const std::vector> newNotes, + const QString& sContext, + int nPassedFrames, + bool bTestAudio = true, + float fPassedTicks = 0.0 ); + /** + * Toggles the grid cell defined by @a nToggleColumn and @a + * nToggleRow twice and checks whether the transport position and + * the audio processing remains consistent. + */ + static void toggleAndCheckConsistency( int nToggleColumn, int nToggleRow, const QString& sContext ); + + static std::vector> copySongNoteQueue(); + /** + * Add every Note in @a newNotes not yet contained in @a noteList + * to the latter. + */ + static void mergeQueues( std::vector>* noteList, + std::vector> newNotes ); + static void mergeQueues( std::vector>* noteList, + std::vector newNotes ); + static void resetSampler( const QString& sContext ); + static void throwException( const QString& sMsg ); +}; +}; + +#endif diff --git a/src/core/AudioEngine/TransportInfo.cpp b/src/core/AudioEngine/TransportInfo.cpp deleted file mode 100644 index 943fd111d1..0000000000 --- a/src/core/AudioEngine/TransportInfo.cpp +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Hydrogen - * Copyright(c) 2002-2008 by Alex >Comix< Cominu [comix@users.sourceforge.net] - * Copyright(c) 2008-2021 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 -#include -#include -#include - -namespace H2Core { - -TransportInfo::TransportInfo() - : m_nFrames( 0 ) - , m_fTick( 0 ) - , m_fTickSize( 400 ) - , m_fBpm( 120 ) { -} - - -TransportInfo::~TransportInfo() { -} - -void TransportInfo::setBpm( float fNewBpm ) { - if ( fNewBpm > MAX_BPM ) { - ERRORLOG( QString( "Provided bpm [%1] is too high. Assigning upper bound %2 instead" ) - .arg( fNewBpm ).arg( MAX_BPM ) ); - fNewBpm = MAX_BPM; - } else if ( fNewBpm < MIN_BPM ) { - ERRORLOG( QString( "Provided bpm [%1] is too low. Assigning lower bound %2 instead" ) - .arg( fNewBpm ).arg( MIN_BPM ) ); - fNewBpm = MIN_BPM; - } - - m_fBpm = fNewBpm; - - if ( Preferences::get_instance()->getRubberBandBatchMode() ) { - Hydrogen::get_instance()->recalculateRubberband( getBpm() ); - } -} - -void TransportInfo::setFrames( long long nNewFrames ) { - if ( nNewFrames < 0 ) { - ERRORLOG( QString( "Provided frame [%1] is negative. Setting frame 0 instead." ) - .arg( nNewFrames ) ); - nNewFrames = 0; - } - - m_nFrames = nNewFrames; -} - -void TransportInfo::setTick( double fNewTick ) { - if ( fNewTick < 0 ) { - ERRORLOG( QString( "Provided tick [%1] is negative. Setting frame 0 instead." ) - .arg( fNewTick ) ); - fNewTick = 0; - } - - m_fTick = fNewTick; -} - -void TransportInfo::setTickSize( float fNewTickSize ) { - if ( fNewTickSize <= 0 ) { - ERRORLOG( QString( "Provided tick size [%1] is too small. Using 400 as a fallback instead." ) - .arg( fNewTickSize ) ); - fNewTickSize = 400; - } - - m_fTickSize = fNewTickSize; -} - -}; diff --git a/src/core/AudioEngine/TransportInfo.h b/src/core/AudioEngine/TransportInfo.h deleted file mode 100644 index bb43bcf5f6..0000000000 --- a/src/core/AudioEngine/TransportInfo.h +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Hydrogen - * Copyright(c) 2002-2008 by Alex >Comix< Cominu [comix@users.sourceforge.net] - * Copyright(c) 2008-2021 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 - * - */ -#ifndef TRANSPORT_INFO_H -#define TRANSPORT_INFO_H - -#include - -namespace H2Core -{ - -/** - * Object holding most of the information about the transport state of - * the AudioEngine, like if it is playing or stopped or its current - * transport position and speed. - * - * Due to the original design of Hydrogen the fundamental variable to - * determine the transport position is a tick. Whenever a tempo change - * is encounter, the tick size (in frames per tick) is rescaled. To - * nevertheless ensure compatibility with frame-based audio systems, - * like JACK, this class will also keep track of the frame count - * during BPM changes, relocations etc. This variable is dubbed - * "externalFrames" to indicate that it's not used within Hydrogen but - * to sync it with other apps. - */ -class TransportInfo : public H2Core::Object -{ - H2_OBJECT(TransportInfo) -public: - - /** - * Constructor of TransportInfo - */ - TransportInfo(); - /** Destructor of TransportInfo */ - ~TransportInfo(); - - long long getFrames() const; - double getTick() const; - float getTickSize() const; - float getBpm() const; - -protected: - void setFrames( long long nNewFrames ); - void setTick( double fNewTick ); - void setBpm( float fNewBpm ); - void setTickSize( float fNewTickSize ); - /** All classes other than the AudioEngine should use - AudioEngine::locate(). - */ - -private: - - /** - * Current transport position in number of frames since the - * beginning of the song. - * - * A __frame__ is a single sample of an audio signal. Thus, with a - * _sample rate_ of 48000Hz, 48000 frames will be recorded in one - * second and, with a _buffer size_ = 1024, 1024 consecutive - * frames will be accumulated before they are handed over to the - * audio engine for processing. Internally, the transport is based - * on float precision ticks. (#m_nFrames / #m_fTickSize) Caution: - * when using the Timeline the ticksize does change throughout the - * song. This requires #m_nFrames to be recalculate with - * AudioEngine::updateFrames() each time the transport position is - * relocated (possibly crossing some tempo markers). - */ - long long m_nFrames; - - /** - * Smallest temporal unit used for transport navigation within - * Hydrogen and is calculated using AudioEngine::computeTick(), - * #m_nFrames, and #m_fTickSize. - * - * Note that the smallest unit for positioning a #Note is a frame - * due to the humanization capabilities. - * - * Float is, unfortunately, not enough. When the engine is running - * for a long time the high precision digits after decimal point - * required to keep frames and ticks in sync would be lost. - */ - double m_fTick; - - /** - * Number of frames that make up one tick. - * - * The notes won't be processed frame by frame but, instead, tick - * by tick. Therefore, #m_fTickSize represents the minimum - * duration of a Note as well as the minimum distance between two - * of them. - * - * Calculated using AudioEngine::computeTickSize(). - */ - float m_fTickSize; - /** Current tempo in beats per minute. - * - * The tempo hold by the #TransportInfo (and thus the - * #AudioEngine) is the one currently used throughout Hydrogen. It - * can be set through three different mechanisms and the - * corresponding values are stored in different places. - * - * The most fundamental one is stored in Song::m_fBpm and can be - * set using the BPM widget in the PlayerControl or via MIDI and - * OSC commands. Writing the value to the current #Song is done by - * the latter commands and widget and not within the AudioEngine. - * - * It is superseded by the tempo markers as soon as the #Timeline - * is activated and at least one TempoMarker is set. The current - * speed during Timeline-based transport will not override - * Song::m_fBpm and is stored using the tempo markers in the - * .h2song file instead. (see #Timeline for details) - * - * Both Song and Timeline tempo are superseded by the BPM - * broadcasted by the JACK timebase master application once - * Hydrogen acts as timebase slave. The corresponding value - * depends entirely on the external application and will not be - * stored by Hydrogen. - */ - float m_fBpm; -}; - -inline long long TransportInfo::getFrames() const { - return m_nFrames; -} -inline double TransportInfo::getTick() const { - return m_fTick; -} -inline float TransportInfo::getTickSize() const { - return m_fTickSize; -} -inline float TransportInfo::getBpm() const { - return m_fBpm; -} -}; - -#endif - diff --git a/src/core/AudioEngine/TransportPosition.cpp b/src/core/AudioEngine/TransportPosition.cpp new file mode 100644 index 0000000000..01ccd4cbb2 --- /dev/null +++ b/src/core/AudioEngine/TransportPosition.cpp @@ -0,0 +1,669 @@ +/* + * Hydrogen + * Copyright(c) 2002-2008 by Alex >Comix< Cominu [comix@users.sourceforge.net] + * Copyright(c) 2008-2021 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 +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace H2Core { + +TransportPosition::TransportPosition( const QString sLabel ) + : m_sLabel( sLabel ) +{ + m_pPlayingPatterns = new PatternList(); + m_pPlayingPatterns->setNeedsLock( true ); + m_pNextPatterns = new PatternList(); + m_pNextPatterns->setNeedsLock( true ); + + reset(); +} + +TransportPosition::~TransportPosition() { + delete m_pPlayingPatterns; + delete m_pNextPatterns; +} + +void TransportPosition::set( std::shared_ptr pOther ) { + m_nFrame = pOther->m_nFrame; + m_fTick = pOther->m_fTick; + m_fTickSize = pOther->m_fTickSize; + m_fBpm = pOther->m_fBpm; + m_nPatternStartTick = pOther->m_nPatternStartTick; + m_nPatternTickPosition = pOther->m_nPatternTickPosition; + m_nColumn = pOther->m_nColumn; + m_fTickMismatch = pOther->m_fTickMismatch; + m_nFrameOffsetTempo = pOther->m_nFrameOffsetTempo; + m_fTickOffsetQueuing = pOther->m_fTickOffsetQueuing; + m_fTickOffsetSongSize = pOther->m_fTickOffsetSongSize; + + m_pPlayingPatterns->clear(); + for ( const auto ppattern : *pOther->m_pPlayingPatterns ) { + if ( ppattern != nullptr ) { + m_pPlayingPatterns->add( ppattern ); + ppattern->addFlattenedVirtualPatterns( m_pPlayingPatterns ); + } + } + m_pNextPatterns->clear(); + for ( const auto ppattern : *pOther->m_pNextPatterns ) { + if ( ppattern != nullptr ) { + m_pNextPatterns->add( ppattern ); + ppattern->addFlattenedVirtualPatterns( m_pNextPatterns ); + } + } + m_nPatternSize = pOther->m_nPatternSize; + m_nLastLeadLagFactor = pOther->m_nLastLeadLagFactor; +} + +void TransportPosition::reset() { + m_nFrame = 0; + m_fTick = 0; + m_fTickSize = 400; + m_fBpm = 120; + m_nPatternStartTick = 0; + m_nPatternTickPosition = 0; + m_nColumn = -1; + m_fTickMismatch = 0; + m_nFrameOffsetTempo = 0; + m_fTickOffsetQueuing = 0; + m_fTickOffsetSongSize = 0; + + m_pPlayingPatterns->clear(); + m_pNextPatterns->clear(); + m_nPatternSize = MAX_NOTES; + m_nLastLeadLagFactor = 0; +} + +void TransportPosition::setBpm( float fNewBpm ) { + if ( fNewBpm > MAX_BPM ) { + ERRORLOG( QString( "[%1] Provided bpm [%2] is too high. Assigning upper bound %3 instead" ) + .arg( m_sLabel ).arg( fNewBpm ).arg( MAX_BPM ) ); + fNewBpm = MAX_BPM; + } else if ( fNewBpm < MIN_BPM ) { + ERRORLOG( QString( "[%1] Provided bpm [%2] is too low. Assigning lower bound %3 instead" ) + .arg( m_sLabel ).arg( fNewBpm ).arg( MIN_BPM ) ); + fNewBpm = MIN_BPM; + } + + m_fBpm = fNewBpm; + + if ( Preferences::get_instance()->getRubberBandBatchMode() ) { + Hydrogen::get_instance()->recalculateRubberband( getBpm() ); + } +} + +void TransportPosition::setFrame( long long nNewFrame ) { + if ( nNewFrame < 0 ) { + ERRORLOG( QString( "[%1] Provided frame [%2] is negative. Setting frame 0 instead." ) + .arg( m_sLabel ).arg( nNewFrame ) ); + nNewFrame = 0; + } + + m_nFrame = nNewFrame; +} + +void TransportPosition::setTick( double fNewTick ) { + if ( fNewTick < 0 ) { + ERRORLOG( QString( "[%1] Provided tick [%2] is negative. Setting frame 0 instead." ) + .arg( m_sLabel ).arg( fNewTick ) ); + fNewTick = 0; + } + + m_fTick = fNewTick; +} + +void TransportPosition::setTickSize( float fNewTickSize ) { + if ( fNewTickSize <= 0 ) { + ERRORLOG( QString( "[%1] Provided tick size [%2] is too small. Using 400 as a fallback instead." ) + .arg( m_sLabel ).arg( fNewTickSize ) ); + fNewTickSize = 400; + } + + m_fTickSize = fNewTickSize; +} + +void TransportPosition::setPatternStartTick( long nPatternStartTick ) { + if ( nPatternStartTick < 0 ) { + ERRORLOG( QString( "[%1] Provided tick [%2] is negative. Setting frame 0 instead." ) + .arg( m_sLabel ).arg( nPatternStartTick ) ); + nPatternStartTick = 0; + } + + m_nPatternStartTick = nPatternStartTick; +} + +void TransportPosition::setPatternTickPosition( long nPatternTickPosition ) { + if ( nPatternTickPosition < 0 ) { + ERRORLOG( QString( "[%1] Provided tick [%2] is negative. Setting frame 0 instead." ) + .arg( m_sLabel ).arg( nPatternTickPosition ) ); + nPatternTickPosition = 0; + } + + m_nPatternTickPosition = nPatternTickPosition; +} + +void TransportPosition::setColumn( int nColumn ) { + if ( nColumn < -1 ) { + ERRORLOG( QString( "[%1] Provided column [%2] it too small. Using [-1] as a fallback instead." ) + .arg( m_sLabel ).arg( nColumn ) ); + nColumn = -1; + } + + m_nColumn = nColumn; +} + + +void TransportPosition::setPatternSize( int nPatternSize ) { + if ( nPatternSize < 0 ) { + ERRORLOG( QString( "[%1] Provided pattern size [%2] it too small. Using [0] as a fallback instead." ) + .arg( m_sLabel ).arg( nPatternSize ) ); + nPatternSize = 0; + } + + m_nPatternSize = nPatternSize; +} + +// This function uses the assumption that sample rate and resolution +// are constant over the whole song. +long long TransportPosition::computeFrameFromTick( const double fTick, double* fTickMismatch, int nSampleRate ) { + + const auto pHydrogen = Hydrogen::get_instance(); + const auto pSong = pHydrogen->getSong(); + const auto pTimeline = pHydrogen->getTimeline(); + const auto pAudioEngine = pHydrogen->getAudioEngine(); + assert( pSong ); + + if ( nSampleRate == 0 ) { + nSampleRate = pHydrogen->getAudioOutput()->getSampleRate(); + } + const int nResolution = pSong->getResolution(); + const double fSongSizeInTicks = pAudioEngine->getSongSizeInTicks(); + + if ( nSampleRate == 0 || nResolution == 0 ) { + ERRORLOG( "Not properly initialized yet" ); + *fTickMismatch = 0; + return 0; + } + + if ( fTick == 0 ) { + *fTickMismatch = 0; + return 0; + } + + const auto tempoMarkers = pTimeline->getAllTempoMarkers(); + + // If there are no patterns in the current, we treat song mode + // like pattern mode. + long long nNewFrame = 0; + if ( pHydrogen->isTimelineEnabled() && + ! ( tempoMarkers.size() == 1 && + pTimeline->isFirstTempoMarkerSpecial() ) && + pHydrogen->getMode() == Song::Mode::Song && + pSong->getPatternGroupVector()->size() > 0 ) { + + double fNewTick = fTick; + double fRemainingTicks = fTick; + double fNextTick, fPassedTicks = 0; + double fNextTickSize; + double fNewFrame = 0; + int ii; + + const int nColumns = pSong->getPatternGroupVector()->size(); + + auto handleEnd = [&]() { + // The next frame is within this segment. + fNewFrame += fRemainingTicks * fNextTickSize; + + nNewFrame = static_cast( std::round( fNewFrame ) ); + + // Keep track of the rounding error to be able to switch + // between fTick and its frame counterpart later on. In + // case fTick is located close to a tempo marker we will + // only cover the part up to the tempo marker in here as + // only this region is governed by fNextTickSize. + const double fRoundingErrorInTicks = + ( fNewFrame - static_cast( nNewFrame ) ) / + fNextTickSize; + + // Compares the negative distance between current position + // (fNewFrame) and the one resulting from rounding - + // fRoundingErrorInTicks - with the negative distance + // between current position (fNewFrame) and location of + // next tempo marker. + if ( fRoundingErrorInTicks > + fPassedTicks + fRemainingTicks - fNextTick ) { + // Whole mismatch located within the current tempo + // interval. + *fTickMismatch = fRoundingErrorInTicks; + + } + else { + // Mismatch at this side of the tempo marker. + *fTickMismatch = fPassedTicks + fRemainingTicks - fNextTick; + + const double fFinalFrame = fNewFrame + + ( fNextTick - fPassedTicks - fRemainingTicks ) * fNextTickSize; + + // Mismatch located beyond the tempo marker. + double fFinalTickSize; + if ( ii < tempoMarkers.size() ) { + fFinalTickSize = AudioEngine::computeDoubleTickSize( + nSampleRate, tempoMarkers[ ii ]->fBpm, nResolution ); + } + else { + fFinalTickSize = AudioEngine::computeDoubleTickSize( + nSampleRate, tempoMarkers[ 0 ]->fBpm, nResolution ); + } + + // DEBUGLOG( QString( "[::computeFrameFromTick mismatch : 2] fTickMismatch: [%1 + %2], static_cast(nNewFrame): %3, fNewFrame: %4, fFinalFrame: %5, fNextTickSize: %6, fPassedTicks: %7, fRemainingTicks: %8, fFinalTickSize: %9" ) + // .arg( fPassedTicks + fRemainingTicks - fNextTick ) + // .arg( ( fFinalFrame - static_cast(nNewFrame) ) / fNextTickSize ) + // .arg( nNewFrame ) + // .arg( fNewFrame, 0, 'f' ) + // .arg( fFinalFrame, 0, 'f' ) + // .arg( fNextTickSize, 0, 'f' ) + // .arg( fPassedTicks, 0, 'f' ) + // .arg( fRemainingTicks, 0, 'f' ) + // .arg( fFinalTickSize, 0, 'f' )); + + *fTickMismatch += ( fFinalFrame - static_cast(nNewFrame) ) / + fFinalTickSize; + } + + // DEBUGLOG( QString( "[::computeFrameFromTick end] fTick: %1, fNewFrame: %2, fNextTick: %3, fRemainingTicks: %4, fPassedTicks: %5, fNextTickSize: %6, tempoMarkers[ ii - 1 ]->nColumn: %7, tempoMarkers[ ii - 1 ]->fBpm: %8, nNewFrame: %9, fTickMismatch: %10, frame increment (fRemainingTicks * fNextTickSize): %11, fRoundingErrorInTicks: %12" ) + // .arg( fTick, 0, 'f' ) + // .arg( fNewFrame, 0, 'g', 30 ) + // .arg( fNextTick, 0, 'f' ) + // .arg( fRemainingTicks, 0, 'f' ) + // .arg( fPassedTicks, 0, 'f' ) + // .arg( fNextTickSize, 0, 'f' ) + // .arg( tempoMarkers[ ii - 1 ]->nColumn ) + // .arg( tempoMarkers[ ii - 1 ]->fBpm ) + // .arg( nNewFrame ) + // .arg( *fTickMismatch, 0, 'g', 30 ) + // .arg( fRemainingTicks * fNextTickSize, 0, 'g', 30 ) + // .arg( fRoundingErrorInTicks, 0, 'f' ) + // ); + + fRemainingTicks -= fNewTick - fPassedTicks; + }; + + while ( fRemainingTicks > 0 ) { + + for ( ii = 1; ii <= tempoMarkers.size(); ++ii ) { + if ( ii == tempoMarkers.size() || + tempoMarkers[ ii ]->nColumn >= nColumns ) { + fNextTick = fSongSizeInTicks; + } else { + fNextTick = + static_cast(pHydrogen->getTickForColumn( tempoMarkers[ ii ]->nColumn ) ); + } + + fNextTickSize = + AudioEngine::computeDoubleTickSize( nSampleRate, + tempoMarkers[ ii - 1 ]->fBpm, + nResolution ); + + if ( fRemainingTicks > ( fNextTick - fPassedTicks ) ) { + // The whole segment of the timeline covered by tempo + // marker ii is left of the current transport position. + fNewFrame += ( fNextTick - fPassedTicks ) * fNextTickSize; + + // DEBUGLOG( QString( "[segment] fTick: %1, fNewFrame: %2, fNextTick: %3, fRemainingTicks: %4, fPassedTicks: %5, fNextTickSize: %6, tempoMarkers[ ii - 1 ]->nColumn: %7, tempoMarkers[ ii - 1 ]->fBpm: %8, tick increment (fNextTick - fPassedTicks): %9, frame increment (fRemainingTicks * fNextTickSize): %10" ) + // .arg( fTick, 0, 'f' ) + // .arg( fNewFrame, 0, 'g', 30 ) + // .arg( fNextTick, 0, 'f' ) + // .arg( fRemainingTicks, 0, 'f' ) + // .arg( fPassedTicks, 0, 'f' ) + // .arg( fNextTickSize, 0, 'f' ) + // .arg( tempoMarkers[ ii - 1 ]->nColumn ) + // .arg( tempoMarkers[ ii - 1 ]->fBpm ) + // .arg( fNextTick - fPassedTicks, 0, 'f' ) + // .arg( ( fNextTick - fPassedTicks ) * fNextTickSize, 0, 'g', 30 ) + // ); + + fRemainingTicks -= fNextTick - fPassedTicks; + + fPassedTicks = fNextTick; + + } + else { + handleEnd(); + break; + } + } + + if ( fRemainingTicks > 0 ) { + // The provided fTick is larger than the song. But, + // luckily, we just calculated the song length in + // frames (fNewFrame). + const int nRepetitions = std::floor(fTick / fSongSizeInTicks); + const double fSongSizeInFrames = fNewFrame; + + fNewFrame *= static_cast(nRepetitions); + fNewTick = std::fmod( fTick, fSongSizeInTicks ); + fRemainingTicks = fNewTick; + fPassedTicks = 0; + + // DEBUGLOG( QString( "[repeat] fTick: %1, fNewFrames: %2, fNewTick: %3, fRemainingTicks: %4, nRepetitions: %5, fSongSizeInTicks: %6, fSongSizeInFrames: %7" ) + // .arg( fTick, 0, 'g',30 ) + // .arg( fNewFrame, 0, 'g', 30 ) + // .arg( fNewTick, 0, 'g', 30 ) + // .arg( fRemainingTicks, 0, 'g', 30 ) + // .arg( nRepetitions ) + // .arg( fSongSizeInTicks, 0, 'g', 30 ) + // .arg( fSongSizeInFrames, 0, 'g', 30 ) + // ); + + if ( std::isinf( fNewFrame ) || + static_cast(fNewFrame) > + std::numeric_limits::max() ) { + ERRORLOG( QString( "Provided ticks [%1] are too large." ).arg( fTick ) ); + return 0; + } + + // The target tick matches a multiple of the song + // size. We need to reproduce the context within the + // last tempo marker in order to get the mismatch + // right. + if ( fRemainingTicks == 0 ) { + ii = tempoMarkers.size(); + fNextTick = static_cast(pHydrogen->getTickForColumn( + tempoMarkers[ 0 ]->nColumn ) ); + fNextTickSize = AudioEngine::computeDoubleTickSize( + nSampleRate, tempoMarkers[ ii - 1 ]->fBpm, nResolution ); + + handleEnd(); + } + } + } + } else { + + // As the timeline is not activate, the column passed is of no + // importance. But we harness the ability of getBpmAtColumn() + // to collect and choose between tempo information gathered + // from various sources. + const float fBpm = AudioEngine::getBpmAtColumn( 0 ); + + const double fTickSize = + AudioEngine::computeDoubleTickSize( nSampleRate, fBpm, + nResolution ); + + // Single tempo for the whole song. + const double fNewFrame = static_cast(fTick) * + fTickSize; + nNewFrame = static_cast( std::round( fNewFrame ) ); + *fTickMismatch = ( fNewFrame - static_cast(nNewFrame ) ) / + fTickSize; + + // DEBUGLOG(QString("[no-timeline] nNewFrame: %1, fTick: %2, fTickSize: %3, fTickMismatch: %4" ) + // .arg( nNewFrame ).arg( fTick, 0, 'f' ).arg( fTickSize, 0, 'f' ) + // .arg( *fTickMismatch, 0, 'g', 30 )); + + } + + return nNewFrame; +} + +// This function uses the assumption that sample rate and resolution +// are constant over the whole song. +double TransportPosition::computeTickFromFrame( const long long nFrame, int nSampleRate ) { + const auto pHydrogen = Hydrogen::get_instance(); + + if ( nFrame < 0 ) { + ERRORLOG( QString( "Provided frame [%1] must be non-negative" ).arg( nFrame ) ); + } + + const auto pSong = pHydrogen->getSong(); + const auto pTimeline = pHydrogen->getTimeline(); + const auto pAudioEngine = pHydrogen->getAudioEngine(); + assert( pSong ); + + if ( nSampleRate == 0 ) { + nSampleRate = pHydrogen->getAudioOutput()->getSampleRate(); + } + const int nResolution = pSong->getResolution(); + double fTick = 0; + + const double fSongSizeInTicks = pAudioEngine->getSongSizeInTicks(); + + if ( nSampleRate == 0 || nResolution == 0 ) { + ERRORLOG( "Not properly initialized yet" ); + return fTick; + } + + if ( nFrame == 0 ) { + return fTick; + } + + const auto tempoMarkers = pTimeline->getAllTempoMarkers(); + + // If there are no patterns in the current, we treat song mode + // like pattern mode. + if ( pHydrogen->isTimelineEnabled() && + ! ( tempoMarkers.size() == 1 && + pTimeline->isFirstTempoMarkerSpecial() ) && + pHydrogen->getMode() == Song::Mode::Song && + pSong->getPatternGroupVector()->size() ) { + + // We are using double precision in here to avoid rounding + // errors. + const double fTargetFrame = static_cast(nFrame); + double fPassedFrames = 0; + double fNextFrame = 0; + double fNextTicks, fPassedTicks = 0; + double fNextTickSize; + long long nRemainingFrames; + + const int nColumns = pSong->getPatternGroupVector()->size(); + + while ( fPassedFrames < fTargetFrame ) { + + for ( int ii = 1; ii <= tempoMarkers.size(); ++ii ) { + + fNextTickSize = + AudioEngine::computeDoubleTickSize( nSampleRate, + tempoMarkers[ ii - 1 ]->fBpm, + nResolution ); + + if ( ii == tempoMarkers.size() || + tempoMarkers[ ii ]->nColumn >= nColumns ) { + fNextTicks = fSongSizeInTicks; + } else { + fNextTicks = + static_cast(pHydrogen->getTickForColumn( tempoMarkers[ ii ]->nColumn )); + } + fNextFrame = (fNextTicks - fPassedTicks) * fNextTickSize; + + if ( fNextFrame < ( fTargetFrame - + fPassedFrames ) ) { + + // DEBUGLOG(QString( "[segment] nFrame: %1, fTick: %2, nSampleRate: %3, fNextTickSize: %4, fNextTicks: %5, fNextFrame: %6, tempoMarkers[ ii -1 ]->nColumn: %7, tempoMarkers[ ii -1 ]->fBpm: %8, fPassedTicks: %9, fPassedFrames: %10, fNewTick (tick increment): %11, fNewTick * fNextTickSize (frame increment): %12" ) + // .arg( nFrame ) + // .arg( fTick, 0, 'f' ) + // .arg( nSampleRate ) + // .arg( fNextTickSize, 0, 'f' ) + // .arg( fNextTicks, 0, 'f' ) + // .arg( fNextFrame, 0, 'f' ) + // .arg( tempoMarkers[ ii -1 ]->nColumn ) + // .arg( tempoMarkers[ ii -1 ]->fBpm ) + // .arg( fPassedTicks, 0, 'f' ) + // .arg( fPassedFrames, 0, 'f' ) + // .arg( fNextTicks - fPassedTicks, 0, 'f' ) + // .arg( (fNextTicks - fPassedTicks) * fNextTickSize, 0, 'g', 30 ) + // ); + + // The whole segment of the timeline covered by tempo + // marker ii is left of the transport position. + fTick += fNextTicks - fPassedTicks; + + fPassedFrames += fNextFrame; + fPassedTicks = fNextTicks; + + } else { + // The target frame is located within a segment. + const double fNewTick = (fTargetFrame - fPassedFrames ) / + fNextTickSize; + + fTick += fNewTick; + + // DEBUGLOG(QString( "[end] nFrame: %1, fTick: %2, nSampleRate: %3, fNextTickSize: %4, fNextTicks: %5, fNextFrame: %6, tempoMarkers[ ii -1 ]->nColumn: %7, tempoMarkers[ ii -1 ]->fBpm: %8, fPassedTicks: %9, fPassedFrames: %10, fNewTick (tick increment): %11, fNewTick * fNextTickSize (frame increment): %12" ) + // .arg( nFrame ) + // .arg( fTick, 0, 'f' ) + // .arg( nSampleRate ) + // .arg( fNextTickSize, 0, 'f' ) + // .arg( fNextTicks, 0, 'f' ) + // .arg( fNextFrame, 0, 'f' ) + // .arg( tempoMarkers[ ii -1 ]->nColumn ) + // .arg( tempoMarkers[ ii -1 ]->fBpm ) + // .arg( fPassedTicks, 0, 'f' ) + // .arg( fPassedFrames, 0, 'f' ) + // .arg( fNewTick, 0, 'f' ) + // .arg( fNewTick * fNextTickSize, 0, 'g', 30 ) + // ); + + fPassedFrames = fTargetFrame; + + break; + } + } + + if ( fPassedFrames != fTargetFrame ) { + // The provided nFrame is larger than the song. But, + // luckily, we just calculated the song length in + // frames. + const double fSongSizeInFrames = fPassedFrames; + const int nRepetitions = std::floor(fTargetFrame / fSongSizeInFrames); + if ( fSongSizeInTicks * nRepetitions > + std::numeric_limits::max() ) { + ERRORLOG( QString( "Provided frames [%1] are too large." ).arg( nFrame ) ); + return 0; + } + fTick = fSongSizeInTicks * nRepetitions; + + fPassedFrames = static_cast(nRepetitions) * + fSongSizeInFrames; + fPassedTicks = 0; + + // DEBUGLOG( QString( "[repeat] frames covered: %1, frames remaining: %2, ticks covered: %3, nRepetitions: %4, fSongSizeInFrames: %5, fSongSizeInTicks: %6" ) + // .arg( fPassedFrames, 0, 'g', 30 ) + // .arg( fTargetFrame - fPassedFrames, 0, 'g', 30 ) + // .arg( fTick, 0, 'g', 30 ) + // .arg( nRepetitions ) + // .arg( fSongSizeInFrames, 0, 'g', 30 ) + // .arg( fSongSizeInTicks, 0, 'g', 30 ) + // ); + + } + } + } + else { + // As the timeline is not activate, the column passed is of no + // importance. But we harness the ability of getBpmAtColumn() + // to collect and choose between tempo information gathered + // from various sources. + const float fBpm = AudioEngine::getBpmAtColumn( 0 ); + const double fTickSize = + AudioEngine::computeDoubleTickSize( nSampleRate, fBpm, + nResolution ); + + // Single tempo for the whole song. + fTick = static_cast(nFrame) / fTickSize; + + // DEBUGLOG(QString( "[no timeline] nFrame: %1, sampleRate: %2, tickSize: %3" ) + // .arg( nFrame ).arg( nSampleRate ).arg( fTickSize, 0, 'f' ) ); + + } + + return fTick; +} + +long long TransportPosition::computeFrame( double fTick, float fTickSize ) { + return std::round( fTick * fTickSize ); +} + +double TransportPosition::computeTick( long long nFrame, float fTickSize ) { + return nFrame / fTickSize; +} + +QString TransportPosition::toQString( const QString& sPrefix, bool bShort ) const { + QString s = Base::sPrintIndention; + QString sOutput; + if ( ! bShort ) { + sOutput = QString( "%1[TransportPosition]\n" ).arg( sPrefix ) + .append( QString( "%1%2m_sLabel: %3\n" ).arg( sPrefix ).arg( s ).arg( m_sLabel ) ) + .append( QString( "%1%2m_nFrame: %3\n" ).arg( sPrefix ).arg( s ).arg( getFrame() ) ) + .append( QString( "%1%2m_fTick: %3\n" ).arg( sPrefix ).arg( s ).arg( getDoubleTick(), 0, 'f' ) ) + .append( QString( "%1%2m_fTick (rounded): %3\n" ).arg( sPrefix ).arg( s ).arg( getTick() ) ) + .append( QString( "%1%2m_fTickSize: %3\n" ).arg( sPrefix ).arg( s ).arg( getTickSize(), 0, 'f' ) ) + .append( QString( "%1%2m_fBpm: %3\n" ).arg( sPrefix ).arg( s ).arg( getBpm(), 0, 'f' ) ) + .append( QString( "%1%2m_nPatternStartTick: %3\n" ).arg( sPrefix ).arg( s ).arg( m_nPatternStartTick ) ) + .append( QString( "%1%2m_nPatternTickPosition: %3\n" ).arg( sPrefix ).arg( s ).arg( m_nPatternTickPosition ) ) + .append( QString( "%1%2m_nColumn: %3\n" ).arg( sPrefix ).arg( s ).arg( m_nColumn ) ) + .append( QString( "%1%2m_fTickMismatch: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fTickMismatch, 0, 'f' ) ) + .append( QString( "%1%2m_nFrameOffsetTempo: %3\n" ).arg( sPrefix ).arg( s ).arg( m_nFrameOffsetTempo ) ) + .append( QString( "%1%2m_fTickOffsetQueuing: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fTickOffsetQueuing, 0, 'f' ) ) + .append( QString( "%1%2m_fTickOffsetSongSize: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fTickOffsetSongSize, 0, 'f' ) ); + if ( m_pPlayingPatterns != nullptr ) { + sOutput.append( QString( "%1%2m_pPlayingPatterns: %3\n" ).arg( sPrefix ).arg( s ).arg( m_pPlayingPatterns->toQString( sPrefix + s ), bShort ) ); + } + if ( m_pNextPatterns != nullptr ) { + sOutput.append( QString( "%1%2m_pNextPatterns: %3\n" ).arg( sPrefix ).arg( s ).arg( m_pNextPatterns->toQString( sPrefix + s ), bShort ) ); + } + sOutput.append( QString( "%1%2m_nPatternSize: %3\n" ).arg( sPrefix ).arg( s ).arg( m_nPatternSize ) ) + .append( QString( "%1%2m_nLastLeadLagFactor: %3\n" ).arg( sPrefix ).arg( s ).arg( m_nLastLeadLagFactor ) ); + } + else { + sOutput = QString( "%1[TransportPosition]" ).arg( sPrefix ) + .append( QString( " m_sLabel: %1" ).arg( m_sLabel ) ) + .append( QString( ", m_nFrame: %1" ).arg( getFrame() ) ) + .append( QString( ", m_fTick: %1" ).arg( getDoubleTick(), 0, 'f' ) ) + .append( QString( ", m_fTick (rounded): %1" ).arg( getTick() ) ) + .append( QString( ", m_fTickSize: %1" ).arg( getTickSize(), 0, 'f' ) ) + .append( QString( ", m_fBpm: %1" ).arg( getBpm(), 0, 'f' ) ) + .append( QString( ", m_nPatternStartTick: %1" ).arg( m_nPatternStartTick ) ) + .append( QString( ", m_nPatternTickPosition: %1" ).arg( m_nPatternTickPosition ) ) + .append( QString( ", m_nColumn: %1" ).arg( m_nColumn ) ) + .append( QString( ", m_fTickMismatch: %1" ).arg( m_fTickMismatch, 0, 'f' ) ) + .append( QString( ", m_nFrameOffsetTempo: %1" ).arg( m_nFrameOffsetTempo ) ) + .append( QString( ", m_fTickOffsetQueuing: %1" ).arg( m_fTickOffsetQueuing, 0, 'f' ) ) + .append( QString( ", m_fTickOffsetSongSize: %1" ).arg( m_fTickOffsetSongSize, 0, 'f' ) ); + if ( m_pPlayingPatterns != nullptr ) { + sOutput.append( QString( ", m_pPlayingPatterns: %1" ).arg( m_pPlayingPatterns->toQString( sPrefix + s ), bShort ) ); + } + if ( m_pNextPatterns != nullptr ) { + sOutput.append( QString( ", m_pNextPatterns: %1" ).arg( m_pNextPatterns->toQString( sPrefix + s ), bShort ) ); + } + sOutput.append( QString( ", m_nPatternSize: %1" ).arg( m_nPatternSize ) ) + .append( QString( ", m_nLastLeadLagFactor: %1" ).arg( m_nLastLeadLagFactor ) ); + + } + + return sOutput; +} + +}; diff --git a/src/core/AudioEngine/TransportPosition.h b/src/core/AudioEngine/TransportPosition.h new file mode 100644 index 0000000000..8a81d0fb01 --- /dev/null +++ b/src/core/AudioEngine/TransportPosition.h @@ -0,0 +1,459 @@ +/* + * Hydrogen + * Copyright(c) 2002-2008 by Alex >Comix< Cominu [comix@users.sourceforge.net] + * Copyright(c) 2008-2021 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 + * + */ +#ifndef TRANSPORT_POSITION_H +#define TRANSPORT_POSITION_H + +#include + +#include +#include +#include + +namespace H2Core +{ + +class PatternList; + +/** + * Object holding most of the information about the transport state of + * the AudioEngine. + * + * Due to the original design of Hydrogen the fundamental variable to + * determine the transport position is a tick. Whenever a tempo or + * song size change is encountered, the tick and frame information are + * shifted or rescaled. To nevertheless ensure consistency, the amount + * of compensation required to retrieve the original position is + * stored in a number of dedicated offset variables. + */ +class TransportPosition : public H2Core::Object +{ + H2_OBJECT(TransportPosition) +public: + + TransportPosition( const QString sLabel = "" ); + ~TransportPosition(); + + const QString getLabel() const; + long long getFrame() const; + /** + * Retrieve a rounded version of #m_fTick. + * + * Only within the #AudioEngine ticks are handled as doubles. + * This is required to allow for a seamless transition between + * frames and ticks without any rounding error. All other parts + * use integer values (due to historical reasons). + */ + long getTick() const; + float getTickSize() const; + float getBpm() const; + long getPatternStartTick() const; + long getPatternTickPosition() const; + int getColumn() const; + double getTickMismatch() const; + long long getFrameOffsetTempo() const; + double getTickOffsetQueuing() const; + double getTickOffsetSongSize() const; + const PatternList* getPlayingPatterns() const; + const PatternList* getNextPatterns() const; + int getPatternSize() const; + long long getLastLeadLagFactor() const; + + /** + * Calculates tick equivalent of @a nFrame. + * + * In case the #Timeline is activated, the function takes all + * passed tempo markers into account in order to determine the + * number of ticks passed when letting the #AudioEngine roll for + * @a nFrame frames. + * + * It depends on the sample rate @a nSampleRate and assumes that + * it as well as the resolution to be constant over the whole + * song. + * + * @param nFrame Transport position in frame which should be + * converted into ticks. + * @param nSampleRate If set to 0, the sample rate provided by the + * audio driver will be used. + */ + static double computeTickFromFrame( long long nFrame, int nSampleRate = 0 ); + + /** + * Calculates frame equivalent of @a fTick. + * + * In case the #Timeline is activated, the function takes all + * passed tempo markers into account in order to determine the + * number of frames passed when letting the #AudioEngine roll for + * @a fTick ticks. + * + * It depends on the sample rate @a nSampleRate and assumes that + * it as well as the resolution to be constant over the whole + * song. + * + * @param fTick Current transport position in ticks. + * @param fTickMismatch Since ticks are stored as doubles and there + * is some loss in precision, this variable is used report how + * much @fTick exceeds/is ahead of the resulting frame. + * @param nSampleRate If set to 0, the sample rate provided by the + * audio driver will be used. + * + * @return frame + */ + static long long computeFrameFromTick( double fTick, double* fTickMismatch, int nSampleRate = 0 ); + + /** Formatted string version for debugging purposes. + * \param sPrefix String prefix which will be added in front of + * every new line + * \param bShort Instead of the whole content of all classes + * stored as members just a single unique identifier will be + * displayed without line breaks. + * + * \return String presentation of current object.*/ + QString toQString( const QString& sPrefix = "", bool bShort = true ) const override; + + friend class AudioEngine; + friend class AudioEngineTests; + +private: + /** + * Copying the content of one position into the other is a lot + * cheaper than performing computations, like + * #AudioEngine::updateTransportPosition(), twice. + */ + void set( std::shared_ptr pOther ); + void reset(); + + void setFrame( long long nNewFrame ); + void setTick( double fNewTick ); + void setTickSize( float fNewTickSize ); + void setBpm( float fNewBpm ); + void setPatternStartTick( long nPatternStartTick ); + void setPatternTickPosition( long nPatternTickPosition ); + void setColumn( int nColumn ); + void setFrameOffsetTempo( long long nFrameOffset ); + void setTickOffsetQueuing( double nTickOffset ); + void setTickOffsetSongSize( double fTickOffset ); + void setPlayingPatterns( PatternList* pPatternList ); + void setNextPatterns( PatternList* pPatternList ); + void setPatternSize( int nPatternSize ); + void setLastLeadLagFactor( long long nValue ); + + PatternList* getPlayingPatterns(); + PatternList* getNextPatterns(); + + /** + * Converts ticks into frames under the assumption of a constant + * @a fTickSize (sample rate, tempo, and resolution did not + * change). + * + * As the assumption above does not hold once a tempo marker is + * introduced, computeFrameFromTick() should be used instead while + * this function is only meant for internal use. + */ + static long long computeFrame( double fTick, float fTickSize ); + /** + * Converts frames into ticks under the assumption of a constant + * @a fTickSize (sample rate, tempo, and resolution did not + * change). + * + * As the assumption above does not hold once a tempo marker is + * introduced, computeFrameFromTick() should be used instead while + * this function is only meant for internal use. + */ + static double computeTick( long long nFrame, float fTickSize ); + + double getDoubleTick() const; + + /** Identifier of the transport position. Used to keep different + * instances apart. + */ + const QString m_sLabel; + + /** + * Current transport position in number of frames since the + * beginning of the song. + * + * A __frame__ is a single sample of an audio signal. Thus, with a + * _sample rate_ of 48000Hz, 48000 frames will be recorded in one + * second and, with a _buffer size_ = 1024, 1024 consecutive + * frames will be accumulated before they are handed over to the + * audio engine for processing. Internally, the transport is based + * on ticks. (#m_nFrame / #m_fTickSize) + */ + long long m_nFrame; + + /** + * Current transport position in number of ticks since the + * beginning of the song. + * + * A tick is the smallest temporal unit used for transport, + * navigation, and audio rendering within Hydrogen. (Note that the + * smallest unit for positioning a #Note is a frame due to the + * humanization capabilities.) + * + * Although the precision of this variable is double, only a + * version of it rounded to integer is used outside of the + * #AudioEngine. + * + * Float is, unfortunately, not enough. When the engine is running + * for a long time the high precision digits after decimal point + * required to keep frames and ticks in sync would be lost. + */ + double m_fTick; + + /** + * Number of frames that make up one tick. + * + * Calculated using AudioEngine::computeTickSize(). + */ + float m_fTickSize; + /** Current tempo in beats per minute. + * + * It can be set through three different mechanisms and the + * corresponding values are stored in different places. + * + * 1. The most fundamental one is stored in #Song::m_fBpm and can + * be set using the BPM widget in the #PlayerControl or via MIDI + * and OSC commands. Writing the value to the current #Song is + * done by the latter commands and widget and not within the + * #AudioEngine. + * + * 2. It is superseded by the tempo markers as soon as the + * #Timeline is activated and at least one TempoMarker is set. The + * current speed during Timeline-based transport will not override + * #Song::m_fBpm and is stored using the tempo markers in the + * .h2song file instead. (see #Timeline for details) + * + * 3. Both #Song and #Timeline tempo are superseded by the BPM + * broadcasted by the JACK timebase master application once + * Hydrogen acts as timebase slave. The corresponding value + * depends entirely on the external application and will not be + * stored by Hydrogen. + */ + float m_fBpm; + + /** + * Dicstance in ticks between the beginning of the song and the + * beginning of the current column (#m_nColumn). + * + * The current transport position corresponds + * to #m_fTick = (roughly) #m_nPatternStartTick + + * #m_nPatternTickPosition. + */ + long m_nPatternStartTick; + /** + * Ticks passed since #m_nPatternStartTick. + * + * The current transport position thus corresponds + * to #m_fTick = (roughly) #m_nPatternStartTick + + * #m_nPatternTickPosition. + */ + long m_nPatternTickPosition; + /** + * Specifies the column transport is located in and can be used as + * the index of the current PatternList/column in the + * #Song::m_pPatternGroupSequence. + * + * A value of -1 corresponds to "pattern list could not be found" + * and is used to indicate that transport reached the end of the + * song (with transport not looped). + */ + int m_nColumn; + + /** Number of ticks #m_nFrame is ahead/behind of + * #m_fTick. + * + * This is due to the rounding error introduced when calculating + * the frame counterpart #m_nFrame of #m_fTick using + * computeFrameFromTick(). + * #m_nFrame. + **/ + double m_fTickMismatch; + + /** + * Frame offset introduced when changing the tempo of the song. + * + * In case the #Timeline is deactivate each tempo change does + * alter #m_fTickSize and results in #m_nFrame and #m_fTick to not + * be consistent anymore. We will handle this by compensating the + * difference in frames (#m_fTick is kept constant during a tempo + * change while #m_nFrame gets rescaled) using + * #m_nFrameOffsetTempo as an additive offset internally. + * + * When locating transport or stopping playback both #m_nFrame and + * #m_fTick become synced again and #m_nFrameOffsetTempo gets + * resetted. + * + * Note this is not the frame equivalent of #m_fTickOffsetQueuing. + */ + long long m_nFrameOffsetTempo; + /** + * Tick offset introduced when changing the tempo of the song. + * + * In case the #Timeline is deactivate each tempo change does + * alter #m_fTickSize and results in the start of the new tick + * interval covered for note enqueuing in + * AudioEngine::updateNoteQueue() to not be consistent with the + * previous interval end anymore. Holes or overlaps could lead to + * note misses or double enqueuings. We will handle this by + * compensating the difference in ticks using + * #m_fTickOffsetQueuing as an additive offset internally. + * + * When locating transport or stopping playback both the tick + * interval and #m_fTickOffsetQueuing get resetted. + * + * Note this is not the tick equivalent of #m_nFrameOffsetTempo. + */ + double m_fTickOffsetQueuing; + + /** + * Tick offset introduced when changing the size of the song. + * + * When altering the size of the song, e.g. by enlarging a pattern + * prior to the one currently playing, both #m_nFrame and #m_fTick + * become invalid. We will handle this by compensating the + * difference between the old and new tick position using + * #m_fTickOffsetSongSize as an additive offset internally. In + * addition, #m_nFrameOffsetTempo and #m_fTickOffsetQueuing will + * be used to compensate for the change in the current frame + * position and tick interval end. + * + * When locating transport or stopping playback both #m_nFrame and + * #m_fTick become synced again and #m_fTickOffsetSongSize gets + * resetted. + */ + double m_fTickOffsetSongSize; + + /** + * Patterns used to toggle the ones in #m_pPlayingPatterns in + * Song::PatternMode::Stacked. + * + * If a #Pattern is already playing and added to #m_pNextPatterns, + * it will the removed from #m_pPlayingPatterns next time + * transport is looped to the beginning and vice versa. + * + * See AudioEngine::updatePlayingPatterns() for details. + */ + PatternList* m_pNextPatterns; + + /** + * Contains all Patterns currently played back. + * + * If transport is in #H2Core::Song::Mode::Song, it corresponds + * to the patterns present in column #m_nColumn. + * + * See AudioEngine::updatePlayingPatterns() for details. + */ + PatternList* m_pPlayingPatterns; + + /** + * Maximum size of all patterns in #m_pPlayingPatterns. + * + * If #m_pPlayingPatterns is empty, #H2Core::MAX_NOTES will be + * used as fallback. + */ + int m_nPatternSize; + + /** + * #AudioEngine::getLeadLagInFrames() calculated for the previous + * transport position. + * + * It is required to ensure a smooth update of the queuing + * position in AudioEngine::updateNoteQueue() without any holes or + * overlaps in the covered ticks (while using the #Timeline in + * #Song::Mode::Song). + */ + long long m_nLastLeadLagFactor; +}; + +inline const QString TransportPosition::getLabel() const { + return m_sLabel; +} +inline long long TransportPosition::getFrame() const { + return m_nFrame; +} +inline double TransportPosition::getDoubleTick() const { + return m_fTick; +} +inline long TransportPosition::getTick() const { + return static_cast(std::floor( m_fTick )); +} +inline float TransportPosition::getTickSize() const { + return m_fTickSize; +} +inline float TransportPosition::getBpm() const { + return m_fBpm; +} +inline long TransportPosition::getPatternStartTick() const { + return m_nPatternStartTick; +} +inline long TransportPosition::getPatternTickPosition() const { + return m_nPatternTickPosition; +} +inline int TransportPosition::getColumn() const { + return m_nColumn; +} +inline double TransportPosition::getTickMismatch() const { + return m_fTickMismatch; +} +inline long long TransportPosition::getFrameOffsetTempo() const { + return m_nFrameOffsetTempo; +} +inline void TransportPosition::setFrameOffsetTempo( long long nFrameOffset ) { + m_nFrameOffsetTempo = nFrameOffset; +} +inline double TransportPosition::getTickOffsetQueuing() const { + return m_fTickOffsetQueuing; +} +inline void TransportPosition::setTickOffsetQueuing( double fTickOffset ) { + m_fTickOffsetQueuing = fTickOffset; +} +inline double TransportPosition::getTickOffsetSongSize() const { + return m_fTickOffsetSongSize; +} +inline void TransportPosition::setTickOffsetSongSize( double fTickOffset ) { + m_fTickOffsetSongSize = fTickOffset; +} +inline const PatternList* TransportPosition::getPlayingPatterns() const { + return m_pPlayingPatterns; +} +inline PatternList* TransportPosition::getPlayingPatterns() { + return m_pPlayingPatterns; +} +inline const PatternList* TransportPosition::getNextPatterns() const { + return m_pNextPatterns; +} +inline PatternList* TransportPosition::getNextPatterns() { + return m_pNextPatterns; +} +inline int TransportPosition::getPatternSize() const { + return m_nPatternSize; +} +inline long long TransportPosition::getLastLeadLagFactor() const { + return m_nLastLeadLagFactor; +} +inline void TransportPosition::setLastLeadLagFactor( long long nValue ) { + m_nLastLeadLagFactor = nValue; +} +}; + +#endif + diff --git a/src/core/Basics/Adsr.cpp b/src/core/Basics/Adsr.cpp index 9156d30004..a7e8c8c245 100644 --- a/src/core/Basics/Adsr.cpp +++ b/src/core/Basics/Adsr.cpp @@ -33,72 +33,63 @@ const float fDecayExponent = 0.044796211247505179, fDecayInit = 1.046934808452493870, fDecayYOffset = -0.046934663351557632; +ADSR::ADSR( unsigned int attack, unsigned int decay, float sustain, unsigned int release ) : + m_nAttack( attack ), + m_nDecay( decay ), + m_fSustain( sustain ), + m_nRelease( release ), + m_state( State::Attack ), + m_fFramesInState( 0.0 ), + m_fValue( 0.0 ), + m_fReleaseValue( 0.0 ), + m_fQ( fAttackInit ) +{ + normalise(); +} -inline static float linear_interpolation( float fVal_A, float fVal_B, double fVal ) +ADSR::ADSR( const std::shared_ptr other ) : + m_nAttack( other->m_nAttack ), + m_nDecay( other->m_nDecay ), + m_fSustain( other->m_fSustain ), + m_nRelease( other->m_nRelease ), + m_state( other->m_state ), + m_fFramesInState( other->m_fFramesInState ), + m_fValue( other->m_fValue ), + m_fReleaseValue( other->m_fReleaseValue ) { - return fVal_A * ( 1 - fVal ) + fVal_B * fVal; - //return fVal_A + fVal * ( fVal_B - fVal_A ); - //return fVal_A + ((fVal_B - fVal_A) * fVal); + normalise(); } +ADSR::~ADSR() { } + void ADSR::normalise() { - if (__attack < 0.0) { - __attack = 0.0; + if (m_nAttack < 0.0) { + m_nAttack = 0.0; } - if (__decay < 0.0) { - __decay = 0.0; + if (m_nDecay < 0.0) { + m_nDecay = 0.0; } - if (__sustain < 0.0) { - __sustain = 0.0; + if (m_fSustain < 0.0) { + m_fSustain = 0.0; } - if (__release < 256) { - __release = 256; + if (m_nRelease < 256) { + m_nRelease = 256; } - if (__attack > 100000) { - __attack = 100000; + if (m_nAttack > 100000) { + m_nAttack = 100000; } - if (__decay > 100000) { - __decay = 100000; + if (m_nDecay > 100000) { + m_nDecay = 100000; } - if (__sustain > 1.0) { - __sustain = 1.0; + if (m_fSustain > 1.0) { + m_fSustain = 1.0; } - if (__release > 100256) { - __release = 100256; + if (m_nRelease > 100256) { + m_nRelease = 100256; } } -ADSR::ADSR( unsigned int attack, unsigned int decay, float sustain, unsigned int release ) : - __attack( attack ), - __decay( decay ), - __sustain( sustain ), - __release( release ), - __state( ATTACK ), - __ticks( 0.0 ), - __value( 0.0 ), - __release_value( 0.0 ), - m_fQ( fAttackInit ) -{ - normalise(); -} - -ADSR::ADSR( const std::shared_ptr other ) : - __attack( other->__attack ), - __decay( other->__decay ), - __sustain( other->__sustain ), - __release( other->__release ), - __state( other->__state ), - __ticks( other->__ticks ), - __value( other->__value ), - __release_value( other->__release_value ) -{ - normalise(); -} - -ADSR::~ADSR() { } - - /** * Apply an exponential envelope to a stereo pair of sample fragments. * @@ -181,92 +172,93 @@ inline double applyExponential( const float fExponent, const float fXOffset, con * This function manages the current state of the ADSR state machine, and applies envelope calculations * appropriate to each phase. */ -bool ADSR::applyADSR( float *pLeft, float *pRight, int nFrames, int nReleaseFrame, float fStep ) +bool ADSR::applyADSR( float *pLeft, float *pRight, int nFinalBufferPos, int nReleaseFrame, float fStep ) { - int n = 0; + int nBufferPos = 0; - if ( __state == ATTACK ) { - int nAttackFrames = std::min( nFrames, nReleaseFrame ); - if ( nAttackFrames * fStep > __attack ) { - // Attack must end before nFrames, so trim it - nAttackFrames = ceil( __attack / fStep ); + if ( m_state == State::Attack ) { + int nAttackFrames = std::min( nFinalBufferPos, nReleaseFrame ); + if ( nAttackFrames * fStep > m_nAttack ) { + // Attack must end before nFinalBufferPos, so trim it + nAttackFrames = ceil( m_nAttack / fStep ); } - m_fQ = applyExponential( fAttackExponent, fAttackInit, 0.0, -1.0, - pLeft, pRight, m_fQ, nAttackFrames, __attack, fStep, &__value ); + m_fQ = applyExponential( fAttackExponent, fAttackInit, 0.0, -1.0, + pLeft, pRight, m_fQ, nAttackFrames, m_nAttack, + fStep, &m_fValue ); - n += nAttackFrames; + nBufferPos += nAttackFrames; - __ticks += nAttackFrames * fStep; + m_fFramesInState += nAttackFrames * fStep; - if ( __ticks >= __attack ) { - __ticks = 0; - __state = DECAY; + if ( m_fFramesInState >= m_nAttack ) { + m_fFramesInState = 0; + m_state = State::Decay; m_fQ = fDecayInit; } } - if ( __state == DECAY ) { - int nDecayFrames = std::min( nFrames, nReleaseFrame ) - n; - if ( nDecayFrames * fStep > __decay ) { - nDecayFrames = ceil( __decay / fStep ); + if ( m_state == State::Decay ) { + int nDecayFrames = std::min( nFinalBufferPos, nReleaseFrame ) - nBufferPos; + if ( nDecayFrames * fStep > m_nDecay ) { + nDecayFrames = ceil( m_nDecay / fStep ); } - m_fQ = applyExponential( fDecayExponent, -fDecayYOffset, __sustain, (1.0-__sustain), - &pLeft[n], &pRight[n], m_fQ, nDecayFrames, __decay, fStep, &__value ); + m_fQ = applyExponential( fDecayExponent, -fDecayYOffset, m_fSustain, (1.0-m_fSustain), + &pLeft[nBufferPos], &pRight[nBufferPos], m_fQ, nDecayFrames, m_nDecay, fStep, &m_fValue ); - n += nDecayFrames; - __ticks += nDecayFrames * fStep; + nBufferPos += nDecayFrames; + m_fFramesInState += nDecayFrames * fStep; - if ( __ticks >= __decay ) { - __ticks = 0; - __state = SUSTAIN; + if ( m_fFramesInState >= m_nDecay ) { + m_fFramesInState = 0; + m_state = State::Sustain; } } - if ( __state == SUSTAIN ) { + if ( m_state == State::Sustain ) { - int nSustainFrames = std::min( nFrames, nReleaseFrame ) - n; + int nSustainFrames = std::min( nFinalBufferPos, nReleaseFrame ) - nBufferPos; if ( nSustainFrames != 0 ) { - __value = __sustain; - if ( __sustain != 1.0 ) { + m_fValue = m_fSustain; + if ( m_fSustain != 1.0 ) { for ( int i = 0; i < nSustainFrames; i++ ) { - pLeft[ n + i ] *= __sustain; - pRight[ n + i ] *= __sustain; + pLeft[ nBufferPos + i ] *= m_fSustain; + pRight[ nBufferPos + i ] *= m_fSustain; } } - n += nSustainFrames; + nBufferPos += nSustainFrames; } } - if ( __state != RELEASE && __state != IDLE && n >= nReleaseFrame ) { - __release_value = __value; - __state = RELEASE; - __ticks = 0; + if ( m_state != State::Release && m_state != State::Idle && nBufferPos >= nReleaseFrame ) { + m_fReleaseValue = m_fValue; + m_state = State::Release; + m_fFramesInState = 0; m_fQ = fDecayInit; } - if ( __state == RELEASE ) { + if ( m_state == State::Release ) { - int nReleaseFrames = nFrames - n; - if ( nReleaseFrames * fStep > __release ) { - nReleaseFrames = ceil( __release / fStep ); + int nReleaseFrames = nFinalBufferPos - nBufferPos; + if ( nReleaseFrames * fStep > m_nRelease ) { + nReleaseFrames = ceil( m_nRelease / fStep ); } - m_fQ = applyExponential( fDecayExponent, -fDecayYOffset, 0.0, __release_value, - &pLeft[n], &pRight[n], m_fQ, nReleaseFrames, __release, fStep, &__value ); + m_fQ = applyExponential( fDecayExponent, -fDecayYOffset, 0.0, m_fReleaseValue, + &pLeft[nBufferPos], &pRight[nBufferPos], m_fQ, nReleaseFrames, m_nRelease, fStep, &m_fValue ); - n += nReleaseFrames; - __ticks += nReleaseFrames * fStep; + nBufferPos += nReleaseFrames; + m_fFramesInState += nReleaseFrames * fStep; - if ( __ticks >= __release ) { - __state = IDLE; + if ( m_fFramesInState >= m_nRelease ) { + m_state = State::Idle; } } - if ( __state == IDLE ) { - for ( ; n < nFrames; n++ ) { - pLeft[ n ] = pRight[ n ] = 0.0; + if ( m_state == State::Idle ) { + for ( ; nBufferPos < nFinalBufferPos; nBufferPos++ ) { + pLeft[ nBufferPos ] = pRight[ nBufferPos ] = 0.0; } return true; } @@ -275,20 +267,42 @@ bool ADSR::applyADSR( float *pLeft, float *pRight, int nFrames, int nReleaseFram void ADSR::attack() { - __state = ATTACK; - __ticks = 0; + m_state = State::Attack; + m_fFramesInState = 0; m_fQ = fAttackInit; } float ADSR::release() { - if ( __state == IDLE ) return 0; - if ( __state == RELEASE ) return __value; - __release_value = __value; - __state = RELEASE; - __ticks = 0; + if ( m_state == State::Idle ) { + return 0; + } + else if ( m_state == State::Release ) { + return m_fValue; + } + + m_fReleaseValue = m_fValue; + m_state = State::Release; + m_fFramesInState = 0; m_fQ = fDecayInit; - return __release_value; + return m_fReleaseValue; +} + +QString ADSR::StateToQString( State state ) { + switch( state ) { + case State::Attack: + return std::move( "Attack" ); + case State::Decay: + return std::move( "Decay" ); + case State::Sustain: + return std::move( "Sustain" ); + case State::Release: + return std::move( "Release" ); + case State::Idle: + return std::move( "Idle" ); + } + + return std::move( "Attack" ); } QString ADSR::toQString( const QString& sPrefix, bool bShort ) const { @@ -296,24 +310,25 @@ QString ADSR::toQString( const QString& sPrefix, bool bShort ) const { QString sOutput; if ( ! bShort ) { sOutput = QString( "%1[ADSR]\n" ).arg( sPrefix ) - .append( QString( "%1%2attack: %3\n" ).arg( sPrefix ).arg( s ).arg( __attack ) ) - .append( QString( "%1%2decay: %3\n" ).arg( sPrefix ).arg( s ).arg( __decay ) ) - .append( QString( "%1%2sustain: %3\n" ).arg( sPrefix ).arg( s ).arg( __sustain ) ) - .append( QString( "%1%2release: %3\n" ).arg( sPrefix ).arg( s ).arg( __release ) ) - .append( QString( "%1%2state: %3\n" ).arg( sPrefix ).arg( s ).arg( __state ) ) - .append( QString( "%1%2ticks: %3\n" ).arg( sPrefix ).arg( s ).arg( __ticks ) ) - .append( QString( "%1%2value: %3\n" ).arg( sPrefix ).arg( s ).arg( __value ) ) - .append( QString( "%1%2release_value: %3\n" ).arg( sPrefix ).arg( s ).arg( __release_value ) ); + .append( QString( "%1%2attack: %3\n" ).arg( sPrefix ).arg( s ).arg( m_nAttack ) ) + .append( QString( "%1%2decay: %3\n" ).arg( sPrefix ).arg( s ).arg( m_nDecay ) ) + .append( QString( "%1%2sustain: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fSustain ) ) + .append( QString( "%1%2release: %3\n" ).arg( sPrefix ).arg( s ).arg( m_nRelease ) ) + .append( QString( "%1%2state: %3\n" ).arg( sPrefix ).arg( s ) + .arg( StateToQString( m_state ) ) ) + .append( QString( "%1%2ticks: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fFramesInState ) ) + .append( QString( "%1%2value: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fValue ) ) + .append( QString( "%1%2release_value: %3\n" ).arg( sPrefix ).arg( s ).arg( m_fReleaseValue ) ); } else { sOutput = QString( "[ADSR]" ) - .append( QString( " attack: %1" ).arg( __attack ) ) - .append( QString( ", decay: %1" ).arg( __decay ) ) - .append( QString( ", sustain: %1" ).arg( __sustain ) ) - .append( QString( ", release: %1" ).arg( __release ) ) - .append( QString( ", state: %1" ).arg( __state ) ) - .append( QString( ", ticks: %1" ).arg( __ticks ) ) - .append( QString( ", value: %1" ).arg( __value ) ) - .append( QString( ", release_value: %1\n" ).arg( __release_value ) ); + .append( QString( " attack: %1" ).arg( m_nAttack ) ) + .append( QString( ", decay: %1" ).arg( m_nDecay ) ) + .append( QString( ", sustain: %1" ).arg( m_fSustain ) ) + .append( QString( ", release: %1" ).arg( m_nRelease ) ) + .append( QString( ", state: %1" ).arg( StateToQString( m_state ) ) ) + .append( QString( ", ticks: %1" ).arg( m_fFramesInState ) ) + .append( QString( ", value: %1" ).arg( m_fValue ) ) + .append( QString( ", release_value: %1\n" ).arg( m_fReleaseValue ) ); } return sOutput; diff --git a/src/core/Basics/Adsr.h b/src/core/Basics/Adsr.h index d42f320fd1..142f335b2e 100644 --- a/src/core/Basics/Adsr.h +++ b/src/core/Basics/Adsr.h @@ -41,10 +41,10 @@ class ADSR : public Object /** * constructor - * \param attack tick duration - * \param decay tick duration + * \param attack phase duration in frames + * \param decay phase duration in frames * \param sustain level - * \param release tick duration + * \param release phase duration in frames */ ADSR ( unsigned int attack = 0, unsigned int decay = 0, float sustain = 1.0, unsigned int release = 1000 ); @@ -54,57 +54,56 @@ class ADSR : public Object /** destructor */ ~ADSR(); - /** - * __attack setter - * \param value the new value - */ - void set_attack( unsigned int value ); - /** __attack accessor */ - unsigned int get_attack(); - /** - * __decay setter - * \param value the new value - */ - void set_decay( unsigned int value ); - /** __decay accessor */ - unsigned int get_decay(); - /** - * __sustain setter - * \param value the new value - */ - void set_sustain( float value ); - /** __sustain accessor */ - float get_sustain(); - /** - * __release setter - * \param value the new value - */ - void set_release( unsigned int value ); - /** __release accessor */ - unsigned int get_release(); + void setAttack( unsigned int value ); + unsigned int getAttack(); + void setDecay( unsigned int value ); + unsigned int getDecay(); + void setSustain( float value ); + float getSustain(); + void setRelease( unsigned int value ); + unsigned int getRelease(); /** - * sets state to ATTACK + * Sets #m_state to #State::Attack */ void attack(); /** - * sets state to RELEASE, - * returns 0 if the state is IDLE, - * __value if the state is RELEASE, - * set state to RELEASE, save __release_value and return it. + * Sets #m_state to #State::Release and return the current + * #m_fReleaseValue. + * + * State setting is only applied if the ADSR is not in #State::Idle. * */ float release(); /** * Compute and apply successive ADSR values to stereo buffers. + * + * In case the ADSR still hold its default values, the + * provided buffers aren't altered. + * + * Conceptionally it multiplies the first #m_nAttack frames of + * a sample with a rising attack exponential, the successive + * #m_nDecay frames with a falling decay exponential, and + * remaining frames with #m_fSustain. Which of these steps are + * covered in a run of applyADSR() depends on #m_state. + * + * If no note length was specified by the user in the GUI, + * #m_fSustain will be applied will the end of the + * corresponding sample and ADSR application won't enter + * release phase which would apply a falling exponential for + * #m_nRelease frames and zero all following frames. + * * \param pLeft left-channel audio buffer * \param pRight right-channel audio buffer - * \param nFrames number of frames of audio - * \param nReleaseFrame frame number of the release point - * \param fStep the increment to be added to __ticks + * \param nFinalBufferPos Up to which frame @a pLeft and @a + * pRight will be processed. + * \param nReleaseFrame Frame number indicating the end of the + * note or sample at which ADSR processing will enter the + * release phase. + * \param fStep the increment to be added to m_fFramesInState. */ - bool applyADSR( float *pLeft, float *pRight, int nFrames, int nReleaseFrame, float fStep ); + bool applyADSR( float *pLeft, float *pRight, int nFinalBufferPos, int nReleaseFrame, float fStep ); /** Formatted string version for debugging purposes. * \param sPrefix String prefix which will be added in front of @@ -114,24 +113,37 @@ class ADSR : public Object * displayed without line breaks. * * \return String presentation of current object.*/ - QString toQString( const QString& sPrefix, bool bShort = true ) const override; + QString toQString( const QString& sPrefix = "", bool bShort = true ) const override; private: - unsigned int __attack; ///< Attack tick count - unsigned int __decay; ///< Decay tick count - float __sustain; ///< Sustain level - unsigned int __release; ///< Release tick count /** possible states */ - enum ADSRState { - ATTACK=0, - DECAY, - SUSTAIN, - RELEASE, - IDLE + enum class State { + Attack = 0, + Decay, + Sustain, + Release, + Idle }; - ADSRState __state; ///< current state - float __ticks; ///< current tick count - float __value; ///< current value - float __release_value; ///< value when the release state was entered + static QString StateToQString( State state ); + + unsigned int m_nAttack; ///< Attack phase duration in frames + unsigned int m_nDecay; ///< Decay phase duration in frames + float m_fSustain; ///< Sustain level + unsigned int m_nRelease; ///< Release phase duration in frames + State m_state; ///< current state + /** + * Tracks the number of frames passed in the current #m_state. + * + * It is used to determine whether ADSR processing should proceed + * into the next state and required if multiple process cycles of + * the #H2Core::Sampler render the same state. + * + * Note that it is given in float instead of integer (the basic + * unit for frames in the #H2Core::AudioEngine) in order to + * account for processing while resampling the original audio. + */ + float m_fFramesInState; + float m_fValue; ///< current value + float m_fReleaseValue; ///< value when the release state was entered double m_fQ; ///< exponential decay state @@ -140,44 +152,44 @@ class ADSR : public Object // DEFINITIONS -inline void ADSR::set_attack( unsigned int value ) +inline void ADSR::setAttack( unsigned int value ) { - __attack = value; + m_nAttack = value; } -inline unsigned int ADSR::get_attack() +inline unsigned int ADSR::getAttack() { - return __attack; + return m_nAttack; } -inline void ADSR::set_decay( unsigned int value ) +inline void ADSR::setDecay( unsigned int value ) { - __decay = value; + m_nDecay = value; } -inline unsigned int ADSR::get_decay() +inline unsigned int ADSR::getDecay() { - return __decay; + return m_nDecay; } -inline void ADSR::set_sustain( float value ) +inline void ADSR::setSustain( float value ) { - __sustain = value; + m_fSustain = value; } -inline float ADSR::get_sustain() +inline float ADSR::getSustain() { - return __sustain; + return m_fSustain; } -inline void ADSR::set_release( unsigned int value ) +inline void ADSR::setRelease( unsigned int value ) { - __release = value; + m_nRelease = value; } -inline unsigned int ADSR::get_release() +inline unsigned int ADSR::getRelease() { - return __release; + return m_nRelease; } }; diff --git a/src/core/Basics/Drumkit.cpp b/src/core/Basics/Drumkit.cpp index 1b3f115b17..eefa1c947f 100644 --- a/src/core/Basics/Drumkit.cpp +++ b/src/core/Basics/Drumkit.cpp @@ -88,7 +88,7 @@ Drumkit::~Drumkit() std::shared_ptr Drumkit::load( const QString& sDrumkitPath, bool bUpgrade, bool bSilent ) { if ( ! Filesystem::drumkit_valid( sDrumkitPath ) ) { - ERRORLOG( QString( "[%1] is not valid drumkit" ).arg( sDrumkitPath ) ); + ERRORLOG( QString( "[%1] is not valid drumkit folder" ).arg( sDrumkitPath ) ); return nullptr; } @@ -273,7 +273,7 @@ QString Drumkit::loadNameFrom( const QString& sDrumkitDir, bool bSilent ) { bool Drumkit::loadDoc( const QString& sDrumkitDir, XMLDoc* pDoc, bool bSilent ) { if ( ! Filesystem::drumkit_valid( sDrumkitDir ) ) { - ERRORLOG( QString( "[%1] is not valid drumkit" ).arg( sDrumkitDir ) ); + ERRORLOG( QString( "[%1] is not valid drumkit folder" ).arg( sDrumkitDir ) ); return false; } @@ -572,7 +572,7 @@ std::vector> Drumkit::summarizeContent( bool Drumkit::remove( const QString& sDrumkitDir ) { if( ! Filesystem::drumkit_valid( sDrumkitDir ) ) { - ERRORLOG( QString( "%1 is not valid drumkit" ).arg( sDrumkitDir ) ); + ERRORLOG( QString( "%1 is not valid drumkit folder" ).arg( sDrumkitDir ) ); return false; } diff --git a/src/core/Basics/DrumkitComponent.cpp b/src/core/Basics/DrumkitComponent.cpp index 8dd260e8b1..e6997f8a2f 100644 --- a/src/core/Basics/DrumkitComponent.cpp +++ b/src/core/Basics/DrumkitComponent.cpp @@ -26,7 +26,6 @@ #include #include -#include #include #include @@ -96,8 +95,6 @@ float DrumkitComponent::get_out_R( int nBufferPos ) void DrumkitComponent::load_from( std::shared_ptr component ) { - AudioEngine* pAudioEngine = Hydrogen::get_instance()->getAudioEngine(); - this->set_id( component->get_id() ); this->set_name( component->get_name() ); this->set_muted( component->is_muted() ); diff --git a/src/core/Basics/Instrument.cpp b/src/core/Basics/Instrument.cpp index 7c97e7f68a..ed9b79f6a7 100644 --- a/src/core/Basics/Instrument.cpp +++ b/src/core/Basics/Instrument.cpp @@ -25,7 +25,6 @@ #include #include -#include #include #include @@ -159,8 +158,6 @@ void Instrument::load_from( std::shared_ptr pDrumkit, std::shared_ptrgetAudioEngine(); - this->get_components()->clear(); set_missing_samples( false ); @@ -380,7 +377,6 @@ std::shared_ptr Instrument::load_from( XMLNode* pNode, const QString pInstrument->set_drumkit_path( sInstrumentDrumkitPath ); pInstrument->__drumkit_name = sInstrumentDrumkitName; - pInstrument->set_volume( pNode->read_float( "volume", 1.0f, true, true, bSilent ) ); pInstrument->set_muted( pNode->read_bool( "isMuted", false, @@ -455,14 +451,14 @@ std::shared_ptr Instrument::load_from( XMLNode* pNode, const QString License instrumentLicense; if ( license == License() ) { // No/empty license supplied. We will use the license stored - // in the drumkit.xml file found at - // sInstrumentDrumkitPath. But since loading it from file is a - // rather expensive action, we will query it from the Drumkit - // database. If, for some reasons, the drumkit is not present - // yet, the License will be loaded directly. + // in the drumkit.xml file found in __drumkit_name. But since + // loading it from file is a rather expensive action, we will + // query it from the Drumkit database. If, for some reasons, + // the drumkit is not present yet, the License will be loaded + // directly. auto pSoundLibraryDatabase = Hydrogen::get_instance()->getSoundLibraryDatabase(); if ( pSoundLibraryDatabase != nullptr ) { - auto pDrumkit = pSoundLibraryDatabase->getDrumkit( sInstrumentDrumkitPath ); + auto pDrumkit = pSoundLibraryDatabase->getDrumkit( pInstrument->get_drumkit_path() ); if ( pDrumkit != nullptr ) { instrumentLicense = pDrumkit->get_license(); } @@ -471,10 +467,10 @@ std::shared_ptr Instrument::load_from( XMLNode* pNode, const QString if ( instrumentLicense == License() ) { if ( ! bSilent ) { WARNINGLOG( QString( "No license could be retrieved from drumkit [%1] in database. Loading directly." ) - .arg( sInstrumentDrumkitPath ) ); + .arg( pInstrument->get_drumkit_path() ) ); } - instrumentLicense = Drumkit::loadLicenseFrom( sInstrumentDrumkitPath ); + instrumentLicense = Drumkit::loadLicenseFrom( pInstrument->get_drumkit_path() ); } } else { instrumentLicense = license; @@ -484,15 +480,16 @@ std::shared_ptr Instrument::load_from( XMLNode* pNode, const QString // current format XMLNode componentNode = pNode->firstChildElement( "instrumentComponent" ); while ( ! componentNode.isNull() ) { - pInstrument->get_components()->push_back( - InstrumentComponent::load_from( &componentNode, sInstrumentDrumkitPath, - instrumentLicense, bSilent ) ); + pInstrument->get_components()-> + push_back( InstrumentComponent::load_from( &componentNode, + pInstrument->get_drumkit_path(), + instrumentLicense, bSilent ) ); componentNode = componentNode.nextSiblingElement( "instrumentComponent" ); } } else { // back compatibility code - auto pCompo = Legacy::loadInstrumentComponent( pNode, sInstrumentDrumkitPath, + auto pCompo = Legacy::loadInstrumentComponent( pNode, pInstrument->get_drumkit_path(), instrumentLicense, bSilent ); if ( pCompo == nullptr ) { ERRORLOG( "Unable to load component. Aborting." ); @@ -528,7 +525,9 @@ std::shared_ptr Instrument::load_from( XMLNode* pNode, const QString } if ( pLayer->get_sample() != nullptr ) { - bSampleFound = true; + if ( ! bSampleFound ) { + bSampleFound = true; + } } else { pInstrument->set_missing_samples( true ); } @@ -602,10 +601,10 @@ void Instrument::save_to( XMLNode* node, int component_id, bool bRecentVersion, InstrumentNode.write_bool( "filterActive", __filter_active ); InstrumentNode.write_float( "filterCutoff", __filter_cutoff ); InstrumentNode.write_float( "filterResonance", __filter_resonance ); - InstrumentNode.write_int( "Attack", __adsr->get_attack() ); - InstrumentNode.write_int( "Decay", __adsr->get_decay() ); - InstrumentNode.write_float( "Sustain", __adsr->get_sustain() ); - InstrumentNode.write_int( "Release", __adsr->get_release() ); + InstrumentNode.write_int( "Attack", __adsr->getAttack() ); + InstrumentNode.write_int( "Decay", __adsr->getDecay() ); + InstrumentNode.write_float( "Sustain", __adsr->getSustain() ); + InstrumentNode.write_int( "Release", __adsr->getRelease() ); InstrumentNode.write_int( "muteGroup", __mute_group ); InstrumentNode.write_int( "midiOutChannel", __midi_out_channel ); InstrumentNode.write_int( "midiOutNote", __midi_out_note ); @@ -655,6 +654,11 @@ std::shared_ptr Instrument::get_component( int DrumkitCompo return nullptr; } +QString Instrument::get_drumkit_path() const +{ + return Filesystem::ensure_session_compatibility( __drumkit_path ); +} + QString Instrument::toQString( const QString& sPrefix, bool bShort ) const { QString s = Base::sPrintIndention; QString sOutput; diff --git a/src/core/Basics/Instrument.h b/src/core/Basics/Instrument.h index 1b66807f9f..bf16d34467 100644 --- a/src/core/Basics/Instrument.h +++ b/src/core/Basics/Instrument.h @@ -282,7 +282,7 @@ class Instrument : public H2Core::Object ///< set the path of the related drumkit void set_drumkit_path( const QString& sPath ); ///< get the path of the related drumkits - const QString& get_drumkit_path() const; + QString get_drumkit_path() const; ///< set the name of the related drumkit void set_drumkit_name( const QString& sName ); ///< get the name of the related drumkits @@ -350,7 +350,13 @@ class Instrument : public H2Core::Object bool __filter_active; ///< is filter active? float __filter_cutoff; ///< filter cutoff (0..1) float __filter_resonance; ///< filter resonant frequency (0..1) - float __random_pitch_factor; ///< random pitch factor + /** + * Factor to scale the random contribution when humanizing pitch + * between 0 and #AudioEngine::fHumanizePitchSD. + * + * Supported range [0,1]. + */ + float __random_pitch_factor; float __pitch_offset; ///< instrument main pitch offset int __midi_out_note; ///< midi out note int __midi_out_channel; ///< midi out channel @@ -668,11 +674,6 @@ inline void Instrument::set_drumkit_path( const QString& sPath ) __drumkit_path = sPath; } -inline const QString& Instrument::get_drumkit_path() const -{ - return __drumkit_path; -} - inline void Instrument::set_drumkit_name( const QString& sName ) { __drumkit_name = sName; diff --git a/src/core/Basics/InstrumentLayer.cpp b/src/core/Basics/InstrumentLayer.cpp index 2339d68886..20a5598dfe 100644 --- a/src/core/Basics/InstrumentLayer.cpp +++ b/src/core/Basics/InstrumentLayer.cpp @@ -26,6 +26,8 @@ #include #include #include +#include +#include #include namespace H2Core @@ -83,14 +85,51 @@ void InstrumentLayer::unload_sample() std::shared_ptr InstrumentLayer::load_from( XMLNode* pNode, const QString& sDrumkitPath, const License& drumkitLicense, bool bSilent ) { + auto pHydrogen = Hydrogen::get_instance(); + QString sFilename = pNode->read_string( "filename", "", false, false, bSilent ); + QString sAbsoluteFilename = sFilename; + if ( ! Filesystem::file_exists( sFilename, true ) && ! sDrumkitPath.isEmpty() && ! sFilename.startsWith( "/" ) ) { + +#ifdef H2CORE_HAVE_OSC + if ( pHydrogen->isUnderSessionManagement() ) { + // If we use the NSM support and the sample files to save + // are corresponding to the drumkit linked/located in the + // session folder, we have to ensure the relative paths + // are loaded. This is vital in order to support + // renaming, duplicating, and porting sessions. + + // QFileInfo::isRelative() can not be used in here as + // samples within drumkits within the user or system + // drumkit folder are stored relatively as well (by saving + // just the filename). + if ( sFilename.left( 2 ) == "./" || + sFilename.left( 2 ) == ".\\" ) { + // Removing the leading "." of the relative path in + // sFilename while still using the associated folder + // separator. + sAbsoluteFilename = NsmClient::get_instance()->getSessionFolderPath() + + sFilename.right( sFilename.size() - 1 ); + } + else { + sFilename = sDrumkitPath + "/" + sFilename; + sAbsoluteFilename = sFilename; + } + } + else { + sFilename = sDrumkitPath + "/" + sFilename; + sAbsoluteFilename = sFilename; + } +#else sFilename = sDrumkitPath + "/" + sFilename; + sAbsoluteFilename = sFilename; +#endif } std::shared_ptr pSample = nullptr; - if ( Filesystem::file_exists( sFilename, true ) ) { + if ( Filesystem::file_exists( sAbsoluteFilename, true ) ) { pSample = std::make_shared( sFilename, drumkitLicense ); // If 'ismodified' is not present, InstrumentLayer was stored as @@ -162,6 +201,7 @@ std::shared_ptr InstrumentLayer::load_from( XMLNode* pNode, con void InstrumentLayer::save_to( XMLNode* node, bool bFull ) { + auto pHydrogen = Hydrogen::get_instance(); auto pSample = get_sample(); if ( pSample == nullptr ) { ERRORLOG( "No sample associated with layer. Skipping it" ); @@ -172,9 +212,26 @@ void InstrumentLayer::save_to( XMLNode* node, bool bFull ) QString sFilename; if ( bFull ) { - sFilename = Filesystem::prepare_sample_path( pSample->get_filepath() ); - } else { - sFilename = get_sample()->get_filename(); + + if ( pHydrogen->isUnderSessionManagement() ) { + // If we use the NSM support and the sample files to save + // are corresponding to the drumkit linked/located in the + // session folder, we have to ensure the relative paths + // are written out. This is vital in order to support + // renaming, duplicating, and porting sessions. + if ( pSample->get_raw_filepath().startsWith( '.' ) ) { + sFilename = pSample->get_raw_filepath(); + } + else { + sFilename = Filesystem::prepare_sample_path( pSample->get_filepath() ); + } + } + else { + sFilename = Filesystem::prepare_sample_path( pSample->get_filepath() ); + } + } + else { + sFilename = pSample->get_filename(); } layer_node.write_string( "filename", sFilename ); diff --git a/src/core/Basics/Note.cpp b/src/core/Basics/Note.cpp index cc41507eea..c27d050d05 100644 --- a/src/core/Basics/Note.cpp +++ b/src/core/Basics/Note.cpp @@ -24,8 +24,10 @@ #include +#include #include #include +#include #include #include #include @@ -40,14 +42,14 @@ namespace H2Core const char* Note::__key_str[] = { "C", "Cs", "D", "Ef", "E", "F", "Fs", "G", "Af", "A", "Bf", "B" }; -Note::Note( std::shared_ptr instrument, int position, float velocity, float pan, int length, float pitch ) - : __instrument( instrument ), +Note::Note( std::shared_ptr pInstrument, int nPosition, float fVelocity, float fPan, int nLength, float fPitch ) + : __instrument( pInstrument ), __instrument_id( 0 ), __specific_compo_id( -1 ), - __position( position ), - __velocity( velocity ), - __length( length ), - __pitch( pitch ), + __position( nPosition ), + __velocity( fVelocity ), + __length( nLength ), + __pitch( fPitch ), __key( C ), __octave( P8 ), __adsr( nullptr ), @@ -67,20 +69,20 @@ Note::Note( std::shared_ptr instrument, int position, float velocity m_nNoteStart( 0 ), m_fUsedTickSize( std::nan("") ) { - if ( __instrument != nullptr ) { - __adsr = __instrument->copy_adsr(); - __instrument_id = __instrument->get_id(); + if ( pInstrument != nullptr ) { + __adsr = pInstrument->copy_adsr(); + __instrument_id = pInstrument->get_id(); - for ( const auto& pCompo : *__instrument->get_components() ) { - std::shared_ptr sampleInfo = std::make_shared(); - sampleInfo->SelectedLayer = -1; - sampleInfo->SamplePosition = 0; + for ( const auto& pCompo : *pInstrument->get_components() ) { + std::shared_ptr pSampleInfo = std::make_shared(); + pSampleInfo->SelectedLayer = -1; + pSampleInfo->SamplePosition = 0; - __layers_selected[ pCompo->get_drumkit_componentID() ] = sampleInfo; + __layers_selected[ pCompo->get_drumkit_componentID() ] = pSampleInfo; } } - setPan( pan ); // this checks the boundaries + setPan( fPan ); // this checks the boundaries } Note::Note( Note* other, std::shared_ptr instrument ) @@ -150,6 +152,17 @@ void Note::setPan( float val ) { m_fPan = check_boundary( val, -1.0f, 1.0f ); } +void Note::set_humanize_delay( int nValue ) +{ + // We do not perform bound checks with + // AudioEngine::nMaxTimeHumanize in here as different contribution + // could push the value first beyond and then within the bounds + // again. The clamping will be done in computeNoteStart() instead. + if ( nValue != __humanize_delay ) { + __humanize_delay = nValue; + } +} + void Note::map_instrument( std::shared_ptr pInstrumentList ) { if ( pInstrumentList == nullptr ) { @@ -216,32 +229,30 @@ bool Note::isPartiallyRendered() const { } void Note::computeNoteStart() { - // Notes not inserted via the audio engine but directly, using - // e.g. the GUI, will be insert at position 0 and don't require a - // specific start position. - if ( __position == 0 ) { - return; - } - auto pHydrogen = Hydrogen::get_instance(); auto pAudioEngine = pHydrogen->getAudioEngine(); double fTickMismatch; m_nNoteStart = - pAudioEngine->computeFrameFromTick( __position, &fTickMismatch ); - - // If there is a negative Humanize delay, take into account so - // we don't miss the time slice. ignore positive delay, or we - // might end the queue processing prematurely based on NoteQueue - // placement. the sampler handles positive delay. - if ( __humanize_delay < 0 ) { - m_nNoteStart += __humanize_delay; + TransportPosition::computeFrameFromTick( __position, &fTickMismatch ); + + m_nNoteStart += std::clamp( __humanize_delay, + -1 * AudioEngine::nMaxTimeHumanize, + AudioEngine::nMaxTimeHumanize ); + + // No note can start before the beginning of the song. + if ( m_nNoteStart < 0 ) { + m_nNoteStart = 0; } if ( pHydrogen->isTimelineEnabled() ) { m_fUsedTickSize = -1; } else { - m_fUsedTickSize = pAudioEngine->getTickSize(); + // This is used for triggering recalculation in case the tempo + // changes where manually applied by the user. They are not + // dependent on a particular position of the transport (as + // Timeline is not activated). + m_fUsedTickSize = pAudioEngine->getTransportPosition()->getTickSize(); } } @@ -406,6 +417,71 @@ std::shared_ptr Note::getSample( int nComponentID, int nSelectedLayer ) return pSample; } +float Note::get_total_pitch() const +{ + float fNotePitch = __octave * KEYS_PER_OCTAVE + __key + __pitch; + + if ( __instrument != nullptr ) { + fNotePitch += __instrument->get_pitch_offset(); + } + return fNotePitch; +} + +void Note::humanize() { + // Due to the nature of the Gaussian distribution, the factors + // will also scale the standard deviations of the generated random + // variables. + const auto pSong = Hydrogen::get_instance()->getSong(); + if ( pSong != nullptr ) { + const float fRandomVelocityFactor = pSong->getHumanizeVelocityValue(); + if ( fRandomVelocityFactor != 0 ) { + set_velocity( __velocity + fRandomVelocityFactor * + Random::getGaussian( AudioEngine::fHumanizeVelocitySD ) ); + } + + const float fRandomTimeFactor = pSong->getHumanizeTimeValue(); + if ( fRandomTimeFactor != 0 ) { + set_humanize_delay( __humanize_delay + fRandomTimeFactor * + AudioEngine::nMaxTimeHumanize * + Random::getGaussian( AudioEngine::fHumanizeTimingSD ) ); + } + } + + if ( __instrument != nullptr ) { + const float fRandomPitchFactor = __instrument->get_random_pitch_factor(); + if ( fRandomPitchFactor != 0 ) { + __pitch += Random::getGaussian( AudioEngine::fHumanizePitchSD ) * + fRandomPitchFactor; + } + } +} + +void Note::swing() { + const auto pSong = Hydrogen::get_instance()->getSong(); + if ( pSong != nullptr && pSong->getSwingFactor() > 0 ) { + /* TODO: incorporate the factor MAX_NOTES / 32. either in + * Song::m_fSwingFactor or make it a member variable. + * + * comment by oddtime: 32 depends on the fact that the swing + * is applied to the upbeat 16th-notes. (not to upbeat + * 8th-notes as in jazz swing!). however 32 could be changed + * but must be >16, otherwise the max delay is too long and + * the swing note could be played after the next downbeat! + */ + // If the Timeline is activated, the tick size may change at + // any point. Therefore, the length in frames of a 16-th note + // offset has to be calculated for a particular transport + // position and is not generally applicable. + double fTickMismatch; + set_humanize_delay( __humanize_delay + + ( TransportPosition::computeFrameFromTick( + __position + MAX_NOTES / 32., &fTickMismatch ) - + TransportPosition::computeFrameFromTick( + __position, &fTickMismatch ) ) * + pSong->getSwingFactor() ); + } +} + void Note::save_to( XMLNode* node ) { node->write_int( "position", __position ); diff --git a/src/core/Basics/Note.h b/src/core/Basics/Note.h index 475e79d75e..6ce6c753de 100644 --- a/src/core/Basics/Note.h +++ b/src/core/Basics/Note.h @@ -86,21 +86,24 @@ class Note : public H2Core::Object /** * constructor - * \param instrument the instrument played by this note - * \param position the position of the note within the pattern - * \param velocity it's velocity - * \param pan pan - * \param length it's length - * \param pitch it's pitch + * + * \param pInstrument the instrument played by this note + * \param nPosition the position of the note within the pattern + * \param fVelocity it's velocity + * \param fFan pan + * \param nLength Length of the note in frames. If set to -1, + * the length of the #H2Core::Sample used during playback will + * be used instead. + * \param fPitch it's pitch */ - Note( std::shared_ptr instrument, int position, float velocity, float pan, int length, float pitch ); + Note( std::shared_ptr pInstrument, int nPosition = 0, float fVelocity = 0.8, float fPan = 0.0, int nLength = -1, float fPitch = 0.0 ); /** * copy constructor with an optional parameter - * \param other - * \param instrument if set will be used as note instrument + * \param pOther + * \param pInstrument if set will be used as note instrument */ - Note( Note* other, std::shared_ptr instrument=nullptr ); + Note( Note* pOther, std::shared_ptr pInstrument = nullptr ); /** destructor */ ~Note(); @@ -261,10 +264,12 @@ class Note : public H2Core::Object * __octave * KEYS_PER_OCTAVE + __key * \endcode */ float get_notekey_pitch() const; - /** returns - * \code{.cpp} - * __octave * 12 + __key + __pitch - * \endcode*/ + /** + * + * @returns + * \code{.cpp} + * __octave * 12 + __key + __pitch + __instrument->get_pitch_offset() + * \endcode*/ float get_total_pitch() const; /** return a string representation of key-octave */ @@ -290,10 +295,6 @@ class Note : public H2Core::Object /** get the ADSR of the note */ std::shared_ptr get_adsr() const; - /** call release on adsr */ - //float release_adsr() const { return __adsr->release(); } - /** call get value on adsr */ - //float get_adsr_value(float v) const { return __adsr->get_value( v ); } /** return true if instrument, key and octave matches with internal * \param instrument the instrument to match with #__instrument @@ -304,6 +305,7 @@ class Note : public H2Core::Object /** Return true if two notes match in instrument, key and octave. */ bool match( const Note *pNote ) const; + bool match( const std::shared_ptr pNote ) const; /** * compute left and right output based on filters @@ -331,6 +333,20 @@ class Note : public H2Core::Object * needs to be rerun. */ void computeNoteStart(); + + /** + * Add random contributions to #__pitch, #__humanize_delay, and + * #__velocity. + */ + void humanize(); + + /** + * Add swing contribution to #__humanize_delay. + * + * As the value applied is deterministic, it will not be handled + * in humanice() but separately. + */ + void swing(); /** Formatted string version for debugging purposes. * \param sPrefix String prefix which will be added in front of @@ -340,7 +356,7 @@ class Note : public H2Core::Object * displayed without line breaks. * * \return String presentation of current object.*/ - QString toQString( const QString& sPrefix, bool bShort = true ) const override; + QString toQString( const QString& sPrefix = "", bool bShort = true ) const override; /** Convert a logarithmic pitch-space value in semitones to a frequency-domain value */ static inline double pitchToFrequency( double fPitch ) { @@ -433,7 +449,7 @@ class Note : public H2Core::Object */ long long m_nNoteStart; /** - * TransportInfo::m_fTickSize used to calculate #m_nNoteStart. + * TransportPosition::m_fTickSize used to calculate #m_nNoteStart. * * If #m_nNoteStart was calculated in the presence of an active * #Timeline, it will be set to -1. @@ -580,11 +596,6 @@ inline std::shared_ptr Note::get_layer_selected( int CompoID return __layers_selected[ CompoID ]; } -inline void Note::set_humanize_delay( int value ) -{ - __humanize_delay = value; -} - inline int Note::get_humanize_delay() const { return __humanize_delay; @@ -655,11 +666,6 @@ inline float Note::get_notekey_pitch() const return __octave * KEYS_PER_OCTAVE + __key; } -inline float Note::get_total_pitch() const -{ - return __octave * KEYS_PER_OCTAVE + __key + __pitch; -} - inline void Note::set_key_octave( Key key, Octave octave ) { if( key>=KEY_MIN && key<=KEY_MAX ) __key = key; @@ -682,6 +688,10 @@ inline bool Note::match( const Note *pNote ) const { return match( pNote->__instrument, pNote->__key, pNote->__octave ); } +inline bool Note::match( const std::shared_ptr pNote ) const +{ + return match( pNote->__instrument, pNote->__key, pNote->__octave ); +} inline void Note::compute_lr_values( float* val_l, float* val_r ) { diff --git a/src/core/Basics/Sample.cpp b/src/core/Basics/Sample.cpp index 30650f5229..d737e2d861 100644 --- a/src/core/Basics/Sample.cpp +++ b/src/core/Basics/Sample.cpp @@ -121,11 +121,16 @@ Sample::~Sample() void Sample::set_filename( const QString& filename ) { QFileInfo Filename = QFileInfo( filename ); - QFileInfo Dest = QFileInfo( __filepath ); + QFileInfo Dest = QFileInfo( get_filepath() ); __filepath = QDir(Dest.absolutePath()).filePath( Filename.fileName() ); } +QString Sample::get_filepath() const +{ + return Filesystem::ensure_session_compatibility( __filepath ); +} + std::shared_ptr Sample::load( const QString& sFilepath, const License& license ) { std::shared_ptr pSample; @@ -152,9 +157,9 @@ bool Sample::load( float fBpm ) SF_INFO sound_info = {0}; // Opens file in read-only mode. - SNDFILE* file = sf_open( __filepath.toLocal8Bit(), SFM_READ, &sound_info ); + SNDFILE* file = sf_open( get_filepath().toLocal8Bit(), SFM_READ, &sound_info ); if ( !file ) { - ERRORLOG( QString( "[Sample::load] Error loading file %1" ).arg( __filepath ) ); + ERRORLOG( QString( "[Sample::load] Error loading file %1" ).arg( get_filepath() ) ); return false; } @@ -181,12 +186,12 @@ bool Sample::load( float fBpm ) // encoding (e.g. 16 bit PCM). sf_count_t count = sf_read_float( file, buffer, sound_info.frames * sound_info.channels ); if( count==0 ){ - WARNINGLOG( QString( "%1 is an empty sample" ).arg( __filepath ) ); + WARNINGLOG( QString( "%1 is an empty sample" ).arg( get_filepath() ) ); } // Deallocate the handler. if ( sf_close( file ) != 0 ){ - WARNINGLOG( QString( "Unable to close sample file %1" ).arg( __filepath ) ); + WARNINGLOG( QString( "Unable to close sample file %1" ).arg( get_filepath() ) ); } // Flush the current content of the left and right channel and diff --git a/src/core/Basics/Sample.h b/src/core/Basics/Sample.h index 058e02c7bc..90d601cc44 100644 --- a/src/core/Basics/Sample.h +++ b/src/core/Basics/Sample.h @@ -206,12 +206,15 @@ class Sample : public H2Core::Object /** \return true if both data channels are null pointers */ bool is_empty() const; + QString get_filepath() const; /** \return #__filepath */ - const QString get_filepath() const; + const QString get_raw_filepath() const; /** \return Filename part of #__filepath */ const QString get_filename() const; /** \param filename Filename part of #__filepath*/ void set_filename( const QString& filename ); + /** \param filename sets #__filepath*/ + void set_filepath( const QString& sFilepath ); /** * #__frames setter * \param value the new value for #__frames @@ -351,11 +354,16 @@ inline bool Sample::is_empty() const return ( __data_l == 0 && __data_r == 0 ); } -inline const QString Sample::get_filepath() const +inline const QString Sample::get_raw_filepath() const { return __filepath; } +inline void Sample::set_filepath( const QString& sFilepath ) +{ + __filepath = sFilepath; +} + inline const QString Sample::get_filename() const { return __filepath.section( "/", -1 ); diff --git a/src/core/Basics/Song.cpp b/src/core/Basics/Song.cpp index dcd51f1075..00f39527bc 100644 --- a/src/core/Basics/Song.cpp +++ b/src/core/Basics/Song.cpp @@ -25,6 +25,9 @@ #include #include +#include +#include + #include #include #include @@ -214,7 +217,7 @@ std::shared_ptr Song::load( const QString& sFilename, bool bSilent ) } } - auto pSong = Song::loadFrom( &songNode, bSilent ); + auto pSong = Song::loadFrom( &songNode, sFilename, bSilent ); if ( pSong != nullptr ) { pSong->setFilename( sFilename ); } @@ -222,7 +225,7 @@ std::shared_ptr Song::load( const QString& sFilename, bool bSilent ) return pSong; } -std::shared_ptr Song::loadFrom( XMLNode* pRootNode, bool bSilent ) +std::shared_ptr Song::loadFrom( XMLNode* pRootNode, const QString& sFilename, bool bSilent ) { auto pPreferences = Preferences::get_instance(); @@ -262,6 +265,16 @@ std::shared_ptr Song::loadFrom( XMLNode* pRootNode, bool bSilent ) QString sPlaybackTrack( pRootNode->read_string( "playbackTrackFilename", "", false, true, bSilent ) ); + if ( sPlaybackTrack.left( 2 ) == "./" || + sPlaybackTrack.left( 2 ) == ".\\" ) { + // Playback track has been made portable by manually + // converting the absolute path stored by Hydrogen into a + // relative one. + QFileInfo info( sFilename ); + sPlaybackTrack = info.absoluteDir() + .filePath( sPlaybackTrack.right( sPlaybackTrack.size() - 2 ) ); + } + // Check the file of the playback track and resort to the default // in case the file can not be found. if ( ! sPlaybackTrack.isEmpty() && @@ -424,12 +437,14 @@ std::shared_ptr Song::loadFrom( XMLNode* pRootNode, bool bSilent ) sLastLoadedDrumkitPath = sMostCommonDrumkit; } + pSong->setLastLoadedDrumkitPath( sLastLoadedDrumkitPath ); if ( sLastLoadedDrumkitName.isEmpty() ) { - sLastLoadedDrumkitName = Drumkit::loadNameFrom( sLastLoadedDrumkitPath, - bSilent ); + // Use the getter in here to support relative paths as well. + sLastLoadedDrumkitName = + Drumkit::loadNameFrom( pSong->getLastLoadedDrumkitPath(), + bSilent ); } - pSong->setLastLoadedDrumkitPath( sLastLoadedDrumkitPath ); pSong->setLastLoadedDrumkitName( sLastLoadedDrumkitName ); // Pattern list @@ -1197,6 +1212,8 @@ void Song::setPanLawKNorm( float fKNorm ) { } void Song::setDrumkit( std::shared_ptr pDrumkit, bool bConditional ) { + auto pHydrogen = Hydrogen::get_instance(); + assert ( pDrumkit ); if ( pDrumkit == nullptr ) { ERRORLOG( "Invalid drumkit supplied" ); @@ -1285,7 +1302,7 @@ void Song::setDrumkit( std::shared_ptr pDrumkit, bool bConditional ) { // Load samples of all instruments. m_pInstrumentList->load_samples( - Hydrogen::get_instance()->getAudioEngine()->getBpm() ); + pHydrogen->getAudioEngine()->getTransportPosition()->getBpm() ); } @@ -1420,6 +1437,11 @@ QString Song::makeComponentNameUnique( const QString& sName ) const { } return sName; } + +QString Song::getLastLoadedDrumkitPath() const +{ + return Filesystem::ensure_session_compatibility( m_sLastLoadedDrumkitPath ); +} QString Song::toQString( const QString& sPrefix, bool bShort ) const { QString s = Base::sPrintIndention; diff --git a/src/core/Basics/Song.h b/src/core/Basics/Song.h index 961da702d5..3e625afad1 100644 --- a/src/core/Basics/Song.h +++ b/src/core/Basics/Song.h @@ -284,7 +284,7 @@ class Song : public H2Core::Object, public std::enable_shared_from_this, public std::enable_shared_from_this loadFrom( XMLNode* pNode, bool bSilent = false ); + static std::shared_ptr loadFrom( XMLNode* pNode, const QString& sFilename, bool bSilent = false ); void writeTo( XMLNode* pNode, bool bSilent = false ); void loadVirtualPatternsFrom( XMLNode* pNode, bool bSilent = false ); @@ -317,7 +317,7 @@ class Song : public H2Core::Object, public std::enable_shared_from_this, public std::enable_shared_from_this +#include #include #include #include @@ -537,7 +538,10 @@ bool CoreActionController::newSong( const QString& sSongPath ) { if ( pHydrogen->isUnderSessionManagement() ) { pHydrogen->restartDrivers(); - } + // The drumkit of the new song will linked into the session + // folder during the next song save. + pHydrogen->setSessionDrumkitNeedsRelinking( true ); + } pSong->setFilename( sSongPath ); @@ -585,7 +589,7 @@ bool CoreActionController::openSong( const QString& sSongPath, const QString& sR return setSong( pSong ); } -bool CoreActionController::openSong( std::shared_ptr pSong ) { +bool CoreActionController::openSong( std::shared_ptr pSong, bool bRelinking ) { auto pHydrogen = Hydrogen::get_instance(); @@ -600,15 +604,15 @@ bool CoreActionController::openSong( std::shared_ptr pSong ) { return false; } - return setSong( pSong ); + return setSong( pSong, bRelinking ); } -bool CoreActionController::setSong( std::shared_ptr pSong ) { +bool CoreActionController::setSong( std::shared_ptr pSong, bool bRelinking ) { auto pHydrogen = Hydrogen::get_instance(); // Update the Song. - pHydrogen->setSong( pSong ); + pHydrogen->setSong( pSong, bRelinking ); if ( pHydrogen->isUnderSessionManagement() ) { pHydrogen->restartDrivers(); @@ -619,9 +623,7 @@ bool CoreActionController::setSong( std::shared_ptr pSong ) { // empty songs - created and set when hitting "New Song" in // the main menu - aren't listed either. insertRecentFile( pSong->getFilename() ); - if ( ! pHydrogen->isUnderSessionManagement() ) { - Preferences::get_instance()->setLastSongFilename( pSong->getFilename() ); - } + Preferences::get_instance()->setLastSongFilename( pSong->getFilename() ); } if ( pHydrogen->getGUIState() != Hydrogen::GUIState::unavailable ) { @@ -648,6 +650,28 @@ bool CoreActionController::saveSong() { ERRORLOG( "Unable to save song. Empty filename!" ); return false; } + +#ifdef H2CORE_HAVE_OSC + if ( pHydrogen->isUnderSessionManagement() && + pHydrogen->getSessionDrumkitNeedsRelinking() && + ! pHydrogen->getSessionIsExported() ) { + + NsmClient::linkDrumkit( pSong ); + + // Properly set in NsmClient::linkDrumkit() + QString sSessionDrumkitPath = pSong->getLastLoadedDrumkitPath(); + + auto drumkitDatabase = pHydrogen->getSoundLibraryDatabase()->getDrumkitDatabase(); + if ( drumkitDatabase.find( sSessionDrumkitPath ) != drumkitDatabase.end() ) { + // In case the session folder is already present in the + // SoundLibraryDatabase, we have to update it (takes a + // while) to ensure it's clean and all kits are valid. If + // it's not present, we can skip it because loading is + // done lazily. + pHydrogen->getSoundLibraryDatabase()->updateDrumkit( sSessionDrumkitPath ); + } + } +#endif // Actual saving bool bSaved = pSong->save( sSongPath ); @@ -904,15 +928,15 @@ bool CoreActionController::activateSongMode( bool bActivate ) { pHydrogen->setMode( Song::Mode::Song ); } else if ( ! bActivate && pHydrogen->getMode() != Song::Mode::Pattern ) { pHydrogen->setMode( Song::Mode::Pattern ); - - // Add the selected pattern to playing ones. - if ( pHydrogen->getPatternMode() == Song::PatternMode::Selected ) { - pAudioEngine->lock( RIGHT_HERE ); - pAudioEngine->updatePlayingPatterns( 0, 0 ); - pAudioEngine->unlock(); - } } + locateToColumn( 0 ); + + // Ensure the playing patterns are properly updated regardless of + // the state of transport before switching song modes. + pAudioEngine->lock( RIGHT_HERE ); + pAudioEngine->updatePlayingPatterns(); + pAudioEngine->unlock(); return true; } @@ -941,7 +965,8 @@ bool CoreActionController::activateLoopMode( bool bActivate ) { // If the transport was already looped at least once, disabling // loop mode will result in immediate stop. Instead, we want to // stop transport at the end of the song. - if ( pSong->lengthInTicks() < pAudioEngine->getTick() ) { + if ( pSong->lengthInTicks() < + pAudioEngine->getTransportPosition()->getTick() ) { pSong->setLoopMode( Song::LoopMode::Finishing ); } else { pSong->setLoopMode( Song::LoopMode::Disabled ); @@ -1003,9 +1028,7 @@ bool CoreActionController::setDrumkit( std::shared_ptr pDrumkit, bool b // Create a symbolic link in the session folder when under session // management. if ( pHydrogen->isUnderSessionManagement() ) { -#ifdef H2CORE_HAVE_OSC - NsmClient::linkDrumkit( NsmClient::get_instance()->m_sSessionFolderPath, false ); -#endif + pHydrogen->setSessionDrumkitNeedsRelinking( true ); } EventQueue::get_instance()->push_event( EVENT_DRUMKIT_LOADED, 0 ); @@ -1338,16 +1361,14 @@ bool CoreActionController::locateToColumn( int nPatternGroup ) { return false; } - auto pAudioEngine = pHydrogen->getAudioEngine(); - EventQueue::get_instance()->push_event( EVENT_METRONOME, 1 ); long nTotalTick = pHydrogen->getTickForColumn( nPatternGroup ); if ( nTotalTick < 0 ) { - // There is no pattern inserted in the SongEditor. if ( pHydrogen->getMode() == Song::Mode::Song ) { - INFOLOG( QString( "Obtained ticks [%1] are smaller than zero. No relocation done." ) - .arg( nTotalTick ) ); + ERRORLOG( QString( "Provided column [%1] violates the allowed range [0;%2). No relocation done." ) + .arg( nPatternGroup ) + .arg( pHydrogen->getSong()->getPatternGroupVector()->size() ) ); return false; } else { // In case of Pattern mode this is not a problem and we @@ -1444,6 +1465,7 @@ bool CoreActionController::removePattern( int nPatternNumber ) { auto pAudioEngine = pHydrogen->getAudioEngine(); auto pSong = pHydrogen->getSong(); + if ( pSong == nullptr ) { ERRORLOG( "no song set" ); return false; @@ -1485,6 +1507,19 @@ bool CoreActionController::removePattern( int nPatternNumber ) { } } } + + PatternList* pColumn; + // Ensure there are no empty columns in the pattern group vector. + for ( int ii = pPatternGroupVector->size() - 1; ii >= 0; --ii ) { + pColumn = pPatternGroupVector->at( ii ); + if ( pColumn->size() == 0 ) { + pPatternGroupVector->erase( pPatternGroupVector->begin() + ii ); + delete pColumn; + } + else { + break; + } + } if ( pHydrogen->isPatternEditorLocked() ) { pHydrogen->updateSelectedPattern( false ); @@ -1506,12 +1541,7 @@ bool CoreActionController::removePattern( int nPatternNumber ) { // Ensure the pattern is not among the list of currently played // patterns cached in the audio engine if transport is in pattern // mode. - for ( int ii = 0; ii < pPlayingPatterns->size(); ++ii ) { - if ( pPlayingPatterns->get( ii ) == pPattern ) { - pAudioEngine->removePlayingPattern( ii ); - break; - } - } + pAudioEngine->removePlayingPattern( pPattern ); // Delete the pattern from the list of available patterns. pPatternList->del( pPattern ); @@ -1620,6 +1650,21 @@ bool CoreActionController::toggleGridCell( int nColumn, int nRow ){ return true; } +void CoreActionController::updatePreferences() { + auto pPref = Preferences::get_instance(); + auto pHydrogen = Hydrogen::get_instance(); + auto pAudioEngine = pHydrogen->getAudioEngine(); + + pAudioEngine->getMetronomeInstrument()->set_volume( + pPref->m_fMetronomeVolume ); + + // If the GUI is active, we have to update it to reflect the + // changes in the preferences. + if ( pHydrogen->getGUIState() == H2Core::Hydrogen::GUIState::ready ) { + H2Core::EventQueue::get_instance()->push_event( H2Core::EVENT_UPDATE_PREFERENCES, 1 ); + } +} + void CoreActionController::insertRecentFile( const QString sFilename ){ auto pPref = Preferences::get_instance(); diff --git a/src/core/CoreActionController.h b/src/core/CoreActionController.h index 3dc71dd3da..cc27eb07b4 100644 --- a/src/core/CoreActionController.h +++ b/src/core/CoreActionController.h @@ -121,9 +121,14 @@ class CoreActionController : public H2Core::Object { * the current #H2Core::Song. All unsaved changes will be lost! * * \param pSong New Song. + * \param bRelinking Whether the drumkit last loaded should be + * relinked when under session management. This flag is used + * to distinguish between the regular load of a song file + * within a session and its replacement by another song (which + * requires an update of the linked drumkit). * \return true on success */ - bool openSong( std::shared_ptr pSong ); + bool openSong( std::shared_ptr pSong, bool bRelinking = true ); /** * Saves the current #H2Core::Song. * @@ -371,6 +376,13 @@ class CoreActionController : public H2Core::Object { * @return bool true on success */ bool toggleGridCell( int nColumn, int nRow ); + + /** + * In case a different preferences file was loaded with Hydrogen + * already fully set up this function refreshes all corresponding + * values and informs the GUI. + */ + void updatePreferences(); private: bool sendMasterVolumeFeedback(); bool sendStripVolumeFeedback( int nStrip ); @@ -394,9 +406,14 @@ class CoreActionController : public H2Core::Object { * current #H2Core::Song. All unsaved changes will be lost! * * \param pSong Pointer to the #H2Core::Song to set. + * \param bRelinking Whether the drumkit last loaded should be + * relinked when under session management. This flag is used + * to distinguish between the regular load of a song file + * within a session and its replacement by another song (which + * requires an update of the linked drumkit). * \return true on success */ - bool setSong( std::shared_ptr pSong ); + bool setSong( std::shared_ptr pSong, bool bRelinking = true ); /** * Loads the drumkit specified in @a sDrumkitPath. diff --git a/src/core/EventQueue.h b/src/core/EventQueue.h index 8604098363..dbf8a6ce04 100644 --- a/src/core/EventQueue.h +++ b/src/core/EventQueue.h @@ -42,25 +42,30 @@ enum EventType { EVENT_NONE, EVENT_STATE, /** - * The list of currently played patterns in changed. + * The list of currently played patterns + * (AudioEngine::getPlayingPatterns()) did change. * - * In #Song::Mode::Song this is triggered every time the column of - * the SongEditor grid changed. Either by rolling transport or by - * relocation. + * In #Song::Mode::Song this is triggered every time transport + * reaches a new column of the SongEditor grid, either by rolling + * or relocation. In #Song::PatternMode::Selected it's triggered + * by selecting a different pattern and in + * #Song::PatternMode::Stacked as soon as transport is looped to + * the beginning after a pattern got activated or deactivated. * - * It is handled by EventListener::patternChangedEvent(). + * It is handled by EventListener::playingPatternsChangedEvent(). */ - EVENT_PATTERN_CHANGED, + EVENT_PLAYING_PATTERNS_CHANGED, /** - * A pattern was added, deleted, or modified. + * Used in #Song::PatternMode::Stacked to indicate that a either + * AudioEngine::getNextPatterns() did change. + * + * It is handled by EventListener::nextPatternsChangedEvent(). */ - EVENT_PATTERN_MODIFIED, + EVENT_NEXT_PATTERNS_CHANGED, /** - * Used in stacked pattern mode to indicate that a either - * AudioEngine::m_pNextPatterns or AudioEngine::m_pPlayingPatterns - * changed. + * A pattern was added, deleted, or modified. */ - EVENT_STACKED_PATTERNS_CHANGED, + EVENT_PATTERN_MODIFIED, /** Another pattern was selected via MIDI or the GUI without * affecting the audio transport. While the selection in the * former case already happens in the GUI, this event will be used diff --git a/src/core/Helpers/Filesystem.cpp b/src/core/Helpers/Filesystem.cpp index c8f8a63fbf..672fd5a519 100644 --- a/src/core/Helpers/Filesystem.cpp +++ b/src/core/Helpers/Filesystem.cpp @@ -745,7 +745,7 @@ QString Filesystem::drumkit_path_search( const QString& dk_name, Lookup lookup, if ( Hydrogen::get_instance()->isUnderSessionManagement() ) { QString sDrumkitPath = QString( "%1/%2" ) - .arg( NsmClient::get_instance()->m_sSessionFolderPath ) + .arg( NsmClient::get_instance()->getSessionFolderPath() ) .arg( "drumkit" ); // If the path is symbolic link, dereference it. @@ -758,14 +758,16 @@ QString Filesystem::drumkit_path_search( const QString& dk_name, Lookup lookup, // drumkit (using its name). QString sDrumkitXMLPath = QString( "%1/%2" ) .arg( sDrumkitPath ).arg( "drumkit.xml" ); + QString sSessionDrumkitName = Drumkit::loadNameFrom( sDrumkitPath ); - if ( dk_name == Drumkit::loadNameFrom( sDrumkitXMLPath ) ) { + if ( dk_name == sSessionDrumkitName ) { // The local drumkit seems legit. return sDrumkitPath; } else if ( ! bSilent ) { - NsmClient::printError( QString( "Local drumkit [%1] and the one referenced in the .h2song file [%2] do not match!" ) + NsmClient::printError( QString( "Local drumkit [%1] name [%2] and the one stored in .h2song file [%3] do not match!" ) .arg( sDrumkitXMLPath ) + .arg( sSessionDrumkitName ) .arg( dk_name ) ); } } @@ -811,6 +813,31 @@ QString Filesystem::drumkit_dir_search( const QString& dk_name, Lookup lookup ) } bool Filesystem::drumkit_valid( const QString& dk_path ) { +#ifdef H2CORE_HAVE_OSC + auto pHydrogen = Hydrogen::get_instance(); + if ( pHydrogen != nullptr && + pHydrogen->isUnderSessionManagement() ) { + + // Explicit handling for relative drumkit paths supported in + // the session management. + QFileInfo info( dk_path ); + if ( info.isRelative() ) { + QString sAbsoluteDrumkitPath = QString( "%1%2" ) + .arg( NsmClient::get_instance()->getSessionFolderPath() ) + // remove the leading dot indicating that the path is relative. + .arg( dk_path.right( dk_path.size() - 1 ) ); + + QFileInfo infoAbs( sAbsoluteDrumkitPath ); + if ( infoAbs.isSymLink() ) { + sAbsoluteDrumkitPath = infoAbs.symLinkTarget(); + } + + return file_readable( sAbsoluteDrumkitPath + "/" + + DRUMKIT_XML, true ); + } + } +#endif + return file_readable( dk_path + "/" + DRUMKIT_XML, true); } QString Filesystem::drumkit_file( const QString& dk_path ) @@ -963,6 +990,25 @@ QString Filesystem::absolute_path( const QString& sFilename, bool bSilent ) { return QString(); } + +QString Filesystem::ensure_session_compatibility( const QString& sPath ) { +#ifdef H2CORE_HAVE_OSC + auto pHydrogen = Hydrogen::get_instance(); + if ( pHydrogen != nullptr && + pHydrogen->isUnderSessionManagement() ) { + + QFileInfo info( sPath ); + if ( info.isRelative() ) { + return QString( "%1%2" ) + .arg( NsmClient::get_instance()->getSessionFolderPath() ) + // remove the leading dot indicating that the path is relative. + .arg( sPath.right( sPath.size() - 1 ) ); + } + } +#endif + + return sPath; +} }; /* vim: set softtabstop=4 noexpandtab: */ diff --git a/src/core/Helpers/Filesystem.h b/src/core/Helpers/Filesystem.h index 0de8b0a0b9..31c16bb378 100644 --- a/src/core/Helpers/Filesystem.h +++ b/src/core/Helpers/Filesystem.h @@ -395,6 +395,12 @@ namespace H2Core * Convert a direct to an absolute path. */ static QString absolute_path( const QString& sFilename, bool bSilent = false ); + /** + * If Hydrogen is under session management, we support for paths + * relative to the session folder. This is required to allow for + * sessions being renamed or duplicated. + */ + static QString ensure_session_compatibility( const QString& sPath ); /** * writes to a file * \param dst the destination path diff --git a/src/core/Helpers/Random.cpp b/src/core/Helpers/Random.cpp new file mode 100644 index 0000000000..90e329241c --- /dev/null +++ b/src/core/Helpers/Random.cpp @@ -0,0 +1,38 @@ +/* + * Hydrogen + * Copyright(c) 2008-2021 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 + +namespace H2Core { + +float Random::getGaussian( float fStandardDeviation ) { + // gaussian distribution -- dimss + float x1, x2, w; + do { + x1 = 2.0 * ( ( ( float ) rand() ) / static_cast(RAND_MAX) ) - 1.0; + x2 = 2.0 * ( ( ( float ) rand() ) / static_cast(RAND_MAX) ) - 1.0; + w = x1 * x1 + x2 * x2; + } while ( w >= 1.0 ); + + w = sqrtf( ( -2.0 * logf( w ) ) / w ); + return x1 * w * fStandardDeviation + 0.0; // tunable +} +}; diff --git a/src/core/Helpers/Random.h b/src/core/Helpers/Random.h new file mode 100644 index 0000000000..5fe5d61347 --- /dev/null +++ b/src/core/Helpers/Random.h @@ -0,0 +1,50 @@ +/* + * Hydrogen + * Copyright(c) 2008-2021 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 + * + */ + +#ifndef H2C_RANDOM_H +#define H2C_RANDOM_H + +#include + +namespace H2Core +{ + +/** + * Container for functions generating random number. + * + * \ingroup docCore + */ +class Random : public H2Core::Object +{ + H2_OBJECT(Random) +public: + /** + * Draws an uncorrelated random value from a Gaussian distribution + * of mean 0 and @a fStandardDeviation. + * + * @param fStandardDeviation Defines the width of the distribution used. + */ + static float getGaussian( float fStandardDeviation ); +}; + +}; + +#endif // H2C_RANDOM_H diff --git a/src/core/Hydrogen.cpp b/src/core/Hydrogen.cpp index 4a1c655081..3fb277f357 100644 --- a/src/core/Hydrogen.cpp +++ b/src/core/Hydrogen.cpp @@ -49,7 +49,7 @@ #include #include #include -#include +#include #include #include #include @@ -111,6 +111,8 @@ Hydrogen::Hydrogen() : m_nSelectedInstrumentNumber( 0 ) , m_oldEngineMode( Song::Mode::Song ) , m_bOldLoopEnabled( false ) , m_nLastRecordedMIDINoteTick( 0 ) + , m_bSessionDrumkitNeedsRelinking( false ) + , m_bSessionIsExported( false ) { if ( __instance ) { ERRORLOG( "Hydrogen audio engine is already running" ); @@ -282,7 +284,7 @@ void Hydrogen::loadPlaybackTrack( QString sFilename ) EventQueue::get_instance()->push_event( EVENT_PLAYBACK_TRACK_CHANGED, 0 ); } -void Hydrogen::setSong( std::shared_ptr pSong ) +void Hydrogen::setSong( std::shared_ptr pSong, bool bRelinking ) { assert ( pSong ); @@ -307,7 +309,6 @@ void Hydrogen::setSong( std::shared_ptr pSong ) pSong->setFilename( pCurrentSong->getFilename() ); } removeSong(); - // delete pCurrentSong; } // In order to allow functions like audioEngine_setupLadspaFX() to @@ -316,6 +317,13 @@ void Hydrogen::setSong( std::shared_ptr pSong ) // audioEngine_setSong(). __song = pSong; + // Ensure the selected instrument is within the range of new + // instrument list. + if ( m_nSelectedInstrumentNumber >= __song->getInstrumentList()->size() ) { + m_nSelectedInstrumentNumber = + std::max( __song->getInstrumentList()->size() - 1, 0 ); + } + // Update the audio engine to work with the new song. m_pAudioEngine->setSong( pSong ); @@ -327,8 +335,8 @@ void Hydrogen::setSong( std::shared_ptr pSong ) m_pCoreActionController->initExternalControlInterfaces(); #ifdef H2CORE_HAVE_OSC - if ( isUnderSessionManagement() ) { - NsmClient::linkDrumkit( NsmClient::get_instance()->m_sSessionFolderPath, true ); + if ( isUnderSessionManagement() && bRelinking ) { + setSessionDrumkitNeedsRelinking( true ); } #endif } @@ -353,6 +361,7 @@ void Hydrogen::addRealtimeNote( int nInstrument, { AudioEngine* pAudioEngine = m_pAudioEngine; + auto pSampler = pAudioEngine->getSampler(); Preferences *pPref = Preferences::get_instance(); unsigned int nRealColumn = 0; unsigned res = pPref->getPatternEditorGridResolution(); @@ -383,11 +392,12 @@ void Hydrogen::addRealtimeNote( int nInstrument, // Get current partern and column, compensating for "lookahead" if required const Pattern* pCurrentPattern = nullptr; long nTickInPattern = 0; - long long nLookaheadInFrames = m_pAudioEngine->getLookaheadInFrames( pAudioEngine->getTick() ); + long long nLookaheadInFrames = m_pAudioEngine->getLookaheadInFrames(); long nLookaheadTicks = - static_cast(std::floor(m_pAudioEngine->computeTickFromFrame( pAudioEngine->getFrames() + - nLookaheadInFrames ) - - m_pAudioEngine->getTick())); + static_cast(std::floor( + TransportPosition::computeTickFromFrame( pAudioEngine->getTransportPosition()->getFrame() + + nLookaheadInFrames ) - + m_pAudioEngine->getTransportPosition()->getTick())); bool doRecord = pPref->getRecordEvents(); if ( getMode() == Song::Mode::Song && doRecord && @@ -397,8 +407,8 @@ void Hydrogen::addRealtimeNote( int nInstrument, // Recording + song playback mode + actually playing PatternList* pPatternList = pSong->getPatternList(); auto pColumns = pSong->getPatternGroupVector(); - int nColumn = pAudioEngine->getColumn(); // current column - // or pattern group + int nColumn = pAudioEngine->getTransportPosition()->getColumn(); // current column + // or pattern group if ( nColumn < 0 || nColumn >= pColumns->size() ) { pAudioEngine->unlock(); // unlock the audio engine ERRORLOG( QString( "Provided column [%1] out of bound [%2,%3)" ) @@ -407,7 +417,7 @@ void Hydrogen::addRealtimeNote( int nInstrument, return; } // Locate nTickInPattern -- may need to jump back one column - nTickInPattern = pAudioEngine->getPatternTickPosition(); + nTickInPattern = pAudioEngine->getTransportPosition()->getPatternTickPosition(); while ( nTickInPattern < nLookaheadTicks ) { nColumn -= 1; if ( nColumn < 0 || nColumn >= pColumns->size() ) { @@ -465,7 +475,7 @@ void Hydrogen::addRealtimeNote( int nInstrument, } // Locate nTickInPattern -- may need to wrap around end of pattern - nTickInPattern = pAudioEngine->getPatternTickPosition(); + nTickInPattern = pAudioEngine->getTransportPosition()->getPatternTickPosition(); if ( nTickInPattern >= nLookaheadTicks ) { nTickInPattern -= nLookaheadTicks; } else { @@ -495,7 +505,6 @@ void Hydrogen::addRealtimeNote( int nInstrument, nInstrumentNumber = m_nInstrumentLookupTable[ nInstrument ]; } auto pInstr = pInstrumentList->get( nInstrumentNumber ); - if ( pInstr == nullptr ) { ERRORLOG( QString( "Unable to retrieved instrument [%1]. Plays selected instrument: [%2]" ) .arg( nInstrumentNumber ) @@ -515,7 +524,7 @@ void Hydrogen::addRealtimeNote( int nInstrument, int nPatternSize = pCurrentPattern->get_length(); int nNoteLength = - static_cast(pAudioEngine->getPatternTickPosition()) - + static_cast(pAudioEngine->getTransportPosition()->getPatternTickPosition()) - m_nLastRecordedMIDINoteTick; if ( bPlaySelectedInstrument ) { @@ -580,12 +589,12 @@ void Hydrogen::addRealtimeNote( int nInstrument, // Play back the note. if ( bPlaySelectedInstrument ) { if ( bNoteOff ) { - if ( pAudioEngine->getSampler()->isInstrumentPlaying( pInstr ) ) { - pAudioEngine->getSampler()->midiKeyboardNoteOff( nNote ); + if ( pSampler->isInstrumentPlaying( pInstr ) ) { + pSampler->midiKeyboardNoteOff( nNote ); } } else { // note on - Note *pNote2 = new Note( pInstr, nRealColumn, fVelocity, fPan, -1, 0 ); + Note *pNote2 = new Note( pInstr, nRealColumn, fVelocity, fPan ); int divider = nNote / 12; Note::Octave octave = (Note::Octave)(divider -3); @@ -597,14 +606,14 @@ void Hydrogen::addRealtimeNote( int nInstrument, } else { if ( bNoteOff ) { - if ( pAudioEngine->getSampler()->isInstrumentPlaying( pInstr ) ) { - Note *pNoteOff = new Note( pInstr, 0.0, 0.0, 0.0, -1, 0 ); + if ( pSampler->isInstrumentPlaying( pInstr ) ) { + Note *pNoteOff = new Note( pInstr ); pNoteOff->set_note_off( true ); midi_noteOn( pNoteOff ); } } else { // note on - Note *pNote2 = new Note( pInstr, nRealColumn, fVelocity, fPan, -1, 0 ); + Note *pNote2 = new Note( pInstr, nRealColumn, fVelocity, fPan ); midi_noteOn( pNote2 ); } } @@ -618,7 +627,7 @@ void Hydrogen::toggleNextPattern( int nPatternNumber ) { m_pAudioEngine->lock( RIGHT_HERE ); m_pAudioEngine->toggleNextPattern( nPatternNumber ); m_pAudioEngine->unlock(); - EventQueue::get_instance()->push_event( EVENT_STACKED_PATTERNS_CHANGED, 0 ); + EventQueue::get_instance()->push_event( EVENT_NEXT_PATTERNS_CHANGED, 0 ); } else { ERRORLOG( "can't set next pattern in song mode" ); @@ -630,7 +639,7 @@ bool Hydrogen::flushAndAddNextPattern( int nPatternNumber ) { m_pAudioEngine->lock( RIGHT_HERE ); m_pAudioEngine->flushAndAddNextPattern( nPatternNumber ); m_pAudioEngine->unlock(); - EventQueue::get_instance()->push_event( EVENT_STACKED_PATTERNS_CHANGED, 0 ); + EventQueue::get_instance()->push_event( EVENT_NEXT_PATTERNS_CHANGED, 0 ); return true; @@ -655,8 +664,12 @@ bool Hydrogen::startExportSession( int nSampleRate, int nSampleDepth ) } std::shared_ptr pSong = getSong(); + if ( pSong == nullptr ) { + ERRORLOG( "No song set yet" ); + return false; + } - m_oldEngineMode = getMode(); + m_oldEngineMode = pSong->getMode(); m_bOldLoopEnabled = pSong->isLoopEnabled(); pSong->setMode( Song::Mode::Song ); @@ -668,7 +681,7 @@ bool Hydrogen::startExportSession( int nSampleRate, int nSampleDepth ) * Stop the current driver and fire up the DiskWriter. */ pAudioEngine->stopAudioDrivers(); - + AudioOutput* pDriver = pAudioEngine->createAudioDriver( "DiskWriterDriver" ); @@ -865,7 +878,10 @@ void Hydrogen::setTapTempo( float fInterval ) fOldBpm2 = fOldBpm1; fOldBpm1 = fBPM; + m_pAudioEngine->lock( RIGHT_HERE ); m_pAudioEngine->setNextBpm( fBPM ); + m_pAudioEngine->unlock(); + // Store it's value in the .h2song file. getSong()->setBpm( fBPM ); @@ -912,7 +928,7 @@ void Hydrogen::setSelectedPatternNumber( int nPat, bool bNeedsLock ) m_nSelectedPatternNumber = nPat; // The specific values provided are not important since we a // in selected pattern mode. - m_pAudioEngine->updatePlayingPatterns( 0, 0 ); + m_pAudioEngine->updatePlayingPatterns(); if ( bNeedsLock ) { m_pAudioEngine->unlock(); @@ -1053,7 +1069,11 @@ bool Hydrogen::handleBeatCounter() (float) ((int) (60 / nBeatDiffAverage * 100)) / 100; + + m_pAudioEngine->lock( RIGHT_HERE ); m_pAudioEngine->setNextBpm( fBeatCountBpm ); + m_pAudioEngine->unlock(); + getSong()->setBpm( fBeatCountBpm ); EventQueue::get_instance()->push_event( EVENT_TEMPO_CHANGED, -1 ); @@ -1320,15 +1340,8 @@ void Hydrogen::setPatternMode( Song::PatternMode mode ) __song->setPatternMode( mode ); setIsModified( true ); - if ( mode == Song::PatternMode::Selected || - m_pAudioEngine->getState() != AudioEngine::State::Playing ) { - // Only update the playing patterns in selected pattern - // mode or if transport is not rolling. In stacked pattern - // mode with transport rolling - // AudioEngine::updatePatternTransportPosition() will call - // the functions and activate the next patterns once the - // current ones are looped. - m_pAudioEngine->updatePlayingPatterns( m_pAudioEngine->getColumn() ); + if ( m_pAudioEngine->getState() != AudioEngine::State::Playing ) { + m_pAudioEngine->updatePlayingPatterns(); m_pAudioEngine->clearNextPatterns(); } @@ -1513,6 +1526,12 @@ int Hydrogen::getColumnForTick( long nTick, bool bLoopMode, long* pPatternStartT std::vector *pPatternColumns = pSong->getPatternGroupVector(); int nColumns = pPatternColumns->size(); + if ( nColumns == 0 ) { + // There are no patterns in the current song. + *pPatternStartTick = 0; + return 0; + } + // Sum the lengths of all pattern columns and use the macro // MAX_NOTES in case some of them are of size zero. If the // supplied value nTick is bigger than this and doesn't belong to @@ -1573,7 +1592,8 @@ long Hydrogen::getTickForColumn( int nColumn ) const const int nPatternGroups = pSong->getPatternGroupVector()->size(); if ( nPatternGroups == 0 ) { - return -1; + // No patterns in song. + return 0; } if ( nColumn >= nPatternGroups ) { @@ -1713,8 +1733,14 @@ QString Hydrogen::toQString( const QString& sPrefix, bool bShort ) const { } } sOutput.append( QString( "%1%2m_nSelectedInstrumentNumber: %3\n" ).arg( sPrefix ).arg( s ).arg( m_nSelectedInstrumentNumber ) ) - .append( QString( "%1%2m_pAudioEngine: \n" ).arg( sPrefix ).arg( s ) )//.arg( m_pAudioEngine ) ) - .append( QString( "%1%2lastMidiEvent: %3\n" ).arg( sPrefix ).arg( s ).arg( m_LastMidiEvent ) ) + .append( QString( "%1%2m_pAudioEngine:\n" ).arg( sPrefix ).arg( s ) ); + if ( m_pAudioEngine != nullptr ) { + sOutput.append( QString( "%1" ) + .arg( m_pAudioEngine->toQString( sPrefix + s + s, bShort ) ) ); + } else { + sOutput.append( QString( "nullptr\n" ) ); + } + sOutput.append( QString( "%1%2lastMidiEvent: %3\n" ).arg( sPrefix ).arg( s ).arg( m_LastMidiEvent ) ) .append( QString( "%1%2lastMidiEventParameter: %3\n" ).arg( sPrefix ).arg( s ).arg( m_nLastMidiEventParameter ) ) .append( QString( "%1%2m_nInstrumentLookupTable: [ %3 ... %4 ]\n" ).arg( sPrefix ).arg( s ) .arg( m_nInstrumentLookupTable[ 0 ] ).arg( m_nInstrumentLookupTable[ MAX_INSTRUMENTS -1 ] ) ); @@ -1759,8 +1785,14 @@ QString Hydrogen::toQString( const QString& sPrefix, bool bShort ) const { } } sOutput.append( QString( ", m_nSelectedInstrumentNumber: %1" ).arg( m_nSelectedInstrumentNumber ) ) - .append( QString( ", m_pAudioEngine: " ) )// .arg( m_pAudioEngine ) ) - .append( QString( ", lastMidiEvent: %1" ).arg( m_LastMidiEvent ) ) + .append( ", m_pAudioEngine:" ); + if ( m_pAudioEngine != nullptr ) { + sOutput.append( QString( "%1" ) + .arg( m_pAudioEngine->toQString( sPrefix, bShort ) ) ); + } else { + sOutput.append( QString( " nullptr" ) ); + } + sOutput.append( QString( ", lastMidiEvent: %1" ).arg( m_LastMidiEvent ) ) .append( QString( ", lastMidiEventParameter: %1" ).arg( m_nLastMidiEventParameter ) ) .append( QString( ", m_nInstrumentLookupTable: [ %1 ... %2 ]" ) .arg( m_nInstrumentLookupTable[ 0 ] ).arg( m_nInstrumentLookupTable[ MAX_INSTRUMENTS -1 ] ) ); diff --git a/src/core/Hydrogen.h b/src/core/Hydrogen.h index 23d455bd41..8fbd4d5b37 100644 --- a/src/core/Hydrogen.h +++ b/src/core/Hydrogen.h @@ -39,8 +39,6 @@ #include #include -inline int randomValue( int max ); - namespace H2Core { class CoreActionController; @@ -81,7 +79,7 @@ class Hydrogen : public H2Core::Object /** * Returns the current Hydrogen instance #__instance. */ - static Hydrogen* get_instance(){ assert(__instance); return __instance; }; + static Hydrogen* get_instance(){ return __instance; }; /** * Destructor taking care of most of the clean up. @@ -122,8 +120,13 @@ class Hydrogen : public H2Core::Object /** * Sets the current song #__song to @a newSong. * \param newSong Pointer to the new Song object. + * \param bRelinking Whether the drumkit last loaded should be + * relinked when under session management. This flag is used + * to distinguish between the regular load of a song file + * within a session and its replacement by another song (which + * requires an update of the linked drumkit). */ - void setSong ( std::shared_ptr newSong ); + void setSong ( std::shared_ptr newSong, bool bRelinking = true ); /** * Find a PatternList/column corresponding to the supplied tick @@ -443,6 +446,11 @@ void previewSample( Sample *pSample ); supported.*/ bool isUnderSessionManagement() const; + void setSessionDrumkitNeedsRelinking( bool bNeedsRelinking ); + bool getSessionDrumkitNeedsRelinking() const; + void setSessionIsExported( bool bIsExported ); + bool getSessionIsExported() const; + ///midi lookuptable int m_nInstrumentLookupTable[MAX_INSTRUMENTS]; @@ -553,13 +561,35 @@ void previewSample( Sample *pSample ); */ int m_nSelectedPatternNumber; + /** + * When using Hydrogen with session management it tries to keep + * all central files within a session folder instead of using the + * once found at the data folder at either user or system + * level. This allows to zip and transfer a session without + * requiring to move the whole data folder as well. + * + * As sample files can be quite large in both size and number the + * drumkit is only linked into the session folder. + * + * This variable indicates whether a different drumkit was loaded + * into the current song (by either directly loading a drumkit or + * replacing the entire song) and thus a relinking is required + * upon saving the song. + */ + bool m_bSessionDrumkitNeedsRelinking; + /** + * Indicates whether NSM session is saved or exported when entering + * the CoreActionController::saveSong() function. + */ + bool m_bSessionIsExported; + /** * Onset of the recorded last in addRealtimeNote(). It is used to * determine the custom length of the note in case the note on * event is followed by a note off event. */ int m_nLastRecordedMIDINoteTick; - /* + /** * Central instance of the audio engine. */ AudioEngine* m_pAudioEngine; @@ -626,6 +656,19 @@ inline int Hydrogen::getSelectedInstrumentNumber() const { return m_nSelectedInstrumentNumber; } + +inline void Hydrogen::setSessionDrumkitNeedsRelinking( bool bNeedsRelinking ) { + m_bSessionDrumkitNeedsRelinking = bNeedsRelinking; +} +inline bool Hydrogen::getSessionDrumkitNeedsRelinking() const { + return m_bSessionDrumkitNeedsRelinking; +} +inline void Hydrogen::setSessionIsExported( bool bSessionIsExported ) { + m_bSessionIsExported = bSessionIsExported; +} +inline bool Hydrogen::getSessionIsExported() const { + return m_bSessionIsExported; +} }; #endif diff --git a/src/core/IO/DiskWriterDriver.cpp b/src/core/IO/DiskWriterDriver.cpp index 642125914d..672e79b09d 100644 --- a/src/core/IO/DiskWriterDriver.cpp +++ b/src/core/IO/DiskWriterDriver.cpp @@ -200,13 +200,12 @@ void* diskWriterDriver_thread( void* param ) nPatternLengthInFrames - nFrameNumber < pDriver->m_nBufferSize ){ nLastRun = nPatternLengthInFrames - nFrameNumber; nUsedBuffer = nLastRun; - }; int ret = pDriver->m_processCallback( nUsedBuffer, nullptr ); // In case the DiskWriter couldn't acquire the lock of the AudioEngine. - while( ret != 0 ) { + while( ret == 2 ) { ret = pDriver->m_processCallback( nUsedBuffer, nullptr ); } diff --git a/src/core/IO/JackAudioDriver.cpp b/src/core/IO/JackAudioDriver.cpp index c298669370..d78d28abb2 100644 --- a/src/core/IO/JackAudioDriver.cpp +++ b/src/core/IO/JackAudioDriver.cpp @@ -33,7 +33,7 @@ #include #include -#include +#include #include #include #include @@ -486,7 +486,7 @@ bool JackAudioDriver::compareAdjacentBBT() const return true; } -void JackAudioDriver::updateTransportInfo() +void JackAudioDriver::updateTransportPosition() { if ( Preferences::get_instance()->m_bJackTransportMode != Preferences::USE_JACK_TRANSPORT ){ @@ -568,12 +568,13 @@ void JackAudioDriver::updateTransportInfo() // The relocation could be either triggered by an user interaction // (e.g. clicking the forward button or clicking somewhere on the // timeline) or by a different JACK client. - if ( pAudioEngine->getFrames() - pAudioEngine->getFrameOffset() != + if ( ( pAudioEngine->getTransportPosition()->getFrame() - + pAudioEngine->getTransportPosition()->getFrameOffsetTempo() ) != m_JackTransportPos.frame ) { // DEBUGLOG( QString( "[relocation detected] frames: %1, offset: %2, Jack frames: %3" ) - // .arg( pAudioEngine->getFrames() ) - // .arg( pAudioEngine->getFrameOffset() ) + // .arg( pAudioEngine->getTransportPosition()->getFrame() ) + // .arg( pAudioEngine->getTransportPosition()->getFrameOffsetTempo() ) // .arg( m_JackTransportPos.frame ) ); if ( ! bTimebaseEnabled || m_timebaseState != Timebase::Slave ) { @@ -589,7 +590,7 @@ void JackAudioDriver::updateTransportInfo() // There is a JACK timebase master and it's not us. If it // provides a tempo that differs from the local one, we will // use the former instead. - if ( pAudioEngine->getBpm() != + if ( pAudioEngine->getTransportPosition()->getBpm() != static_cast(m_JackTransportPos.beats_per_minute ) || !compareAdjacentBBT() ) { relocateUsingBBT(); @@ -1086,7 +1087,7 @@ void JackAudioDriver::JackTimebaseCallback(jack_transport_state_t state, Hydrogen* pHydrogen = Hydrogen::get_instance(); std::shared_ptr pSong = pHydrogen->getSong(); - auto pAudioEngine = pHydrogen->getAudioEngine(); + auto pPos = pHydrogen->getAudioEngine()->getTransportPosition(); if ( pSong == nullptr ) { // DEBUGLOG( "No song set." ); return; @@ -1119,28 +1120,28 @@ void JackAudioDriver::JackTimebaseCallback(jack_transport_state_t state, pJackPosition->beats_per_bar = fNumerator; // Time signature "denominator" pJackPosition->beat_type = fDenumerator; - pJackPosition->beats_per_minute = static_cast(pAudioEngine->getBpm()); + pJackPosition->beats_per_minute = static_cast(pPos->getBpm()); - if ( pAudioEngine->getFrames() < 1 ) { + if ( pPos->getFrame() < 1 ) { pJackPosition->bar = 1; pJackPosition->beat = 1; pJackPosition->tick = 0; pJackPosition->bar_start_tick = 0; } else { // +1 since the counting bars starts at 1. - pJackPosition->bar = pAudioEngine->getColumn() + 1; + pJackPosition->bar = pPos->getColumn() + 1; // Number of ticks that have elapsed between frame 0 and the // first beat of the next measure. - pJackPosition->bar_start_tick = pAudioEngine->getPatternStartTick(); + pJackPosition->bar_start_tick = pPos->getPatternStartTick(); - pJackPosition->beat = pAudioEngine->getPatternTickPosition() / + pJackPosition->beat = pPos->getPatternTickPosition() / pJackPosition->ticks_per_beat; // +1 since the counting beats starts at 1. pJackPosition->beat++; // Counting ticks starts at 0. - pJackPosition->tick = pAudioEngine->getPatternTickPosition(); + pJackPosition->tick = pPos->getPatternTickPosition(); } @@ -1180,7 +1181,8 @@ void JackAudioDriver::printState() const { std::cout << "\033[35m[Hydrogen] [JackAudioDriver state]" << ", m_JackTransportState: " << m_JackTransportState << ", m_timebaseState: " << static_cast(m_timebaseState) - << ", current pattern column: " << pHydrogen->getAudioEngine()->getColumn() + << ", current pattern column: " + << pHydrogen->getAudioEngine()->getTransportPosition()->getColumn() << "\33[0m" << std::endl; } diff --git a/src/core/IO/JackAudioDriver.h b/src/core/IO/JackAudioDriver.h index 9928f46b93..894b43eaa8 100644 --- a/src/core/IO/JackAudioDriver.h +++ b/src/core/IO/JackAudioDriver.h @@ -60,11 +60,11 @@ class InstrumentComponent; * _JackTransportRolling_ and the transport position is updated * according to the request. * - * Also note that Hydrogen overwrites its local TransportInfo only + * Also note that Hydrogen overwrites its local TransportPosition only * with the transport position of the JACK server if there is a * mismatch due to a relocation triggered by another JACK * client. During normal transport the current position - * TransportInfo::m_nFrames will be always the same as the one of JACK + * TransportPosition::m_nFrames will be always the same as the one of JACK * during a cycle and incremented by the buffer size in * audioEngine_process() at the very end of the cycle. The same * happens for the transport information of the JACK server but in @@ -286,7 +286,7 @@ class JackAudioDriver : public Object, public AudioOutput * #m_JackTransportPos and in #m_JackTransportState, and updates * the AudioEngine in case of a mismatch. */ - void updateTransportInfo(); + void updateTransportPosition(); /** * Registers Hydrogen as JACK timebase master. @@ -558,14 +558,14 @@ class JackAudioDriver : public Object, public AudioOutput * releasing Hydrogen in the later case, it won't advertise this * fact but simply won't call the JackTimebaseCallback() * anymore. But since this will be called in every cycle after - * updateTransportInfo(), we can use this variable to determine if + * updateTransportPosition(), we can use this variable to determine if * Hydrogen is still timebase master. * * As Hydrogen registered as timebase master using * initTimebaseMaster() it will be initialized with 1, decremented - * in updateTransportInfo(), and reset to 1 in + * in updateTransportPosition(), and reset to 1 in * JackTimebaseCallback(). Whenever it is zero in - * updateTransportInfo(), #m_nTimebaseTracking will be updated + * updateTransportPosition(), #m_nTimebaseTracking will be updated * accordingly. */ int m_nTimebaseTracking; @@ -611,7 +611,7 @@ class JackAudioDriver : public NullDriver { // Required since these functions are a friend of AudioEngine which // need to be build even if no JACK support is desired. - void updateTransportInfo() {} + void updateTransportPosition() {} void relocateUsingBBT() {} }; diff --git a/src/core/IO/MidiInput.cpp b/src/core/IO/MidiInput.cpp index 432a8ab2c5..ae275bcce1 100644 --- a/src/core/IO/MidiInput.cpp +++ b/src/core/IO/MidiInput.cpp @@ -229,7 +229,6 @@ void MidiInput::handleNoteOnMessage( const MidiMessage& msg ) MidiActionManager * pMidiActionManager = MidiActionManager::get_instance(); MidiMap * pMidiMap = MidiMap::get_instance(); Hydrogen *pHydrogen = Hydrogen::get_instance(); - AudioEngine* pAudioEngine = pHydrogen->getAudioEngine(); auto pPref = Preferences::get_instance(); pHydrogen->m_LastMidiEvent = "NOTE"; diff --git a/src/core/Lilipond/Lilypond.cpp b/src/core/Lilipond/Lilypond.cpp index 8f486450d8..eb70afb6dc 100644 --- a/src/core/Lilipond/Lilypond.cpp +++ b/src/core/Lilipond/Lilypond.cpp @@ -70,7 +70,7 @@ void H2Core::LilyPond::extractData( const Song &song ) { // Get the main information about the music const std::vector *group = song.getPatternGroupVector(); - if ( !group ) { + if ( !group || group->size() == 0 ) { m_Measures.clear(); return; } diff --git a/src/core/Logger.cpp b/src/core/Logger.cpp index 7833647493..a27bf16c7e 100644 --- a/src/core/Logger.cpp +++ b/src/core/Logger.cpp @@ -27,7 +27,6 @@ #include #include #include -#include #ifdef WIN32 #include @@ -54,14 +53,11 @@ void* loggerThread_func( void* param ) { #endif FILE* log_file = nullptr; if ( logger->__use_file ) { - - QString sLogFilename = Filesystem::log_file_path(); - - log_file = fopen( sLogFilename.toLocal8Bit(), "w" ); - if ( log_file ) { - fprintf( log_file, "Start logger" ); - } else { - fprintf( stderr, "Error: can't open log file for writing...\n" ); + log_file = fopen( logger->m_sLogFilePath.toLocal8Bit().data(), "w" ); + if ( ! log_file ) { + fprintf( stderr, + QString( "Error: can't open log file [%1] for writing...\n" ) + .arg( logger->m_sLogFilePath ).toLocal8Bit().data() ); } } Logger::queue_t* queue = &logger->__msg_queue; @@ -74,7 +70,9 @@ void* loggerThread_func( void* param ) { if( !queue->empty() ) { for( it = last = queue->begin() ; it != queue->end() ; ++it ) { last = it; - fprintf( stdout, "%s", it->toLocal8Bit().data() ); + if ( logger->m_bUseStdout ) { + fprintf( stdout, "%s", it->toLocal8Bit().data() ); + } if( log_file ) { fprintf( log_file, "%s", it->toLocal8Bit().data() ); fflush( log_file ); @@ -100,18 +98,35 @@ void* loggerThread_func( void* param ) { return nullptr; } -Logger* Logger::bootstrap( unsigned msk ) { +Logger* Logger::bootstrap( unsigned msk, const QString& sLogFilePath, bool bUseStdout ) { Logger::set_bit_mask( msk ); - return Logger::create_instance(); + return Logger::create_instance( sLogFilePath, bUseStdout ); } -Logger* Logger::create_instance() { - if ( __instance == nullptr ) __instance = new Logger; +Logger* Logger::create_instance( const QString& sLogFilePath, bool bUseStdout ) { + if ( __instance == nullptr ) __instance = new Logger( sLogFilePath, bUseStdout ); return __instance; } -Logger::Logger() : __use_file( true ), __running( true ) { +Logger::Logger( const QString& sLogFilePath, bool bUseStdout ) : + __use_file( true ), + __running( true ), + m_sLogFilePath( sLogFilePath ), + m_bUseStdout( bUseStdout ) { __instance = this; + + // Sanity checks. + QFileInfo fiLogFile( m_sLogFilePath ); + QFileInfo fiParentFolder( fiLogFile.absolutePath() ); + if ( ( fiLogFile.exists() && ! fiLogFile.isWritable() ) || + ( ! fiLogFile.exists() && ! fiParentFolder.isWritable() ) ) { + m_sLogFilePath = ""; + } + + if ( m_sLogFilePath.isEmpty() ) { + m_sLogFilePath = Filesystem::log_file_path(); + } + pthread_attr_t attr; pthread_attr_init( &attr ); pthread_mutex_init( &__mutex, nullptr ); diff --git a/src/core/Logger.h b/src/core/Logger.h index 1b44025055..81c33d68bd 100644 --- a/src/core/Logger.h +++ b/src/core/Logger.h @@ -27,10 +27,10 @@ #include #include #include +#include #include -class QString; class QStringList; namespace H2Core { @@ -59,14 +59,14 @@ class Logger { * create the logger instance if not exists, set the log level and return the instance * \param msk the logging level bitmask */ - static Logger* bootstrap( unsigned msk ); + static Logger* bootstrap( unsigned msk, const QString& sLogFilePath = QString(), bool bUseStdout = true ); /** * If #__instance equals 0, a new H2Core::Logger * singleton will be created and stored in it. * * It is called in Hydrogen::create_instance(). */ - static Logger* create_instance(); + static Logger* create_instance( const QString& sLogFilePath = QString(), bool bUseStdout = true ); /** * Returns a pointer to the current H2Core::Logger * singleton stored in #__instance. @@ -161,11 +161,13 @@ class Logger { static unsigned __bit_msk; ///< the bitmask of log_level_t static const char* __levels[]; ///< levels strings pthread_cond_t __messages_available; + QString m_sLogFilePath; + bool m_bUseStdout; thread_local static QString *pCrashContext; /** constructor */ - Logger(); + Logger( const QString& sLogFilePath = QString(), bool bUseStdout = true ); #ifndef HAVE_SSCANF /** diff --git a/src/core/MidiAction.cpp b/src/core/MidiAction.cpp index 100c1d39e8..9771d7b71c 100644 --- a/src/core/MidiAction.cpp +++ b/src/core/MidiAction.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -151,6 +152,7 @@ MidiActionManager::MidiActionManager() { m_actionMap.insert(std::make_pair("SELECT_NEXT_PATTERN", std::make_pair( &MidiActionManager::select_next_pattern, 1 ) )); m_actionMap.insert(std::make_pair("SELECT_ONLY_NEXT_PATTERN", std::make_pair( &MidiActionManager::select_only_next_pattern, 1 ) )); m_actionMap.insert(std::make_pair("SELECT_NEXT_PATTERN_CC_ABSOLUTE", std::make_pair( &MidiActionManager::select_next_pattern_cc_absolute, 0 ) )); + m_actionMap.insert(std::make_pair("SELECT_ONLY_NEXT_PATTERN_CC_ABSOLUTE", std::make_pair( &MidiActionManager::select_only_next_pattern_cc_absolute, 0 ) )); m_actionMap.insert(std::make_pair("SELECT_NEXT_PATTERN_RELATIVE", std::make_pair( &MidiActionManager::select_next_pattern_relative, 1 ) )); m_actionMap.insert(std::make_pair("SELECT_AND_PLAY_PATTERN", std::make_pair( &MidiActionManager::select_and_play_pattern, 1 ) )); m_actionMap.insert(std::make_pair("PAN_RELATIVE", std::make_pair( &MidiActionManager::pan_relative, 1 ) )); @@ -364,6 +366,24 @@ bool MidiActionManager::tap_tempo( std::shared_ptr , Hydrogen* pHydrogen } bool MidiActionManager::select_next_pattern( std::shared_ptr pAction, Hydrogen* pHydrogen ) { + bool ok; + return nextPatternSelection( pAction->getParameter1().toInt(&ok,10) ); +} + + +bool MidiActionManager::select_next_pattern_relative( std::shared_ptr pAction, Hydrogen* pHydrogen ) { + bool ok; + return nextPatternSelection( pHydrogen->getSelectedPatternNumber() + + pAction->getParameter1().toInt(&ok,10) ); +} + +bool MidiActionManager::select_next_pattern_cc_absolute( std::shared_ptr pAction, Hydrogen* pHydrogen ) { + bool ok; + return nextPatternSelection( pAction->getValue().toInt(&ok,10) ); +} + +bool MidiActionManager::nextPatternSelection( int nPatternNumber ) { + auto pHydrogen = Hydrogen::get_instance(); auto pSong = pHydrogen->getSong(); // Preventive measure to avoid bad things. @@ -371,25 +391,36 @@ bool MidiActionManager::select_next_pattern( std::shared_ptr pAction, Hy ERRORLOG( "No song set yet" ); return false; } - - bool ok; - int row = pAction->getParameter1().toInt(&ok,10); - if( row > pSong->getPatternList()->size() - 1 || - row < 0 ) { - ERRORLOG( QString( "Provided value [%1] out of bound [0,%2]" ).arg( row ) + + if ( nPatternNumber > pSong->getPatternList()->size() - 1 || + nPatternNumber < 0 ) { + ERRORLOG( QString( "Provided value [%1] out of bound [0,%2]" ).arg( nPatternNumber ) .arg( pSong->getPatternList()->size() - 1 ) ); return false; } + if ( pHydrogen->getPatternMode() == Song::PatternMode::Selected ) { - pHydrogen->setSelectedPatternNumber( row ); + pHydrogen->setSelectedPatternNumber( nPatternNumber ); } else if ( pHydrogen->getPatternMode() == Song::PatternMode::Stacked ) { - pHydrogen->toggleNextPattern( row ); + pHydrogen->toggleNextPattern( nPatternNumber ); } + return true; } bool MidiActionManager::select_only_next_pattern( std::shared_ptr pAction, Hydrogen* pHydrogen ) { + bool ok; + return onlyNextPatternSelection( pAction->getParameter1().toInt(&ok,10) ); +} + +bool MidiActionManager::select_only_next_pattern_cc_absolute( std::shared_ptr pAction, Hydrogen* pHydrogen ) { + bool ok; + return onlyNextPatternSelection( pAction->getValue().toInt(&ok,10) ); +} + +bool MidiActionManager::onlyNextPatternSelection( int nPatternNumber ) { + auto pHydrogen = Hydrogen::get_instance(); auto pSong = pHydrogen->getSong(); // Preventive measure to avoid bad things. @@ -398,80 +429,26 @@ bool MidiActionManager::select_only_next_pattern( std::shared_ptr pActio return false; } - bool ok; - int nRow = pAction->getParameter1().toInt(&ok,10); - if ( nRow > pSong->getPatternList()->size() -1 || - nRow < 0 ) { + if ( nPatternNumber > pSong->getPatternList()->size() -1 || + nPatternNumber < 0 ) { if ( pHydrogen->getPatternMode() == Song::PatternMode::Selected ) { ERRORLOG( QString( "Provided pattern number [%1] out of bound [0,%2]." ) - .arg( nRow ) + .arg( nPatternNumber ) .arg( pSong->getPatternList()->size() - 1 ) ); return false; } else { INFOLOG( QString( "Provided pattern number [%1] out of bound [0,%2]. All patterns will be deselected." ) - .arg( nRow ) + .arg( nPatternNumber ) .arg( pSong->getPatternList()->size() - 1 ) ); } } if ( pHydrogen->getPatternMode() == Song::PatternMode::Selected ) { - return select_next_pattern( pAction, pHydrogen ); - } - - return pHydrogen->flushAndAddNextPattern( nRow ); -} - -bool MidiActionManager::select_next_pattern_relative( std::shared_ptr pAction, Hydrogen* pHydrogen ) { - auto pSong = pHydrogen->getSong(); - - // Preventive measure to avoid bad things. - if ( pSong == nullptr ) { - ERRORLOG( "No song set yet" ); - return false; - } - - bool ok; - if( pHydrogen->getPatternMode() == Song::PatternMode::Stacked ) { - return true; - } - int row = pHydrogen->getSelectedPatternNumber() + pAction->getParameter1().toInt(&ok,10); - if( row > pSong->getPatternList()->size() - 1 || - row < 0 ) { - ERRORLOG( QString( "Provided value [%1] out of bound [0,%2]" ).arg( row ) - .arg( pSong->getPatternList()->size() - 1 ) ); - return false; - } - - pHydrogen->setSelectedPatternNumber( row ); - return true; -} - -bool MidiActionManager::select_next_pattern_cc_absolute( std::shared_ptr pAction, Hydrogen* pHydrogen ) { - // Preventive measure to avoid bad things. - if ( pHydrogen->getSong() == nullptr ) { - ERRORLOG( "No song set yet" ); - return false; + return nextPatternSelection( nPatternNumber ); } - bool ok; - int row = pAction->getValue().toInt(&ok,10); - - if( row > pHydrogen->getSong()->getPatternList()->size() - 1 || - row < 0 ) { - ERRORLOG( QString( "Provided value [%1] out of bound [0,%2]" ).arg( row ) - .arg( pHydrogen->getSong()->getPatternList()->size() - 1 ) ); - return false; - } - - if( pHydrogen->getPatternMode() == Song::PatternMode::Selected ) { - pHydrogen->setSelectedPatternNumber( row ); - } - else { - return true;// only usefully in normal pattern mode - } - - return true; + return pHydrogen->flushAndAddNextPattern( nPatternNumber ); } bool MidiActionManager::select_and_play_pattern( std::shared_ptr pAction, Hydrogen* pHydrogen ) { @@ -950,6 +927,7 @@ bool MidiActionManager::bpm_cc_relative( std::shared_ptr pAction, Hydrog } auto pAudioEngine = pHydrogen->getAudioEngine(); + const float fBpm = pAudioEngine->getTransportPosition()->getBpm(); //this Action should be triggered only by CC commands @@ -963,19 +941,23 @@ bool MidiActionManager::bpm_cc_relative( std::shared_ptr pAction, Hydrog } if ( m_nLastBpmChangeCCParameter >= cc_param && - pAudioEngine->getBpm() - mult > MIN_BPM ) { + fBpm - mult > MIN_BPM ) { // Use tempo in the next process cycle of the audio engine. - pAudioEngine->setNextBpm( pAudioEngine->getBpm() - 1*mult ); + pAudioEngine->lock( RIGHT_HERE ); + pAudioEngine->setNextBpm( fBpm - 1*mult ); + pAudioEngine->unlock(); // Store it's value in the .h2song file. - pHydrogen->getSong()->setBpm( pAudioEngine->getBpm() - 1*mult ); + pHydrogen->getSong()->setBpm( fBpm - 1*mult ); } if ( m_nLastBpmChangeCCParameter < cc_param - && pAudioEngine->getBpm() + mult < MAX_BPM ) { + && fBpm + mult < MAX_BPM ) { // Use tempo in the next process cycle of the audio engine. - pAudioEngine->setNextBpm( pAudioEngine->getBpm() + 1*mult ); + pAudioEngine->lock( RIGHT_HERE ); + pAudioEngine->setNextBpm( fBpm + 1*mult ); + pAudioEngine->unlock(); // Store it's value in the .h2song file. - pHydrogen->getSong()->setBpm( pAudioEngine->getBpm() + 1*mult ); + pHydrogen->getSong()->setBpm( fBpm + 1*mult ); } m_nLastBpmChangeCCParameter = cc_param; @@ -997,6 +979,7 @@ bool MidiActionManager::bpm_fine_cc_relative( std::shared_ptr pAction, H } auto pAudioEngine = pHydrogen->getAudioEngine(); + const float fBpm = pAudioEngine->getTransportPosition()->getBpm(); //this Action should be triggered only by CC commands bool ok; @@ -1009,18 +992,22 @@ bool MidiActionManager::bpm_fine_cc_relative( std::shared_ptr pAction, H } if ( m_nLastBpmChangeCCParameter >= cc_param && - pAudioEngine->getBpm() - mult > MIN_BPM ) { + fBpm - mult > MIN_BPM ) { // Use tempo in the next process cycle of the audio engine. - pAudioEngine->setNextBpm( pAudioEngine->getBpm() - 0.01*mult ); + pAudioEngine->lock( RIGHT_HERE ); + pAudioEngine->setNextBpm( fBpm - 0.01*mult ); + pAudioEngine->unlock(); // Store it's value in the .h2song file. - pHydrogen->getSong()->setBpm( pAudioEngine->getBpm() - 0.01*mult ); + pHydrogen->getSong()->setBpm( fBpm - 0.01*mult ); } if ( m_nLastBpmChangeCCParameter < cc_param - && pAudioEngine->getBpm() + mult < MAX_BPM ) { + && fBpm + mult < MAX_BPM ) { // Use tempo in the next process cycle of the audio engine. - pAudioEngine->setNextBpm( pAudioEngine->getBpm() + 0.01*mult ); + pAudioEngine->lock( RIGHT_HERE ); + pAudioEngine->setNextBpm( fBpm + 0.01*mult ); + pAudioEngine->unlock(); // Store it's value in the .h2song file. - pHydrogen->getSong()->setBpm( pAudioEngine->getBpm() + 0.01*mult ); + pHydrogen->getSong()->setBpm( fBpm + 0.01*mult ); } m_nLastBpmChangeCCParameter = cc_param; @@ -1038,14 +1025,17 @@ bool MidiActionManager::bpm_increase( std::shared_ptr pAction, Hydrogen* } auto pAudioEngine = pHydrogen->getAudioEngine(); + const float fBpm = pAudioEngine->getTransportPosition()->getBpm(); bool ok; int mult = pAction->getParameter1().toInt(&ok,10); // Use tempo in the next process cycle of the audio engine. - pAudioEngine->setNextBpm( pAudioEngine->getBpm() + 1*mult ); + pAudioEngine->lock( RIGHT_HERE ); + pAudioEngine->setNextBpm( fBpm + 1*mult ); + pAudioEngine->unlock(); // Store it's value in the .h2song file. - pHydrogen->getSong()->setBpm( pAudioEngine->getBpm() + 1*mult ); + pHydrogen->getSong()->setBpm( fBpm + 1*mult ); EventQueue::get_instance()->push_event( EVENT_TEMPO_CHANGED, -1 ); @@ -1060,14 +1050,17 @@ bool MidiActionManager::bpm_decrease( std::shared_ptr pAction, Hydrogen* } auto pAudioEngine = pHydrogen->getAudioEngine(); + const float fBpm = pAudioEngine->getTransportPosition()->getBpm(); bool ok; int mult = pAction->getParameter1().toInt(&ok,10); // Use tempo in the next process cycle of the audio engine. - pAudioEngine->setNextBpm( pAudioEngine->getBpm() - 1*mult ); + pAudioEngine->lock( RIGHT_HERE ); + pAudioEngine->setNextBpm( fBpm - 1*mult ); + pAudioEngine->unlock(); // Store it's value in the .h2song file. - pHydrogen->getSong()->setBpm( pAudioEngine->getBpm() - 1*mult ); + pHydrogen->getSong()->setBpm( fBpm - 1*mult ); EventQueue::get_instance()->push_event( EVENT_TEMPO_CHANGED, -1 ); @@ -1081,7 +1074,8 @@ bool MidiActionManager::next_bar( std::shared_ptr , Hydrogen* pHydrogen return false; } - int nNewColumn = std::max( 0, pHydrogen->getAudioEngine()->getColumn() ) + 1; + int nNewColumn = std::max( 0, pHydrogen->getAudioEngine()-> + getTransportPosition()->getColumn() ) + 1; pHydrogen->getCoreActionController()->locateToColumn( nNewColumn ); return true; @@ -1095,7 +1089,8 @@ bool MidiActionManager::previous_bar( std::shared_ptr , Hydrogen* pHydro return false; } - pHydrogen->getCoreActionController()->locateToColumn( pHydrogen->getAudioEngine()->getColumn() -1 ); + pHydrogen->getCoreActionController()->locateToColumn( + pHydrogen->getAudioEngine()->getTransportPosition()->getColumn() -1 ); return true; } diff --git a/src/core/MidiAction.h b/src/core/MidiAction.h index 454ab0bd8e..c2ccc056d1 100644 --- a/src/core/MidiAction.h +++ b/src/core/MidiAction.h @@ -148,8 +148,8 @@ class MidiActionManager : public H2Core::Object bool effect_level_absolute(std::shared_ptr , H2Core::Hydrogen * ); bool select_next_pattern(std::shared_ptr , H2Core::Hydrogen * ); bool select_only_next_pattern(std::shared_ptr , H2Core::Hydrogen * ); + bool select_only_next_pattern_cc_absolute(std::shared_ptr , H2Core::Hydrogen * ); bool select_next_pattern_cc_absolute(std::shared_ptr , H2Core::Hydrogen * ); - bool select_next_pattern_promptly(std::shared_ptr , H2Core::Hydrogen * ); bool select_next_pattern_relative(std::shared_ptr , H2Core::Hydrogen * ); bool select_and_play_pattern(std::shared_ptr , H2Core::Hydrogen * ); bool pan_relative(std::shared_ptr , H2Core::Hydrogen * ); @@ -173,6 +173,8 @@ class MidiActionManager : public H2Core::Object int m_nLastBpmChangeCCParameter; bool setSong( int nSongNumber, H2Core::Hydrogen* pHydrogen ); + bool nextPatternSelection( int nPatternNumber ); + bool onlyNextPatternSelection( int nPatternNumber ); public: diff --git a/src/core/NsmClient.cpp b/src/core/NsmClient.cpp index 66b125712b..845095e0c6 100644 --- a/src/core/NsmClient.cpp +++ b/src/core/NsmClient.cpp @@ -25,9 +25,14 @@ #include "core/EventQueue.h" #include "core/Hydrogen.h" #include "core/Basics/Drumkit.h" +#include "core/Basics/Instrument.h" +#include "core/Basics/InstrumentComponent.h" +#include "core/Basics/InstrumentLayer.h" +#include "core/Basics/Sample.h" #include "core/Basics/Song.h" #include "core/AudioEngine/AudioEngine.h" #include "core/NsmClient.h" +#include #include #include @@ -45,7 +50,8 @@ NsmClient::NsmClient() : m_pNsm( nullptr ), m_bUnderSessionManagement( false ), m_NsmThread( 0 ), - m_sSessionFolderPath( "" ) + m_sSessionFolderPath( "" ), + m_bIsNewSession( false ) { } @@ -94,7 +100,7 @@ int NsmClient::OpenCallback( const char *name, NsmClient::copyPreferences( name ); - NsmClient::get_instance()->m_sSessionFolderPath = name; + NsmClient::get_instance()->setSessionFolderPath( name ); const QFileInfo sessionPath( name ); const QString sSongPath = QString( "%1/%2%3" ) @@ -119,7 +125,8 @@ int NsmClient::OpenCallback( const char *name, NsmClient::printError( "Preferences instance is not ready yet!" ); return ERR_NOT_NOW; } - + + bool bEmptySongOpened = false; std::shared_ptr pSong = nullptr; if ( songFileInfo.exists() ) { @@ -138,9 +145,20 @@ int NsmClient::OpenCallback( const char *name, return ERR_LAUNCH_FAILED; } pSong->setFilename( sSongPath ); + bEmptySongOpened = true; + + // Mark empty song modified in order to emphasis that an + // initial song save is required to generate the song file and + // link the associated drumkit in the session folder. + pSong->setIsModified( true ); + NsmClient::get_instance()->setIsNewSession( true ); + + // The drumkit of the new song will linked into the session + // folder during the next song save. + pHydrogen->setSessionDrumkitNeedsRelinking( true ); } - if ( ! pController->openSong( pSong ) ) { + if ( ! pController->openSong( pSong, false /*relinking*/ ) ) { NsmClient::printError( "Unable to handle opening action!" ); return ERR_LAUNCH_FAILED; } @@ -153,7 +171,8 @@ int NsmClient::OpenCallback( const char *name, void NsmClient::copyPreferences( const char* name ) { auto pPref = H2Core::Preferences::get_instance(); - const auto pHydrogen = H2Core::Hydrogen::get_instance(); + auto pHydrogen = H2Core::Hydrogen::get_instance(); + auto pCoreActionController = pHydrogen->getCoreActionController(); QFile preferences( H2Core::Filesystem::usr_config_path() ); if ( !preferences.exists() ) { @@ -187,64 +206,61 @@ void NsmClient::copyPreferences( const char* name ) { } } - // If the GUI is active, we have to update it to reflect the - // changes in the preferences. - if ( pHydrogen->getGUIState() == H2Core::Hydrogen::GUIState::ready ) { - H2Core::EventQueue::get_instance()->push_event( H2Core::EVENT_UPDATE_PREFERENCES, 1 ); - } + pCoreActionController->updatePreferences(); NsmClient::printMessage( "Preferences loaded!" ); } -void NsmClient::linkDrumkit( const QString& sName, bool bCheckLinkage ) { +void NsmClient::linkDrumkit( std::shared_ptr pSong ) { const auto pHydrogen = H2Core::Hydrogen::get_instance(); bool bRelinkDrumkit = true; - const QString sDrumkitName = pHydrogen->getLastLoadedDrumkitName(); - const QString sDrumkitAbsPath = pHydrogen->getLastLoadedDrumkitPath(); + const QString sDrumkitName = pSong->getLastLoadedDrumkitName(); + const QString sDrumkitAbsPath = pSong->getLastLoadedDrumkitPath(); + + const QString sSessionFolder = NsmClient::get_instance()->getSessionFolderPath(); + + // Sanity check in order to avoid circular linking. + if ( sDrumkitAbsPath.contains( sSessionFolder, Qt::CaseInsensitive ) ) { + NsmClient::printError( QString( "Last loaded drumkit [%1] with absolute path [%2] is located within the session folder [%3]. Linking skipped." ) + .arg( sDrumkitName ) + .arg( sDrumkitAbsPath ) + .arg( sSessionFolder ) ); + return; + } const QString sLinkedDrumkitPath = QString( "%1/%2" ) - .arg( sName ).arg( "drumkit" ); + .arg( sSessionFolder ).arg( "drumkit" ); const QFileInfo linkedDrumkitPathInfo( sLinkedDrumkitPath ); - if ( bCheckLinkage ) { - // Check whether the linked folder is still valid. - if ( linkedDrumkitPathInfo.isSymLink() || - linkedDrumkitPathInfo.isDir() ) { - - // In case of a symbolic link, the target it is pointing to - // has to be resolved. If drumkit is a real folder, we will - // search for a drumkit.xml therein. - QString sDrumkitXMLPath; - if ( linkedDrumkitPathInfo.isSymLink() ) { - sDrumkitXMLPath = QString( "%1/%2" ) - .arg( linkedDrumkitPathInfo.symLinkTarget() ) - .arg( "drumkit.xml" ); - } else { - sDrumkitXMLPath = QString( "%1/%2" ) - .arg( sLinkedDrumkitPath ).arg( "drumkit.xml" ); - } + // Check whether the linked folder is still valid. + if ( linkedDrumkitPathInfo.isSymLink() || + linkedDrumkitPathInfo.isDir() ) { - const QFileInfo drumkitXMLInfo( sDrumkitXMLPath ); - if ( drumkitXMLInfo.exists() ) { - - const QString sDrumkitNameXML = H2Core::Drumkit::loadNameFrom( sDrumkitXMLPath ); + // In case of a symbolic link, the target it is pointing to + // has to be resolved. If drumkit is a real folder, we will + // search for a drumkit.xml therein. + QString sLinkedDrumkitPath; + if ( linkedDrumkitPathInfo.isSymLink() ) { + sLinkedDrumkitPath = QString( "%1" ) + .arg( linkedDrumkitPathInfo.symLinkTarget() ); + } else { + sLinkedDrumkitPath = QString( "%1" ) + .arg( sLinkedDrumkitPath ); + } + + if ( H2Core::Filesystem::drumkit_valid( sLinkedDrumkitPath ) ) { + const QString sLinkedDrumkitName = H2Core::Drumkit::loadNameFrom( sLinkedDrumkitPath ); - if ( sDrumkitNameXML == sDrumkitName ) { - bRelinkDrumkit = false; - } - else { - NsmClient::printError( QString( "Linked [%1] and loaded [%2] drumkit do not match." ) - .arg( sDrumkitNameXML ) - .arg( sDrumkitName ) ); - } + if ( sLinkedDrumkitName == sDrumkitName ) { + bRelinkDrumkit = false; } - else { - NsmClient::printError( "Symlink does not point to valid drumkit." ); - } } + else { + NsmClient::printError( "Symlink does not point to valid drumkit." ); + } } // The symbolic link either does not exist, is not valid, or does @@ -261,13 +277,14 @@ void NsmClient::linkDrumkit( const QString& sName, bool bCheckLinkage ) { // renamed to 'drumkit' manually again. QDir oldDrumkitFolder( sLinkedDrumkitPath ); if ( ! oldDrumkitFolder.rename( sLinkedDrumkitPath, - QString( "%1/drumkit_old" ).arg( sName ) ) ) { + QString( "%1/drumkit_old" ) + .arg( sSessionFolder ) ) ) { NsmClient::printError( QString( "Unable to rename drumkit folder [%1]." ) .arg( sLinkedDrumkitPath ) ); return; } } else { - if ( !linkedDrumkitFile.remove() ) { + if ( ! linkedDrumkitFile.remove() ) { NsmClient::printError( QString( "Unable to remove symlink to drumkit [%1]." ) .arg( sLinkedDrumkitPath ) ); return; @@ -290,6 +307,127 @@ void NsmClient::linkDrumkit( const QString& sName, bool bCheckLinkage ) { } } } + + // Replace the temporary reference to the "global" drumkit to the + // (freshly) linked/found one in the session folder. + NsmClient::replaceDrumkitPath( pSong, "./drumkit" ); + + pHydrogen->setSessionDrumkitNeedsRelinking( false ); +} + +int NsmClient::dereferenceDrumkit( std::shared_ptr pSong ) { + auto pHydrogen = H2Core::Hydrogen::get_instance(); + + if ( pSong == nullptr ) { + ERRORLOG( "no song set" ); + return -1; + } + + const QString sLastLoadedDrumkitPath = pSong->getLastLoadedDrumkitPath(); + const QString sLastLoadedDrumkitName = pSong->getLastLoadedDrumkitName(); + + if ( ! sLastLoadedDrumkitPath.contains( NsmClient::get_instance()-> + getSessionFolderPath(), + Qt::CaseInsensitive ) ) { + // Regular path. We do not have to alter it. + return 0; + } + + const QFileInfo lastLoadedDrumkitInfo( sLastLoadedDrumkitPath ); + if ( lastLoadedDrumkitInfo.isSymLink() ) { + + QString sDeferencedDrumkit = lastLoadedDrumkitInfo.symLinkTarget(); + + NsmClient::printMessage( QString( "Dereferencing linked drumkit to [%1]" ) + .arg( sDeferencedDrumkit ) ); + NsmClient::replaceDrumkitPath( pSong, sDeferencedDrumkit ); + } + else if ( lastLoadedDrumkitInfo.isDir() ) { + // Drumkit is not linked into the session folder but present + // within a directory (probably because the session was + // transfered from another device to recovered from a + // backup). + // + // This is a little bit tricky as we do not want to install + // the kit into the user's data folder on our own (loss of + // data etc.). If a kit containing the same name is present, + // we will assume the kits do match. That's nowhere near + // perfect but we are dealing with an edge-case of an + // edge-case in here anyway. If it not exists, we will prompt + // a warning dialog (via the GUI) asking the user to install + // it herself. + bool bDrumkitFound = false; + for ( const auto& pDrumkitEntry : + pHydrogen->getSoundLibraryDatabase()->getDrumkitDatabase() ) { + + auto pDrumkit = pDrumkitEntry.second; + if ( pDrumkit != nullptr ) { + if ( pDrumkit->get_name() == sLastLoadedDrumkitName ) { + NsmClient::replaceDrumkitPath( pSong, pDrumkitEntry.first ); + bDrumkitFound = true; + break; + + } + } + } + + if ( ! bDrumkitFound ) { + ERRORLOG( QString( "Drumkit used in session folder [%1] is not present on the current system. It has to be installed first in order to use the exported song" ) + .arg( sLastLoadedDrumkitName ) ); + NsmClient::replaceDrumkitPath( pSong, "" ); + return -2; + } + else { + INFOLOG( QString( "Drumkit used in session folder [%1] was dereferenced to [%2]" ) + .arg( sLastLoadedDrumkitName ) + .arg( pSong->getLastLoadedDrumkitPath() ) ); + } + } + else { + ERRORLOG( "This should not happen" ); + return -1; + } + return 0; +} + +void NsmClient::replaceDrumkitPath( std::shared_ptr pSong, const QString& sDrumkitPath ) { + auto pHydrogen = H2Core::Hydrogen::get_instance(); + + // We are only replacing the paths corresponding to the drumkit + // which is either about to be linked into the session folder or + // the one which is supposed to replace the linked one. + const QString sDrumkitToBeReplaced = pSong->getLastLoadedDrumkitPath(); + + pSong->setLastLoadedDrumkitPath( sDrumkitPath ); + + for ( auto pInstrument : *pSong->getInstrumentList() ) { + if ( pInstrument != nullptr && + pInstrument->get_drumkit_path() == sDrumkitToBeReplaced ) { + + pInstrument->set_drumkit_path( sDrumkitPath ); + + // Use full paths in case the drumkit in sDrumkitPath is + // not located in either the user's or system's drumkit + // folder or just use the filenames (and load the + // relatively) otherwise. + for ( auto pComponent : *pInstrument->get_components() ) { + if ( pComponent != nullptr ) { + for ( auto pInstrumentLayer : *pComponent ) { + if ( pInstrumentLayer != nullptr ) { + auto pSample = pInstrumentLayer->get_sample(); + if ( pSample != nullptr ) { + QString sNewPath = QString( "%1/%2" ) + .arg( sDrumkitPath ) + .arg( pSample->get_filename() ); + + pSample->set_filepath( H2Core::Filesystem::prepare_sample_path( sNewPath ) ); + } + } + } + } + } + } + } } void NsmClient::printError( const QString& msg ) { diff --git a/src/core/NsmClient.h b/src/core/NsmClient.h index d9d3eaca0b..07ed8e64d0 100644 --- a/src/core/NsmClient.h +++ b/src/core/NsmClient.h @@ -122,27 +122,50 @@ class NsmClient : public H2Core::Object * * Sets #bNsmShutdown to true.*/ void shutdown(); + /** - * Responsible for linking and loading of the drumkit samples. - * - * Upon first invocation of this function in a new project, a - * symbolic link to the folder containing the samples of the - * current drumkit will be created in @a sName `drumkit`. In all - * following runs of the session the linked samples will be used - * over the default ones. + * Responsible for linking a drumkit on user or system level into + * the session folder and updating all corresponding references in + * @a pSong. * - * If the session were archived, the symbolic link would had + * If the session was archived, the symbolic link had * been replaced by a folder containing the samples. In such an * occasion the samples located in the folder will be loaded. This * ensure portability of Hydrogen within a session regardless of * the local drumkits present in the user's home. * - * \param sName Absolute path to the session folder. - * \param bCheckLinkage Whether or not the linked drumkit should - * be verified to correspond to @a sName. If set to none, the - * drumkit will always be relinked. + * \param pSong @H2Core::Song containing references to global + * drumkit. + */ + static void linkDrumkit( std::shared_ptr pSong ); + + /** + * Replaces a path in Song::m_sLastLoadedDrumkitPath pointing to + * the session folder with one pointing to the corresponding kit + * in the data folder. + * + * In case the session drumkit does not exist at neither system + * nor user level, Song::m_sLastLoadedDrumkitPath will be replaced + * by an empty string. + * + * \return 0 : success. -1 : general error, -2 : drumkit is + * present as directory in session folder. But a drumkit holding + * the same name couldn't be found on the system. + */ + static int dereferenceDrumkit( std::shared_ptr pSong ); + + /** + * Replaces @H2Core::Song::m_sLastLoadedDrumkitPath as well as all + * @H2Core::Instrument::__drumkit_path bearing the same value in + * @a pSong with @a sDrumkitPath. + * + * This is required when telling a #H2Core::Song to use the + * drumkit linked/found in the session folder instead of its + * counterpart in the user's or system's drumkit data folder or + * the over way around when exporting the song of the session. */ - static void linkDrumkit( const QString& sName, bool bCheckLinkage ); + static void replaceDrumkitPath( std::shared_ptr pSong, const QString& sDrumkitPath ); + /** Custom function to print a colored error message. * * Since the OpenCallback() and SaveCallback() functions will be @@ -165,13 +188,11 @@ class NsmClient : public H2Core::Object /** \return m_bUnderSessionManagement*/ bool getUnderSessionManagement() const; - /** Folder all the content of the current session will be - * stored in. - * - * Set at the beginning of each session in - * NsmClient::OpenCallback(). - */ - QString m_sSessionFolderPath; + QString getSessionFolderPath() const; + void setSessionFolderPath( const QString& sPath ); + + bool getIsNewSession() const; + void setIsNewSession( bool bNew ); private: /**Private constructor to allow construction only via @@ -191,6 +212,21 @@ class NsmClient : public H2Core::Object * createInitialClient() has to be called first. */ bool m_bUnderSessionManagement; + + /** Folder all the content of the current session will be + * stored in. + * + * Set at the beginning of each session in + * NsmClient::OpenCallback(). + */ + QString m_sSessionFolderPath; + + /** + * Indicates whether a song file was already found in the session + * folder and successfully loaded or the session was initialized + * with an empty song. + */ + bool m_bIsNewSession; /** * Callback function for the NSM server to tell Hydrogen to open a @@ -290,6 +326,19 @@ inline bool NsmClient::getUnderSessionManagement() const { return m_bUnderSessionManagement; } +inline QString NsmClient::getSessionFolderPath() const { + return m_sSessionFolderPath; +} +inline void NsmClient::setSessionFolderPath( const QString& sPath ) { + m_sSessionFolderPath = sPath; +} + +inline bool NsmClient::getIsNewSession() const { + return m_bIsNewSession; +} +inline void NsmClient::setIsNewSession( bool bNew ) { + m_bIsNewSession = bNew; +} #endif /* H2CORE_HAVE_OSC */ #endif // NSM_CLIENT_H diff --git a/src/core/Object.cpp b/src/core/Object.cpp index ce86f483b7..22a467980a 100644 --- a/src/core/Object.cpp +++ b/src/core/Object.cpp @@ -202,7 +202,7 @@ QString Base::base_clock( const QString& sMsg ) // Clock is invoked for the first time. sResult = "Start clocking"; } else { - sResult = QString( "elapsed [%1]s" ) + sResult = QString( "elapsed [%1]ms" ) .arg( ( now.tv_sec - __last_clock.tv_sec ) * 1000.0 + ( now.tv_usec - __last_clock.tv_usec ) / 1000.0 ); } diff --git a/src/core/OscServer.cpp b/src/core/OscServer.cpp index 527ab58079..7a5d7f6726 100644 --- a/src/core/OscServer.cpp +++ b/src/core/OscServer.cpp @@ -552,12 +552,16 @@ void OscServer::BPM_Handler(lo_arg **argv,int i) { INFOLOG( "processing message" ); auto pHydrogen = H2Core::Hydrogen::get_instance(); + auto pAudioEngine = pHydrogen->getAudioEngine(); float fNewBpm = argv[0]->f; fNewBpm = std::clamp( fNewBpm, static_cast(MIN_BPM), static_cast(MAX_BPM) ); - pHydrogen->getAudioEngine()->setNextBpm( fNewBpm ); + pAudioEngine->lock( RIGHT_HERE ); + pAudioEngine->setNextBpm( fNewBpm ); + pAudioEngine->unlock(); + pHydrogen->getSong()->setBpm( fNewBpm ); pHydrogen->setIsModified( true ); diff --git a/src/core/Sampler/Sampler.cpp b/src/core/Sampler/Sampler.cpp index 702090d6a8..cf7f9d9049 100644 --- a/src/core/Sampler/Sampler.cpp +++ b/src/core/Sampler/Sampler.cpp @@ -29,6 +29,7 @@ #include #include +#include #include #include #include @@ -107,9 +108,16 @@ Sampler::~Sampler() */ float const Sampler::K_NORM_DEFAULT = 1.33333333333333; -void Sampler::process( uint32_t nFrames, std::shared_ptr pSong ) +void Sampler::process( uint32_t nFrames ) { - AudioOutput* pAudioOutpout = Hydrogen::get_instance()->getAudioOutput(); + auto pHydrogen = Hydrogen::get_instance(); + auto pSong = pHydrogen->getSong(); + if ( pSong == nullptr ) { + ERRORLOG( "no song" ); + return; + } + + AudioOutput* pAudioOutpout = pHydrogen->getAudioOutput(); assert( pAudioOutpout ); memset( m_pMainOut_L, 0, nFrames * sizeof( float ) ); @@ -118,52 +126,62 @@ void Sampler::process( uint32_t nFrames, std::shared_ptr pSong ) // Track output queues are zeroed by // audioEngine_process_clearAudioBuffers() + for ( auto& pComponent : *pSong->getComponents() ) { + pComponent->reset_outs(nFrames); + } + // Max notes limit - int m_nMaxNotes = Preferences::get_instance()->m_nMaxNotes; - while ( ( int )m_playingNotesQueue.size() > m_nMaxNotes ) { + int nMaxNotes = Preferences::get_instance()->m_nMaxNotes; + while ( ( int )m_playingNotesQueue.size() > nMaxNotes ) { Note * pOldNote = m_playingNotesQueue[ 0 ]; m_playingNotesQueue.erase( m_playingNotesQueue.begin() ); pOldNote->get_instrument()->dequeue(); + WARNINGLOG( QString( "Number of playing notes [%1] exceeds maximum [%2]. Dropping note [%3]" ) + .arg( m_playingNotesQueue.size() ).arg( nMaxNotes ) + .arg( pOldNote->toQString() ) ); delete pOldNote; // FIXME: send note-off instead of removing the note from the list? } - for ( auto& pComponent : *pSong->getComponents() ) { - pComponent->reset_outs(nFrames); - } - - // eseguo tutte le note nella lista di note in esecuzione + // Render next `nFrames` audio frames of all playing notes. unsigned i = 0; Note* pNote; while ( i < m_playingNotesQueue.size() ) { - pNote = m_playingNotesQueue[ i ]; // recupero una nuova nota - if ( renderNote( pNote, nFrames, pSong ) ) { // la nota e' finita + pNote = m_playingNotesQueue[ i ]; + if ( renderNote( pNote, nFrames ) ) { + // End of note was reached during rendering. m_playingNotesQueue.erase( m_playingNotesQueue.begin() + i ); pNote->get_instrument()->dequeue(); m_queuedNoteOffs.push_back( pNote ); } else { - ++i; // carico la prox nota + // As finished notes are poped above + ++i; } } - //Queue midi note off messages for notes that have a length specified for them - while ( !m_queuedNoteOffs.empty() ) { - pNote = m_queuedNoteOffs[0]; - MidiOutput* pMidiOut = Hydrogen::get_instance()->getMidiOutput(); + if ( m_queuedNoteOffs.size() > 0 ) { + MidiOutput* pMidiOut = pHydrogen->getMidiOutput(); + if ( pMidiOut != nullptr ) { + //Queue midi note off messages for notes that have a length specified for them + while ( ! m_queuedNoteOffs.empty() ) { + pNote = m_queuedNoteOffs[0]; - if( pMidiOut != nullptr && !pNote->get_instrument()->is_muted() ){ - pMidiOut->handleQueueNoteOff( pNote->get_instrument()->get_midi_out_channel(), - pNote->get_midi_key(), - pNote->get_midi_velocity() ); - } + if ( ! pNote->get_instrument()->is_muted() ){ + pMidiOut->handleQueueNoteOff( + pNote->get_instrument()->get_midi_out_channel(), + pNote->get_midi_key(), + pNote->get_midi_velocity() ); + } - m_queuedNoteOffs.erase( m_queuedNoteOffs.begin() ); + m_queuedNoteOffs.erase( m_queuedNoteOffs.begin() ); - if( pNote != nullptr ){ - delete pNote; - } + if ( pNote != nullptr ){ + delete pNote; + } - pNote = nullptr; - }//while + pNote = nullptr; + } + } + } processPlaybackTrack(nFrames); } @@ -183,25 +201,26 @@ void Sampler::noteOn(Note *pNote ) int nMuteGrp = pInstr->get_mute_group(); if ( nMuteGrp != -1 ) { // remove all notes using the same mute group - for ( const auto& pNote: m_playingNotesQueue ) { // delete older note - if ( ( pNote->get_instrument() != pInstr ) && ( pNote->get_instrument()->get_mute_group() == nMuteGrp ) ) { - pNote->get_adsr()->release(); + for ( const auto& pOtherNote: m_playingNotesQueue ) { // delete older note + if ( ( pOtherNote->get_instrument() != pInstr ) && + ( pOtherNote->get_instrument()->get_mute_group() == nMuteGrp ) ) { + pOtherNote->get_adsr()->release(); } } } //note off notes - if( pNote->get_note_off() ){ - for ( const auto& pNote: m_playingNotesQueue ) { - if ( ( pNote->get_instrument() == pInstr ) ) { + if ( pNote->get_note_off() ){ + for ( const auto& pOtherNote: m_playingNotesQueue ) { + if ( ( pOtherNote->get_instrument() == pInstr ) ) { //ERRORLOG("note_off"); - pNote->get_adsr()->release(); + pOtherNote->get_adsr()->release(); } } } pInstr->enqueue(); - if( !pNote->get_note_off() ){ + if ( ! pNote->get_note_off() ){ m_playingNotesQueue.push_back( pNote ); } } @@ -418,18 +437,20 @@ void Sampler::handleSongSizeChange() { return; } - auto pAudioEngine = Hydrogen::get_instance()->getAudioEngine(); + const long nTickOffset = + static_cast(std::floor(Hydrogen::get_instance()->getAudioEngine()-> + getTransportPosition()->getTickOffsetSongSize())); for ( auto nnote : m_playingNotesQueue ) { - // DEBUGLOG( QString( "new pos: %1, old note: %2" ) - // .arg( std::max( nnote->get_position() + - // static_cast(std::floor(pAudioEngine->getTickOffset())), + // DEBUGLOG( QString( "pos: %1 -> %2, nTickOffset: %3, note: %4" ) + // .arg( nnote->get_position() ) + // .arg( std::max( nnote->get_position() + nTickOffset, // static_cast(0) ) ) + // .arg( nTickOffset ) // .arg( nnote->toQString( "", true ) ) ); - nnote->set_position( std::max( nnote->get_position() + - static_cast(std::floor(pAudioEngine->getTickOffset())), + nnote->set_position( std::max( nnote->get_position() + nTickOffset, static_cast(0) ) ); nnote->computeNoteStart(); @@ -441,29 +462,31 @@ void Sampler::handleSongSizeChange() { //------------------------------------------------------------------ -/// Render a note -/// Return false: the note is not ended -/// Return true: the note is ended -bool Sampler::renderNote( Note* pNote, unsigned nBufferSize, std::shared_ptr pSong ) +bool Sampler::renderNote( Note* pNote, unsigned nBufferSize ) { - assert( pSong ); + auto pHydrogen = Hydrogen::get_instance(); + auto pSong = pHydrogen->getSong(); + if ( pSong == nullptr ) { + ERRORLOG( "no song" ); + return true; + } auto pInstr = pNote->get_instrument(); if ( pInstr == nullptr ) { ERRORLOG( "NULL instrument" ); - return 1; + return true; } - long long nFrames; - Hydrogen* pHydrogen = Hydrogen::get_instance(); + long long nFrame; auto pAudioDriver = pHydrogen->getAudioOutput(); auto pAudioEngine = pHydrogen->getAudioEngine(); if ( pAudioEngine->getState() == AudioEngine::State::Playing || pAudioEngine->getState() == AudioEngine::State::Testing ) { - nFrames = pAudioEngine->getFrames(); + nFrame = pAudioEngine->getTransportPosition()->getFrame(); } else { - // use this to support realtime events when not playing - nFrames = pAudioEngine->getRealtimeFrames(); + // use this to support realtime events when transport is not + // rolling. + nFrame = pAudioEngine->getRealtimeFrame(); } // Only if the Sampler has not started rendering the note yet we @@ -474,27 +497,26 @@ bool Sampler::renderNote( Note* pNote, unsigned nBufferSize, std::shared_ptrisPartiallyRendered() ) { long long nNoteStartInFrames = pNote->getNoteStart(); - // DEBUGLOG(QString( "framepos: %1, note pos: %2, ticksize: %3, curr tick: %4, curr frame: %5, nNoteStartInFrames: %6 ") - // .arg( nFrames).arg( pNote->get_position() ).arg( pAudioEngine->getTickSize() ) - // .arg( pAudioEngine->getTick() ).arg( pAudioEngine->getFrames() ) + // DEBUGLOG(QString( "nFrame: %1, note pos: %2, pAudioEngine->getTransportPosition()->getTickSize(): %3, pAudioEngine->getTransportPosition()->getTick(): %4, pAudioEngine->getTransportPosition()->getFrame(): %5, nNoteStartInFrames: %6 ") + // .arg( nFrame ).arg( pNote->get_position() ) + // .arg( pAudioEngine->getTransportPosition()->getTickSize() ) + // .arg( pAudioEngine->getTransportPosition()->getTick() ) + // .arg( pAudioEngine->getTransportPosition()->getFrame() ) // .arg( nNoteStartInFrames ) // .append( pNote->toQString( "", true ) ) ); - if ( nNoteStartInFrames > nFrames ) { // scrivo silenzio prima dell'inizio della nota - nInitialSilence = nNoteStartInFrames - nFrames; + if ( nNoteStartInFrames > nFrame ) { + // The note doesn't start right at the beginning of the + // buffer rendered in this cycle. + nInitialSilence = nNoteStartInFrames - nFrame; if ( nBufferSize < nInitialSilence ) { + // this note is not valid. it's in the future...let's skip it.... + ERRORLOG( QString( "Note pos in the future?? nFrame: %1, note start: %2, nInitialSilence: %3, nBufferSize: %4" ) + .arg( nFrame ).arg( pNote->getNoteStart() ) + .arg( nInitialSilence ).arg( nBufferSize ) ); - if ( ! pNote->isPartiallyRendered() && - pNote->getNoteStart() > nFrames + nBufferSize ) { - // this note is not valid. it's in the future...let's skip it.... - ERRORLOG( QString( "Note pos in the future?? Current frames: %1, note frame pos: %2" ).arg( nFrames ).arg( pNote->getNoteStart() ) ); - - return true; - } - // delay note execution - // DEBUGLOG("delayed"); - return false; + return true; } } } @@ -524,36 +546,51 @@ bool Sampler::renderNote( Note* pNote, unsigned nBufferSize, std::shared_ptrhasJackAudioDriver() && + Preferences::get_instance()->m_JackTrackOutputMode == + Preferences::JackTrackOutputMode::preFader ) { + fNotePan_L = panLaw( pNote->getPan(), pSong ); + fNotePan_R = panLaw( -1 * pNote->getPan(), pSong ); + } //--------------------------------------------------------- - auto components = pInstr->get_components(); - bool nReturnValues[ components->size() ]; - for(int i = 0; i < components->size(); i++){ + auto pComponents = pInstr->get_components(); + bool nReturnValues[ pComponents->size() ]; + + for( int i = 0; i < pComponents->size(); i++ ){ nReturnValues[i] = false; } int nReturnValueIndex = 0; int nAlreadySelectedLayer = -1; + bool bComponentFound = false; - for ( const auto& pCompo : *components ) { - nReturnValues[nReturnValueIndex] = false; + for ( const auto& pCompo : *pComponents ) { std::shared_ptr pMainCompo = nullptr; - if( pNote->get_specific_compo_id() != -1 && pNote->get_specific_compo_id() != pCompo->get_drumkit_componentID() ) { + if ( pNote->get_specific_compo_id() != -1 && + pNote->get_specific_compo_id() != pCompo->get_drumkit_componentID() ) { nReturnValueIndex++; continue; } + bComponentFound = true; - if( pInstr->is_preview_instrument() - || pInstr->is_metronome_instrument()){ - pMainCompo = pHydrogen->getSong()->getComponents()->front(); + if ( pInstr->is_preview_instrument() || + pInstr->is_metronome_instrument() ){ + pMainCompo = pSong->getComponents()->front(); } else { int nComponentID = pCompo->get_drumkit_componentID(); if ( nComponentID >= 0 ) { - pMainCompo = pHydrogen->getSong()->getComponent( nComponentID ); + pMainCompo = pSong->getComponent( nComponentID ); } else { /* Invalid component found. This is possible on loading older or broken song files. */ - pMainCompo = pHydrogen->getSong()->getComponents()->front(); + pMainCompo = pSong->getComponents()->front(); } } @@ -588,20 +625,24 @@ bool Sampler::renderNote( Note* pNote, unsigned nBufferSize, std::shared_ptrget_pitch(); if ( pSelectedLayer->SamplePosition >= pSample->get_frames() ) { - WARNINGLOG( "sample position out of bounds. The layer has been resized during note play?" ); + WARNINGLOG( QString( "sample position [%1] out of bounds [0,%2]. The layer has been resized during note play?" ) + .arg( pSelectedLayer->SamplePosition ) + .arg( pSample->get_frames() ) ); nReturnValues[nReturnValueIndex] = true; nReturnValueIndex++; continue; } - float cost_L = 1.0f; - float cost_R = 1.0f; - float cost_track_L = 1.0f; - float cost_track_R = 1.0f; + float fCost_L = 1.0f; + float fCost_R = 1.0f; + float fCostTrack_L = 1.0f; + float fCostTrack_R = 1.0f; - bool isMutedForExport = (pHydrogen->getIsExportSessionActive() && !pInstr->is_currently_exported()); + bool bIsMutedForExport = ( pHydrogen->getIsExportSessionActive() && + ! pInstr->is_currently_exported() ); bool bAnyInstrumentIsSoloed = pSong->getInstrumentList()->isAnyInstrumentSoloed(); - bool isMutedBecauseOfSolo = (bAnyInstrumentIsSoloed && !pInstr->is_soloed()); + bool bIsMutedBecauseOfSolo = ( bAnyInstrumentIsSoloed && + ! pInstr->is_soloed() ); /* * Is instrument muted? @@ -612,53 +653,50 @@ bool Sampler::renderNote( Note* pNote, unsigned nBufferSize, std::shared_ptris_muted() || pSong->getIsMuted() || pMainCompo->is_muted() || isMutedBecauseOfSolo) { - cost_L = 0.0; - cost_R = 0.0; - if ( Preferences::get_instance()->m_JackTrackOutputMode == Preferences::JackTrackOutputMode::postFader ) { - cost_track_L = 0.0; - cost_track_R = 0.0; + if ( bIsMutedForExport || pInstr->is_muted() || pSong->getIsMuted() || + pMainCompo->is_muted() || bIsMutedBecauseOfSolo) { + fCost_L = 0.0; + fCost_R = 0.0; + if ( Preferences::get_instance()->m_JackTrackOutputMode == + Preferences::JackTrackOutputMode::postFader ) { + fCostTrack_L = 0.0; + fCostTrack_R = 0.0; } - } else { // Precompute some values... + } else { + float fMonoGain = 1.0; if ( pInstr->get_apply_velocity() ) { - cost_L = cost_L * pNote->get_velocity(); // note velocity - cost_R = cost_R * pNote->get_velocity(); // note velocity + fMonoGain *= pNote->get_velocity(); // note velocity } - - cost_L *= fPan_L; // pan - cost_L = cost_L * fLayerGain; // layer gain - cost_L = cost_L * pInstr->get_gain(); // instrument gain - - cost_L = cost_L * pCompo->get_gain(); // Component gain - cost_L = cost_L * pMainCompo->get_volume(); // Component volument - - cost_L = cost_L * pInstr->get_volume(); // instrument volume - if ( Preferences::get_instance()->m_JackTrackOutputMode == Preferences::JackTrackOutputMode::postFader ) { - cost_track_L = cost_L * 2; + fMonoGain *= fLayerGain; // layer gain + fMonoGain *= pInstr->get_gain(); // instrument gain + fMonoGain *= pCompo->get_gain(); // Component gain + fMonoGain *= pMainCompo->get_volume(); // Component volument + fMonoGain *= pInstr->get_volume(); // instrument volume + fMonoGain *= pSong->getVolume(); // song volume + + fCost_L = fMonoGain * fPan_L; // pan + fCost_R = fMonoGain * fPan_R; // pan + if ( Preferences::get_instance()->m_JackTrackOutputMode == + Preferences::JackTrackOutputMode::postFader ) { + fCostTrack_R = fCost_R * 2; + fCostTrack_L = fCost_L * 2; } - cost_L = cost_L * pSong->getVolume(); // song volume - - cost_R *= fPan_R; // pan - cost_R = cost_R * fLayerGain; // layer gain - cost_R = cost_R * pInstr->get_gain(); // instrument gain - - cost_R = cost_R * pCompo->get_gain(); // Component gain - cost_R = cost_R * pMainCompo->get_volume(); // Component volument - - cost_R = cost_R * pInstr->get_volume(); // instrument volume - if ( Preferences::get_instance()->m_JackTrackOutputMode == Preferences::JackTrackOutputMode::postFader ) { - cost_track_R = cost_R * 2; - } - cost_R = cost_R * pSong->getVolume(); // song pan } // direct track outputs only use velocity - if ( Preferences::get_instance()->m_JackTrackOutputMode == Preferences::JackTrackOutputMode::preFader ) { - cost_track_L = cost_track_L * pNote->get_velocity(); - cost_track_L = cost_track_L * fLayerGain; - cost_track_R = cost_track_L; + if ( Preferences::get_instance()->m_JackTrackOutputMode == + Preferences::JackTrackOutputMode::preFader ) { + if ( pInstr->get_apply_velocity() ) { + fCostTrack_L *= pNote->get_velocity(); + } + fCostTrack_L *= fLayerGain; + + fCostTrack_R = fCostTrack_L; + + fCostTrack_L *= fNotePan_L; + fCostTrack_R *= fNotePan_R; } // Se non devo fare resample (drumkit) posso evitare di utilizzare i float e gestire il tutto in @@ -668,24 +706,35 @@ bool Sampler::renderNote( Note* pNote, unsigned nBufferSize, std::shared_ptrget_total_pitch() + fLayerPitch; - //_INFOLOG( "total pitch: " + to_string( fTotalPitch ) ); - if ( (int) pSelectedLayer->SamplePosition == 0 && !pInstr->is_muted() ) { - if ( Hydrogen::get_instance()->getMidiOutput() != nullptr ){ - Hydrogen::get_instance()->getMidiOutput()->handleQueueNote( pNote ); + // Once the Sampler does start rendering a note we also push + // it to all connected MIDI devices. + if ( (int) pSelectedLayer->SamplePosition == 0 && ! pInstr->is_muted() ) { + if ( pHydrogen->getMidiOutput() != nullptr ){ + pHydrogen->getMidiOutput()->handleQueueNote( pNote ); } } + // Actual rendering. if ( fTotalPitch == 0.0 && pSample->get_sample_rate() == pAudioDriver->getSampleRate() ) { // NO RESAMPLE - nReturnValues[nReturnValueIndex] = renderNoteNoResample( pSample, pNote, pSelectedLayer, pCompo, pMainCompo, nBufferSize, nInitialSilence, cost_L, cost_R, cost_track_L, cost_track_R, pSong ); + nReturnValues[nReturnValueIndex] = renderNoteNoResample( pSample, pNote, pSelectedLayer, pCompo, pMainCompo, nBufferSize, nInitialSilence, fCost_L, fCost_R, fCostTrack_L, fCostTrack_R ); } else { // RESAMPLE - nReturnValues[nReturnValueIndex] = renderNoteResample( pSample, pNote, pSelectedLayer, pCompo, pMainCompo, nBufferSize, nInitialSilence, cost_L, cost_R, cost_track_L, cost_track_R, fLayerPitch, pSong ); + nReturnValues[nReturnValueIndex] = renderNoteResample( pSample, pNote, pSelectedLayer, pCompo, pMainCompo, nBufferSize, nInitialSilence, fCost_L, fCost_R, fCostTrack_L, fCostTrack_R, fLayerPitch ); } nReturnValueIndex++; } - for ( unsigned i = 0 ; i < components->size() ; i++ ) { - if ( !nReturnValues[i] ) { + + // Sanity check whether the note could be rendered. + if ( ! bComponentFound ) { + ERRORLOG( QString( "Specific note component [%1] not found in instrument associated with note: [%2]" ) + .arg( pNote->get_specific_compo_id() ) + .arg( pNote->toQString() ) ); + return true; + } + + for ( const auto& bReturnValue : nReturnValues ) { + if ( ! bReturnValue ) { return false; } } @@ -736,10 +785,13 @@ bool Sampler::processPlaybackTrack(int nBufferSize) int nAvail_bytes = 0; int nInitialBufferPos = 0; - if(pSample->get_sample_rate() == pAudioDriver->getSampleRate()){ - //No resampling - m_nPlayBackSamplePosition = pAudioEngine->getFrames() - - pAudioEngine->getFrameOffset(); + const long long nFrame = pAudioEngine->getTransportPosition()->getFrame(); + const long long nFrameOffset = + pAudioEngine->getTransportPosition()->getFrameOffsetTempo(); + + if ( pSample->get_sample_rate() == pAudioDriver->getSampleRate() ) { + // No resampling + m_nPlayBackSamplePosition = nFrame - nFrameOffset; nAvail_bytes = pSample->get_frames() - m_nPlayBackSamplePosition; @@ -750,14 +802,14 @@ bool Sampler::processPlaybackTrack(int nBufferSize) int nInitialSamplePos = ( int ) m_nPlayBackSamplePosition; int nSamplePos = nInitialSamplePos; - int nTimes = nInitialBufferPos + nAvail_bytes; + int nFinalBufferPos = nInitialBufferPos + nAvail_bytes; - if(m_nPlayBackSamplePosition > pSample->get_frames()){ + if ( m_nPlayBackSamplePosition > pSample->get_frames() ) { //playback track has ended.. return true; } - for ( int nBufferPos = nInitialBufferPos; nBufferPos < nTimes; ++nBufferPos ) { + for ( int nBufferPos = nInitialBufferPos; nBufferPos < nFinalBufferPos; ++nBufferPos ) { fVal_L = pSample_data_L[ nSamplePos ]; fVal_R = pSample_data_R[ nSamplePos ]; @@ -787,11 +839,11 @@ bool Sampler::processPlaybackTrack(int nBufferSize) fStep *= ( float )pSample->get_sample_rate() / pAudioDriver->getSampleRate(); // Adjust for audio driver sample rate - if( pAudioEngine->getFrames() == 0){ + if ( nFrame == 0 ){ fSamplePos = 0; } else { - fSamplePos = ( ( ( pAudioEngine->getFrames() - pAudioEngine->getFrameOffset() ) - /nBufferSize) * (nBufferSize * fStep)); + fSamplePos = ( nFrame - nFrameOffset ) / nBufferSize * + nBufferSize * fStep; } nAvail_bytes = ( int )( ( float )( pSample->get_frames() - fSamplePos ) / fStep ); @@ -800,9 +852,9 @@ bool Sampler::processPlaybackTrack(int nBufferSize) nAvail_bytes = nBufferSize; } - int nTimes = nInitialBufferPos + nAvail_bytes; + int nFinalBufferPos = nInitialBufferPos + nAvail_bytes; - for ( int nBufferPos = nInitialBufferPos; nBufferPos < nTimes; ++nBufferPos ) { + for ( int nBufferPos = nInitialBufferPos; nBufferPos < nFinalBufferPos; ++nBufferPos ) { int nSamplePos = ( int ) fSamplePos; double fDiff = fSamplePos - nSamplePos; if ( ( nSamplePos + 1 ) >= nSampleFrames ) { @@ -876,49 +928,62 @@ bool Sampler::renderNoteNoResample( std::shared_ptr pDrumCompo, int nBufferSize, int nInitialSilence, - float cost_L, - float cost_R, - float cost_track_L, - float cost_track_R, - std::shared_ptr pSong + float fCost_L, + float fCost_R, + float fCostTrack_L, + float fCostTrack_R ) { - auto pAudioDriver = Hydrogen::get_instance()->getAudioOutput(); - auto pAudioEngine = Hydrogen::get_instance()->getAudioEngine(); + auto pHydrogen = Hydrogen::get_instance(); + auto pAudioDriver = pHydrogen->getAudioOutput(); + auto pSong = pHydrogen->getSong(); auto pInstrument = pNote->get_instrument(); - bool retValue = true; // the note is ended + bool bRetValue = true; // the note is ended int nNoteLength = -1; if ( pNote->get_length() != -1 ) { - - int nEffectiveDelay = 0; - if ( pNote->get_humanize_delay() < 0 ) { - nEffectiveDelay = pNote->get_humanize_delay(); - } + // The user set a custom duration of the note in the + // PatternEditor. This will be used instead of the full sample + // length. + + // Delay is already introduced into the note start used below + // in Note::computeNoteStart. We need to account for it in + // here to in order to get the length of the note right. + const int nDelay = std::clamp( + pNote->get_humanize_delay(), -1 * AudioEngine::nMaxTimeHumanize, + AudioEngine::nMaxTimeHumanize ); double fTickMismatch; - nNoteLength = - pAudioEngine->computeFrameFromTick( pNote->get_position() + - nEffectiveDelay + - pNote->get_length(), &fTickMismatch ) - - pNote->getNoteStart(); + nNoteLength = TransportPosition::computeFrameFromTick( + pNote->get_position() + nDelay + pNote->get_length(), + &fTickMismatch ) - pNote->getNoteStart(); } - - int nAvail_bytes = pSample->get_frames() - ( int )pSelectedLayerInfo->SamplePosition; // verifico il numero di frame disponibili ancora da eseguire - - if ( nAvail_bytes > nBufferSize - nInitialSilence ) { // il sample e' piu' grande del buffersize - // imposto il numero dei bytes disponibili uguale al buffersize + + // The number of frames of the sample left to process. + int nRemainingFrames = pSample->get_frames() - ( int )pSelectedLayerInfo->SamplePosition; + + int nAvail_bytes; + if ( nRemainingFrames > nBufferSize - nInitialSilence ) { + // It The number of frames of the sample left to process + // exceeds what's available in the current process cycle of + // the Sampler. Clip it. nAvail_bytes = nBufferSize - nInitialSilence; - retValue = false; // the note is not ended yet - } else if ( pInstrument->is_filter_active() && pNote->filter_sustain() ) { + // the note is not ended yet + bRetValue = false; + } + else if ( pInstrument->is_filter_active() && pNote->filter_sustain() ) { // If filter is causing note to ring, process more samples. nAvail_bytes = nBufferSize - nInitialSilence; } + else { + nAvail_bytes = nRemainingFrames; + } int nInitialBufferPos = nInitialSilence; int nInitialSamplePos = ( int )pSelectedLayerInfo->SamplePosition; int nSamplePos = nInitialSamplePos; - int nTimes = nInitialBufferPos + nAvail_bytes; + // Either end of buffer or end of sample prior within the buffer. + int nFinalBufferPos = nInitialBufferPos + nAvail_bytes; auto pSample_data_L = pSample->get_data_l(); auto pSample_data_R = pSample->get_data_r(); @@ -932,8 +997,8 @@ bool Sampler::renderNoteNoResample( float fVal_R; #ifdef H2CORE_HAVE_JACK - float * pTrackOutL = nullptr; - float * pTrackOutR = nullptr; + float* pTrackOutL = nullptr; + float* pTrackOutR = nullptr; if ( Preferences::get_instance()->m_bJackTrackOuts ) { auto pJackAudioDriver = dynamic_cast( pAudioDriver ); @@ -946,15 +1011,15 @@ bool Sampler::renderNoteNoResample( float buffer_L[ MAX_BUFFER_SIZE ]; float buffer_R[ MAX_BUFFER_SIZE ]; - int nNoteEnd; + int nNoteEnd = nInitialBufferPos + 1; if ( nNoteLength == -1) { - nNoteEnd = pSelectedLayerInfo->SamplePosition + nTimes + 1; + nNoteEnd += pSelectedLayerInfo->SamplePosition + nFinalBufferPos; } else { - nNoteEnd = nNoteLength - pSelectedLayerInfo->SamplePosition; + nNoteEnd += nNoteLength - (int)pSelectedLayerInfo->SamplePosition; } - int nSampleFrames = std::min( nTimes, + int nSampleFrames = std::min( nFinalBufferPos, ( nInitialSilence + pSample->get_frames() - ( int )pSelectedLayerInfo->SamplePosition ) ); for ( int nBufferPos = nInitialBufferPos; nBufferPos < nSampleFrames; ++nBufferPos ) { @@ -962,18 +1027,18 @@ bool Sampler::renderNoteNoResample( buffer_R[ nBufferPos ] = pSample_data_R[ nSamplePos ]; nSamplePos++; } - for ( int nBufferPos = nSampleFrames; nBufferPos < nTimes; ++nBufferPos ) { + for ( int nBufferPos = nSampleFrames; nBufferPos < nFinalBufferPos; ++nBufferPos ) { buffer_L[ nBufferPos ] = buffer_R[ nBufferPos ] = 0.0; } - - if ( pADSR->applyADSR( buffer_L, buffer_R, nTimes, nNoteEnd, 1 ) ) { - retValue = true; + if ( pADSR->applyADSR( buffer_L, buffer_R, nFinalBufferPos, nNoteEnd, 1 ) ) { + bRetValue = true; } + bool bFilterIsActive = pInstrument->is_filter_active(); // Low pass resonant filter if ( bFilterIsActive ) { - for ( int nBufferPos = nInitialBufferPos; nBufferPos < nTimes; ++nBufferPos ) { + for ( int nBufferPos = nInitialBufferPos; nBufferPos < nFinalBufferPos; ++nBufferPos ) { fVal_L = buffer_L[ nBufferPos ]; fVal_R = buffer_R[ nBufferPos ]; @@ -986,23 +1051,23 @@ bool Sampler::renderNoteNoResample( } } - for ( int nBufferPos = nInitialBufferPos; nBufferPos < nTimes; ++nBufferPos ) { + for ( int nBufferPos = nInitialBufferPos; nBufferPos < nFinalBufferPos; ++nBufferPos ) { fVal_L = buffer_L[ nBufferPos ]; fVal_R = buffer_R[ nBufferPos ]; #ifdef H2CORE_HAVE_JACK - if( pTrackOutL ) { - pTrackOutL[nBufferPos] += fVal_L * cost_track_L; + if ( pTrackOutL != nullptr ) { + pTrackOutL[nBufferPos] += fVal_L * fCostTrack_L; } - if( pTrackOutR ) { - pTrackOutR[nBufferPos] += fVal_R * cost_track_R; + if ( pTrackOutR != nullptr ) { + pTrackOutR[nBufferPos] += fVal_R * fCostTrack_R; } #endif - fVal_L = fVal_L * cost_L; - fVal_R = fVal_R * cost_R; + fVal_L *= fCost_L; + fVal_R *= fCost_R; // update instr peak if ( fVal_L > fInstrPeak_L ) { @@ -1021,7 +1086,7 @@ bool Sampler::renderNoteNoResample( } if ( pInstrument->is_filter_active() && pNote->filter_sustain() ) { // Note is still ringing, do not end. - retValue = false; + bRetValue = false; } pSelectedLayerInfo->SamplePosition += nAvail_bytes; @@ -1032,7 +1097,9 @@ bool Sampler::renderNoteNoResample( #ifdef H2CORE_HAVE_LADSPA // LADSPA // change the below return logic if you add code after that ifdef - if (pInstrument->is_muted() || pSong->getIsMuted() ) return retValue; + if ( pInstrument->is_muted() || pSong->getIsMuted() ){ + return bRetValue; + } float masterVol = pSong->getVolume(); for ( unsigned nFX = 0; nFX < MAX_FX; ++nFX ) { LadspaFX *pFX = Effects::get_instance()->getLadspaFX( nFX ); @@ -1060,7 +1127,7 @@ bool Sampler::renderNoteNoResample( // ~LADSPA #endif - return retValue; + return bRetValue; } bool Sampler::renderNoteResample( @@ -1071,38 +1138,40 @@ bool Sampler::renderNoteResample( std::shared_ptr pDrumCompo, int nBufferSize, int nInitialSilence, - float cost_L, - float cost_R, - float cost_track_L, - float cost_track_R, - float fLayerPitch, - std::shared_ptr pSong + float fCost_L, + float fCost_R, + float fCostTrack_L, + float fCostTrack_R, + float fLayerPitch ) { - auto pAudioDriver = Hydrogen::get_instance()->getAudioOutput(); - auto pAudioEngine = Hydrogen::get_instance()->getAudioEngine(); + auto pHydrogen = Hydrogen::get_instance(); + auto pAudioDriver = pHydrogen->getAudioOutput(); + auto pSong = pHydrogen->getSong(); auto pInstrument = pNote->get_instrument(); int nNoteLength = -1; - // Note is not located at the very beginning of the song and is - // enqueued by the AudioEngine. Take possible changes in tempo - // into account + if ( pNote->get_length() != -1 ) { + // The user set a custom duration of the note in the + // PatternEditor. This will be used instead of the full sample + // length. double fTickMismatch; - int nEffectiveDelay = 0; - if ( pNote->get_humanize_delay() < 0 ) { - nEffectiveDelay = pNote->get_humanize_delay(); - } + // Delay is already introduced into the note start used below + // in Note::computeNoteStart. We need to account for it in + // here to in order to get the length of the note right. + const int nDelay = std::clamp( + pNote->get_humanize_delay(), -1 * AudioEngine::nMaxTimeHumanize, + AudioEngine::nMaxTimeHumanize ); - nNoteLength = - pAudioEngine->computeFrameFromTick( pNote->get_position() + - nEffectiveDelay + - pNote->get_length(), &fTickMismatch, - pSample->get_sample_rate() ) - - pAudioEngine->computeFrameFromTick( pNote->get_position() + - nEffectiveDelay, &fTickMismatch, - pSample->get_sample_rate() ); + nNoteLength = + TransportPosition::computeFrameFromTick( pNote->get_position() + nDelay + + pNote->get_length(), &fTickMismatch, + pSample->get_sample_rate() ) - + TransportPosition::computeFrameFromTick( pNote->get_position() + nDelay, + &fTickMismatch, + pSample->get_sample_rate() ); } float fNotePitch = pNote->get_total_pitch() + fLayerPitch; @@ -1111,24 +1180,30 @@ bool Sampler::renderNoteResample( fStep *= static_cast(pSample->get_sample_rate()) / static_cast(pAudioDriver->getSampleRate()); // Adjust for audio driver sample rate - // verifico il numero di frame disponibili ancora da eseguire - int nAvail_bytes = ( int )( ( float )( pSample->get_frames() - pSelectedLayerInfo->SamplePosition ) / fStep ); - + // The number of frames of the sample left to process. + int nRemainingFrames = ( int )( ( float )( pSample->get_frames() - pSelectedLayerInfo->SamplePosition ) / fStep ); - bool retValue = true; // the note is ended - if ( nAvail_bytes > nBufferSize - nInitialSilence ) { // il sample e' piu' grande del buffersize - // imposto il numero dei bytes disponibili uguale al buffersize + bool bRetValue = true; // the note is ended + int nAvail_bytes; + if ( nRemainingFrames > nBufferSize - nInitialSilence ) { + // It The number of frames of the sample left to process + // exceeds what's available in the current process cycle of + // the Sampler. Clip it. nAvail_bytes = nBufferSize - nInitialSilence; - retValue = false; // the note is not ended yet - } else if ( pInstrument->is_filter_active() && pNote->filter_sustain() ) { + // the note is not ended yet + bRetValue = false; + } + else if ( pInstrument->is_filter_active() && pNote->filter_sustain() ) { // If filter is causing note to ring, process more samples. nAvail_bytes = nBufferSize - nInitialSilence; } + else { + nAvail_bytes = nRemainingFrames; + } int nInitialBufferPos = nInitialSilence; - //float fInitialSamplePos = pNote->get_sample_position( pCompo->get_drumkit_componentID() ); double fSamplePos = pSelectedLayerInfo->SamplePosition; - int nTimes = nInitialBufferPos + nAvail_bytes; + int nFinalBufferPos = nInitialBufferPos + nAvail_bytes; auto pSample_data_L = pSample->get_data_l(); auto pSample_data_R = pSample->get_data_r(); @@ -1141,22 +1216,23 @@ bool Sampler::renderNoteResample( float fVal_L; float fVal_R; int nSampleFrames = pSample->get_frames(); - int nNoteEnd; - if ( nNoteLength == -1) { - nNoteEnd = nSampleFrames + 1; + int nNoteEnd = nInitialBufferPos + 1; + if ( nNoteLength == -1 ) { + nNoteEnd += nRemainingFrames; } else { - nNoteEnd = nNoteLength - pSelectedLayerInfo->SamplePosition; + nNoteEnd += (int)( (float) ( nNoteLength - + pSelectedLayerInfo->SamplePosition ) / fStep ); } #ifdef H2CORE_HAVE_JACK - float * pTrackOutL = nullptr; - float * pTrackOutR = nullptr; + float* pTrackOutL = nullptr; + float* pTrackOutR = nullptr; if ( Preferences::get_instance()->m_bJackTrackOuts ) { auto pJackAudioDriver = dynamic_cast( pAudioDriver ); - if( pJackAudioDriver ) { + if ( pJackAudioDriver != nullptr ) { pTrackOutL = pJackAudioDriver->getTrackOut_L( pInstrument, pCompo ); pTrackOutR = pJackAudioDriver->getTrackOut_R( pInstrument, pCompo ); } @@ -1173,7 +1249,7 @@ bool Sampler::renderNoteResample( // - template and multiple instantiations for is_filter_active x each interpolation method // - iterate LP IIR filter coefficients to longer IIR filter to fit vector width // - for ( int nBufferPos = nInitialBufferPos; nBufferPos < nTimes; ++nBufferPos ) { + for ( int nBufferPos = nInitialBufferPos; nBufferPos < nFinalBufferPos; ++nBufferPos ) { int nSamplePos = ( int )fSamplePos; double fDiff = fSamplePos - nSamplePos; @@ -1248,12 +1324,12 @@ bool Sampler::renderNoteResample( fSamplePos += fStep; } - if ( pADSR->applyADSR( buffer_L, buffer_R, nTimes, nNoteEnd, 1 ) ) { - retValue = true; + if ( pADSR->applyADSR( buffer_L, buffer_R, nFinalBufferPos, nNoteEnd, fStep ) ) { + bRetValue = true; } - + // Mix rendered sample buffer to track and mixer output - for ( int nBufferPos = nInitialBufferPos; nBufferPos < nTimes; ++nBufferPos ) { + for ( int nBufferPos = nInitialBufferPos; nBufferPos < nFinalBufferPos; ++nBufferPos ) { fVal_L = buffer_L[nBufferPos]; fVal_R = buffer_R[nBufferPos]; @@ -1265,15 +1341,15 @@ bool Sampler::renderNoteResample( #ifdef H2CORE_HAVE_JACK if ( pTrackOutL ) { - pTrackOutL[nBufferPos] += fVal_L * cost_track_L; + pTrackOutL[nBufferPos] += fVal_L * fCostTrack_L; } if ( pTrackOutR ) { - pTrackOutR[nBufferPos] += fVal_R * cost_track_R; + pTrackOutR[nBufferPos] += fVal_R * fCostTrack_R; } #endif - fVal_L = fVal_L * cost_L; - fVal_R = fVal_R * cost_R; + fVal_L *= fCost_L; + fVal_R *= fCost_R; // update instr peak if ( fVal_L > fInstrPeak_L ) { @@ -1293,7 +1369,7 @@ bool Sampler::renderNoteResample( if ( pInstrument->is_filter_active() && pNote->filter_sustain() ) { // Note is still ringing, do not end. - retValue = false; + bRetValue = false; } pSelectedLayerInfo->SamplePosition += nAvail_bytes * fStep; @@ -1305,7 +1381,7 @@ bool Sampler::renderNoteResample( // LADSPA // change the below return logic if you add code after that ifdef if ( pInstrument->is_muted() || pSong->getIsMuted() ) { - return retValue; + return bRetValue; } float masterVol = pSong->getVolume(); for ( unsigned nFX = 0; nFX < MAX_FX; ++nFX ) { @@ -1333,7 +1409,7 @@ bool Sampler::renderNoteResample( } } #endif - return retValue; + return bRetValue; } @@ -1364,7 +1440,7 @@ void Sampler::stopPlayingNotes( std::shared_ptr pInstr ) /// Preview, uses only the first layer -void Sampler::preview_sample(std::shared_ptr pSample, int length ) +void Sampler::preview_sample(std::shared_ptr pSample, int nLength ) { Hydrogen::get_instance()->getAudioEngine()->lock( RIGHT_HERE ); @@ -1373,7 +1449,7 @@ void Sampler::preview_sample(std::shared_ptr pSample, int length ) pLayer->set_sample( pSample ); - Note *pPreviewNote = new Note( m_pPreviewInstrument, 0, 1.0, 0.f, length, 0 ); + Note *pPreviewNote = new Note( m_pPreviewInstrument, 0, 1.0, 0.f, nLength ); stopPlayingNotes( m_pPreviewInstrument ); noteOn( pPreviewNote ); @@ -1396,7 +1472,7 @@ void Sampler::preview_instrument( std::shared_ptr pInstr ) m_pPreviewInstrument = pInstr; pInstr->set_is_preview_instrument(true); - Note *pPreviewNote = new Note( m_pPreviewInstrument, 0, 1.0, 0.f, MAX_NOTES, 0 ); + Note *pPreviewNote = new Note( m_pPreviewInstrument, 0, 1.0, 0.f, MAX_NOTES ); noteOn( pPreviewNote ); // exclusive note Hydrogen::get_instance()->getAudioEngine()->unlock(); diff --git a/src/core/Sampler/Sampler.h b/src/core/Sampler/Sampler.h index 6f1043ac13..f3ef9e5d0a 100644 --- a/src/core/Sampler/Sampler.h +++ b/src/core/Sampler/Sampler.h @@ -173,7 +173,7 @@ class Sampler : public H2Core::Object Sampler(); ~Sampler(); - void process( uint32_t nFrames, std::shared_ptr pSong ); + void process( uint32_t nFrames ); /** * @return True, if the #Sampler is still processing notes. @@ -264,7 +264,12 @@ class Sampler : public H2Core::Object bool processPlaybackTrack(int nBufferSize); - bool renderNote( Note* pNote, unsigned nBufferSize, std::shared_ptr pSong ); + /** + * Render a note + * + * @return false - the note is not ended, true - the note is ended + */ + bool renderNote( Note* pNote, unsigned nBufferSize ); Interpolation::InterpolateMode m_interpolateMode; @@ -279,8 +284,7 @@ class Sampler : public H2Core::Object float cost_L, float cost_R, float cost_track_L, - float cost_track_R, - std::shared_ptr pSong + float cost_track_R ); bool renderNoteResample( @@ -295,8 +299,7 @@ class Sampler : public H2Core::Object float cost_R, float cost_track_L, float cost_track_R, - float fLayerPitch, - std::shared_ptr pSong + float fLayerPitch ); }; diff --git a/src/core/SoundLibrary/SoundLibraryDatabase.cpp b/src/core/SoundLibrary/SoundLibraryDatabase.cpp index f4b2db0794..1825b8f5cb 100644 --- a/src/core/SoundLibrary/SoundLibraryDatabase.cpp +++ b/src/core/SoundLibrary/SoundLibraryDatabase.cpp @@ -110,6 +110,24 @@ void SoundLibraryDatabase::updateDrumkits( bool bTriggerEvent ) { m_drumkitDatabase[ sDrumkitPath ] = pDrumkit; } + else { + ERRORLOG( QString( "Unable to load drumkit at [%1]" ).arg( sDrumkitPath ) ); + } + } + + if ( bTriggerEvent ) { + EventQueue::get_instance()->push_event( EVENT_SOUND_LIBRARY_CHANGED, 0 ); + } +} + +void SoundLibraryDatabase::updateDrumkit( const QString& sDrumkitPath, bool bTriggerEvent ) { + + auto pDrumkit = Drumkit::load( sDrumkitPath ); + if ( pDrumkit != nullptr ) { + m_drumkitDatabase[ sDrumkitPath ] = pDrumkit; + } + else { + ERRORLOG( QString( "Unable to load drumkit at [%1]" ).arg( sDrumkitPath ) ); } if ( bTriggerEvent ) { diff --git a/src/core/SoundLibrary/SoundLibraryDatabase.h b/src/core/SoundLibrary/SoundLibraryDatabase.h index 63245dcfd9..d87bb0c084 100644 --- a/src/core/SoundLibrary/SoundLibraryDatabase.h +++ b/src/core/SoundLibrary/SoundLibraryDatabase.h @@ -62,6 +62,7 @@ class SoundLibraryDatabase : public H2Core::Object void update(); void updateDrumkits( bool bTriggerEvent = true ); + void updateDrumkit( const QString& sDrumkitPath, bool bTriggerEvent = true ); std::shared_ptr getDrumkit( const QString& sDrumkitPath ); const std::map> getDrumkitDatabase() const { return m_drumkitDatabase; diff --git a/src/gui/src/AudioEngineInfoForm.cpp b/src/gui/src/AudioEngineInfoForm.cpp index ae0915564b..a8d8910b57 100644 --- a/src/gui/src/AudioEngineInfoForm.cpp +++ b/src/gui/src/AudioEngineInfoForm.cpp @@ -36,6 +36,7 @@ #include #include #include +#include using namespace H2Core; AudioEngineInfoForm::AudioEngineInfoForm(QWidget* parent) @@ -94,8 +95,8 @@ void AudioEngineInfoForm::updateInfo() // Song position QString sColumn = "N/A"; - if ( pAudioEngine->getColumn() != -1 ) { - sColumn = QString::number( pAudioEngine->getColumn() ); + if ( pAudioEngine->getTransportPosition()->getColumn() != -1 ) { + sColumn = QString::number( pAudioEngine->getTransportPosition()->getColumn() ); } m_pSongPositionLbl->setText( sColumn ); @@ -125,7 +126,7 @@ void AudioEngineInfoForm::updateInfo() } // tick number - sprintf(tmp, "%03d", (int)pAudioEngine->getPatternTickPosition() ); + sprintf(tmp, "%03d", (int)pAudioEngine->getTransportPosition()->getPatternTickPosition() ); nTicksLbl->setText(tmp); @@ -148,7 +149,7 @@ void AudioEngineInfoForm::updateInfo() sampleRateLbl->setText(QString(tmp)); // Number of frames - sprintf(tmp, "%d", static_cast( pAudioEngine->getFrames() ) ); + sprintf(tmp, "%d", static_cast( pAudioEngine->getTransportPosition()->getFrame() ) ); nFramesLbl->setText(tmp); } else { @@ -158,7 +159,7 @@ void AudioEngineInfoForm::updateInfo() sampleRateLbl->setText( "N/A" ); nFramesLbl->setText( "N/A" ); } - nRealtimeFramesLbl->setText( QString( "%1" ).arg( pAudioEngine->getRealtimeFrames() ) ); + nRealtimeFramesLbl->setText( QString( "%1" ).arg( pAudioEngine->getRealtimeFrame() ) ); // Midi driver info @@ -252,7 +253,7 @@ void AudioEngineInfoForm::stateChangedEvent( H2Core::AudioEngine::State state ) } -void AudioEngineInfoForm::patternChangedEvent() +void AudioEngineInfoForm::playingPatternsChangedEvent() { updateAudioEngineState(); } diff --git a/src/gui/src/AudioEngineInfoForm.h b/src/gui/src/AudioEngineInfoForm.h index f899e89890..1f34880d48 100644 --- a/src/gui/src/AudioEngineInfoForm.h +++ b/src/gui/src/AudioEngineInfoForm.h @@ -46,7 +46,7 @@ class AudioEngineInfoForm : public QWidget, public Ui_AudioEngineInfoForm_UI, p // EventListener implementation virtual void stateChangedEvent( H2Core::AudioEngine::State state) override; - virtual void patternChangedEvent() override; + virtual void playingPatternsChangedEvent() override; virtual void updateSongEvent(int) override; //~ EventListener implementation diff --git a/src/gui/src/Director.cpp b/src/gui/src/Director.cpp index cf5000b343..3ac174bcba 100644 --- a/src/gui/src/Director.cpp +++ b/src/gui/src/Director.cpp @@ -60,6 +60,7 @@ #include #include #include +#include #include #include @@ -79,8 +80,8 @@ Director::Director ( QWidget* pParent ) setWindowTitle ( tr ( "Director" ) ); - m_fBpm = pAudioEngine->getBpm(); - m_nBar = pAudioEngine->getColumn() + 1; + m_fBpm = pAudioEngine->getTransportPosition()->getBpm(); + m_nBar = pAudioEngine->getTransportPosition()->getColumn() + 1; if ( m_nBar <= 0 ){ m_nBar = 1; } @@ -146,8 +147,8 @@ void Director::timelineUpdateEvent( int nValue ) { auto pHydrogen = Hydrogen::get_instance(); auto pAudioEngine = pHydrogen->getAudioEngine(); - m_fBpm = pAudioEngine->getBpm(); - m_nBar = pAudioEngine->getColumn() + 1; + m_fBpm = pAudioEngine->getTransportPosition()->getBpm(); + m_nBar = pAudioEngine->getTransportPosition()->getColumn() + 1; if ( m_nBar <= 0 ){ m_nBar = 1; @@ -174,7 +175,7 @@ void Director::metronomeEvent( int nValue ) //bpm m_fBpm = pHydrogen->getSong()->getBpm(); //bar - m_nBar = pHydrogen->getAudioEngine()->getColumn() + 1; + m_nBar = pHydrogen->getAudioEngine()->getTransportPosition()->getColumn() + 1; if ( m_nBar <= 0 ){ m_nBar = 1; diff --git a/src/gui/src/EventListener.h b/src/gui/src/EventListener.h index dcdef5a382..214f9a04a3 100644 --- a/src/gui/src/EventListener.h +++ b/src/gui/src/EventListener.h @@ -29,7 +29,8 @@ class EventListener { public: virtual void stateChangedEvent( H2Core::AudioEngine::State state) { UNUSED( state ); } - virtual void patternChangedEvent() {} + virtual void playingPatternsChangedEvent() {} + virtual void nextPatternsChangedEvent(){} virtual void patternModifiedEvent() {} virtual void songModifiedEvent() {} virtual void selectedPatternChangedEvent() {} @@ -65,7 +66,6 @@ class EventListener virtual void playbackTrackChangedEvent(){} virtual void soundLibraryChangedEvent(){} virtual void nextShotEvent(){} - virtual void stackedPatternsChangedEvent(){} virtual ~EventListener() {} }; diff --git a/src/gui/src/ExportSongDialog.cpp b/src/gui/src/ExportSongDialog.cpp index 8877e8edbc..c48246f3a7 100644 --- a/src/gui/src/ExportSongDialog.cpp +++ b/src/gui/src/ExportSongDialog.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #include @@ -551,7 +552,7 @@ void ExportSongDialog::closeExport() { if( m_pPreferences->getRubberBandBatchMode() ){ m_pHydrogen->getAudioEngine()->lock( RIGHT_HERE ); - m_pHydrogen->recalculateRubberband( m_pHydrogen->getAudioEngine()->getBpm() ); + m_pHydrogen->recalculateRubberband( m_pHydrogen->getAudioEngine()->getTransportPosition()->getBpm() ); m_pHydrogen->getAudioEngine()->unlock(); } m_pPreferences->setRubberBandBatchMode( m_bOldRubberbandBatchMode ); diff --git a/src/gui/src/HydrogenApp.cpp b/src/gui/src/HydrogenApp.cpp index d3789c51c4..6bce5e7852 100644 --- a/src/gui/src/HydrogenApp.cpp +++ b/src/gui/src/HydrogenApp.cpp @@ -123,7 +123,9 @@ HydrogenApp::HydrogenApp( MainForm *pMainForm ) addEventListener( this ); connect( this, &HydrogenApp::preferencesChanged, - m_pMainForm, &MainForm::onPreferencesChanged ); + m_pMainForm, &MainForm::onPreferencesChanged ); + connect( this, &HydrogenApp::preferencesChanged, + this, &HydrogenApp::onPreferencesChanged ); } void HydrogenApp::setWindowProperties( QWidget *pWindow, WindowProperties &prop, unsigned flags ) { @@ -389,7 +391,7 @@ bool HydrogenApp::openSong( QString sFilename ) { // In case the user did open a hidden file, the baseName() // will be an empty string. QString sBaseName( fileInfo.completeBaseName() ); - if ( sBaseName.front() == "." ) { + if ( sBaseName.startsWith( "." ) ) { sBaseName.remove( 0, 1 ); } @@ -472,7 +474,7 @@ bool HydrogenApp::recoverEmptySong() { // In case the user did open a hidden file, the baseName() // will be an empty string. QString sBaseName( fileInfo.completeBaseName() ); - if ( sBaseName.front() == "." ) { + if ( sBaseName.startsWith( "." ) ) { sBaseName.remove( 0, 1 ); } @@ -716,8 +718,12 @@ void HydrogenApp::onEventQueueTimer() pListener->stateChangedEvent( static_cast(event.value) ); break; - case EVENT_PATTERN_CHANGED: - pListener->patternChangedEvent(); + case EVENT_PLAYING_PATTERNS_CHANGED: + pListener->playingPatternsChangedEvent(); + break; + + case EVENT_NEXT_PATTERNS_CHANGED: + pListener->nextPatternsChangedEvent(); break; case EVENT_PATTERN_MODIFIED: @@ -859,10 +865,6 @@ void HydrogenApp::onEventQueueTimer() case EVENT_NEXT_SHOT: pListener->nextShotEvent(); break; - - case EVENT_STACKED_PATTERNS_CHANGED: - pListener->stackedPatternsChangedEvent(); - break; default: ERRORLOG( QString("[onEventQueueTimer] Unhandled event: %1").arg( event.type ) ); @@ -1203,3 +1205,11 @@ bool HydrogenApp::checkDrumkitLicense( std::shared_ptr pDrumkit return true; } + +void HydrogenApp::onPreferencesChanged( H2Core::Preferences::Changes changes ) { + if ( changes & H2Core::Preferences::Changes::AudioTab ) { + H2Core::Hydrogen::get_instance()->getAudioEngine()-> + getMetronomeInstrument()->set_volume( + Preferences::get_instance()->m_fMetronomeVolume ); + } +} diff --git a/src/gui/src/HydrogenApp.h b/src/gui/src/HydrogenApp.h index 247b2750b6..616515d98e 100644 --- a/src/gui/src/HydrogenApp.h +++ b/src/gui/src/HydrogenApp.h @@ -186,6 +186,7 @@ class HydrogenApp : public QObject, public EventListener, public H2Core::Objec * underlying options in the Preferences class. */ void changePreferences( H2Core::Preferences::Changes changes ); + void onPreferencesChanged( H2Core::Preferences::Changes changes ); private slots: void propagatePreferences(); diff --git a/src/gui/src/InstrumentEditor/InstrumentEditor.cpp b/src/gui/src/InstrumentEditor/InstrumentEditor.cpp index 4981ed8abd..927f8df328 100644 --- a/src/gui/src/InstrumentEditor/InstrumentEditor.cpp +++ b/src/gui/src/InstrumentEditor/InstrumentEditor.cpp @@ -202,14 +202,14 @@ InstrumentEditor::InstrumentEditor( QWidget* pParent ) // ADSR m_pAttackRotary = new Rotary( m_pInstrumentProp, Rotary::Type::Normal, - tr( "Attack" ), false ); + tr( "Length of Attack phase.\n\nValue" ), false ); m_pDecayRotary = new Rotary( m_pInstrumentProp, Rotary::Type::Normal, - tr( "Decay" ), false ); + tr( "Length of Decay phase.\n\nValue" ), false ); m_pSustainRotary = new Rotary( m_pInstrumentProp, Rotary::Type::Normal, - tr( "Sustain" ), false ); + tr( "Sample volume in Sustain phase.\n\nValue" ), false ); m_pSustainRotary->setDefaultValue( m_pSustainRotary->getMax() ); m_pReleaseRotary = new Rotary( m_pInstrumentProp, Rotary::Type::Normal, - tr( "Release" ), false ); + tr( "Length of Release phase.\n\nValue" ), false ); m_pReleaseRotary->setDefaultValue( 0.09 ); connect( m_pAttackRotary, SIGNAL( valueChanged( WidgetWithInput* ) ), this, SLOT( rotaryChanged( WidgetWithInput* ) ) ); @@ -517,14 +517,14 @@ void InstrumentEditor::selectedInstrumentChangedEvent() m_pInstrument = pHydrogen->getSelectedInstrument(); // update layer list - if ( m_pInstrument ) { + if ( m_pInstrument != nullptr ) { m_pNameLbl->setText( m_pInstrument->get_name() ); // ADSR - m_pAttackRotary->setValue( sqrtf(m_pInstrument->get_adsr()->get_attack() / 100000.0) ); - m_pDecayRotary->setValue( sqrtf(m_pInstrument->get_adsr()->get_decay() / 100000.0) ); - m_pSustainRotary->setValue( m_pInstrument->get_adsr()->get_sustain() ); - float fTmp = m_pInstrument->get_adsr()->get_release() - 256.0; + m_pAttackRotary->setValue( sqrtf(m_pInstrument->get_adsr()->getAttack() / 100000.0) ); + m_pDecayRotary->setValue( sqrtf(m_pInstrument->get_adsr()->getDecay() / 100000.0) ); + m_pSustainRotary->setValue( m_pInstrument->get_adsr()->getSustain() ); + float fTmp = m_pInstrument->get_adsr()->getRelease() - 256.0; if( fTmp < 0.0 ) { fTmp = 0.0; } @@ -721,16 +721,16 @@ void InstrumentEditor::rotaryChanged( WidgetWithInput *ref) m_pInstrument->set_filter_resonance( fVal ); } else if ( pRotary == m_pAttackRotary ) { - m_pInstrument->get_adsr()->set_attack( fVal * fVal * 100000 ); + m_pInstrument->get_adsr()->setAttack( fVal * fVal * 100000 ); } else if ( pRotary == m_pDecayRotary ) { - m_pInstrument->get_adsr()->set_decay( fVal * fVal * 100000 ); + m_pInstrument->get_adsr()->setDecay( fVal * fVal * 100000 ); } else if ( pRotary == m_pSustainRotary ) { - m_pInstrument->get_adsr()->set_sustain( fVal ); + m_pInstrument->get_adsr()->setSustain( fVal ); } else if ( pRotary == m_pReleaseRotary ) { - m_pInstrument->get_adsr()->set_release( 256.0 + fVal * fVal * 100000 ); + m_pInstrument->get_adsr()->setRelease( 256.0 + fVal * fVal * 100000 ); } else if ( pRotary == m_pLayerGainRotary ) { char tmp[20]; @@ -959,7 +959,6 @@ void InstrumentEditor::loadLayerBtnClicked() // Ensure instrument pointer is current m_pInstrument = pHydrogen->getSelectedInstrument(); - if ( m_pInstrument == nullptr ) { WARNINGLOG( "No instrument selected" ); return; diff --git a/src/gui/src/InstrumentEditor/LayerPreview.cpp b/src/gui/src/InstrumentEditor/LayerPreview.cpp index df272eb7e3..20d5172e27 100644 --- a/src/gui/src/InstrumentEditor/LayerPreview.cpp +++ b/src/gui/src/InstrumentEditor/LayerPreview.cpp @@ -218,7 +218,7 @@ void LayerPreview::selectedInstrumentChangedEvent() bool bSelectedLayerChanged = false; // select the last valid layer - if ( m_pInstrument ) { + if ( m_pInstrument != nullptr ) { for (int i = InstrumentComponent::getMaxLayers() - 1; i >= 0; i-- ) { auto p_compo = m_pInstrument->get_component( m_nSelectedComponent ); if ( p_compo ) { @@ -281,18 +281,15 @@ void LayerPreview::mouseReleaseEvent(QMouseEvent *ev) void LayerPreview::mousePressEvent(QMouseEvent *ev) { - const unsigned nPosition = 0; - const float fPan = 0.f; - const int nLength = -1; - const float fPitch = 0.0f; + const int nPosition = 0; - if ( !m_pInstrument ) { + if ( m_pInstrument == nullptr ) { return; } if ( ev->y() < 20 ) { - float fVelocity = (float)ev->x() / (float)width(); + const float fVelocity = (float)ev->x() / (float)width(); - Note * pNote = new Note( m_pInstrument, nPosition, fVelocity, fPan, nLength, fPitch ); + Note * pNote = new Note( m_pInstrument, nPosition, fVelocity ); pNote->set_specific_compo_id( m_nSelectedComponent ); Hydrogen::get_instance()->getAudioEngine()->getSampler()->noteOn(pNote); @@ -320,10 +317,11 @@ void LayerPreview::mousePressEvent(QMouseEvent *ev) InstrumentEditorPanel::get_instance()->selectLayer( m_nSelectedLayer ); auto pCompo = m_pInstrument->get_component(m_nSelectedComponent); - if(pCompo) { + if( pCompo != nullptr ) { auto pLayer = pCompo->get_layer( m_nSelectedLayer ); - if ( pLayer ) { - Note *note = new Note( m_pInstrument , nPosition, m_pInstrument->get_component(m_nSelectedComponent)->get_layer( m_nSelectedLayer )->get_end_velocity() - 0.01, fPan, nLength, fPitch ); + if ( pLayer != nullptr ) { + const float fVelocity = pLayer->get_end_velocity() - 0.01; + Note *note = new Note( m_pInstrument, nPosition, fVelocity ); note->set_specific_compo_id( m_nSelectedComponent ); Hydrogen::get_instance()->getAudioEngine()->getSampler()->noteOn(note); diff --git a/src/gui/src/MainForm.cpp b/src/gui/src/MainForm.cpp index 4e1c40f368..34d80c2173 100644 --- a/src/gui/src/MainForm.cpp +++ b/src/gui/src/MainForm.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -34,6 +35,7 @@ #include #include #include +#include #include #include "AboutDialog.h" @@ -614,8 +616,8 @@ void MainForm::action_file_new() // of undoing the action. Therefore, a warning popup will // check whether the action was intentional. QMessageBox confirmationBox; - confirmationBox.setText( "Replace current song with empty one?" ); - confirmationBox.setInformativeText( "You won't be able to undo this action! Please export the current song from the session first in order to keep it." ); + confirmationBox.setText( tr( "Replace current song with empty one?" ) ); + confirmationBox.setInformativeText( tr( "You won't be able to undo this action after saving the new song! Please export the current song from the session first in order to keep it." ) ); confirmationBox.setStandardButtons( QMessageBox::Yes | QMessageBox::No ); confirmationBox.setDefaultButton( QMessageBox::No ); @@ -632,7 +634,7 @@ void MainForm::action_file_new() // autosave file in order to start fresh. QFileInfo fileInfo( Filesystem::empty_song_path() ); QString sBaseName( fileInfo.completeBaseName() ); - if ( sBaseName.front() == "." ) { + if ( sBaseName.startsWith( "." ) ) { sBaseName.remove( 0, 1 ); } QFileInfo autoSaveFile( QString( "%1/.%2.autosave.h2song" ) @@ -643,6 +645,10 @@ void MainForm::action_file_new() } h2app->openSong( pSong ); + + // The drumkit of the new song will linked into the session + // folder during the next song save. + pHydrogen->setSessionDrumkitNeedsRelinking( true ); } @@ -655,7 +661,32 @@ void MainForm::action_file_save_as() if ( pSong == nullptr ) { return; } + const bool bUnderSessionManagement = pHydrogen->isUnderSessionManagement(); + if ( bUnderSessionManagement && + pHydrogen->getSessionDrumkitNeedsRelinking() ) { + // When used under session management "save as" will be used + // for exporting and Hydrogen is allowed to + // be in a transient state which is not ready for export. This + // way the user is able to undo e.g. loading a drumkit by + // closing the session without storing and the overall state + // is not getting bricked during unexpected shut downs. + // + // We will prompt for saving the changes applied to the + // drumkit usage and require the user to exit this transient + // state first. + if ( QMessageBox::information( this, "Hydrogen", + tr( "\nThere have been recent changes to the drumkit settings.\n" + "The session needs to be saved before exporting will can be continued.\n" ), + QMessageBox::Save | QMessageBox::Cancel, + QMessageBox::Save ) + == QMessageBox::Cancel ) { + INFOLOG( "Exporting cancelled at relinking" ); + return; + } + + action_file_save(); + } QString sPath = Preferences::get_instance()->getLastSaveSongAsDirectory(); if ( ! Filesystem::dir_writable( sPath, false ) ){ @@ -677,48 +708,77 @@ void MainForm::action_file_save_as() fd.setSidebarUrls( fd.sidebarUrls() << QUrl::fromLocalFile( Filesystem::songs_dir() ) ); - QString defaultFilename; - QString lastFilename = pSong->getFilename(); + QString sDefaultFilename; + + // Cachce a couple of things we have to restore when under session + // management. + QString sLastFilename = pSong->getFilename(); + QString sLastLoadedDrumkitPath = pSong->getLastLoadedDrumkitPath(); - if ( lastFilename == Filesystem::empty_song_path() ) { - defaultFilename = Filesystem::default_song_name(); - } else if ( lastFilename.isEmpty() ) { - defaultFilename = pHydrogen->getSong()->getName(); + if ( sLastFilename == Filesystem::empty_song_path() ) { + sDefaultFilename = Filesystem::default_song_name(); + } else if ( sLastFilename.isEmpty() ) { + sDefaultFilename = pHydrogen->getSong()->getName(); } else { - QFileInfo fileInfo( lastFilename ); - defaultFilename = fileInfo.completeBaseName(); + QFileInfo fileInfo( sLastFilename ); + sDefaultFilename = fileInfo.completeBaseName(); } - defaultFilename += Filesystem::songs_ext; + sDefaultFilename += Filesystem::songs_ext; - fd.selectFile( defaultFilename ); + fd.selectFile( sDefaultFilename ); if (fd.exec() == QDialog::Accepted) { - QString filename = fd.selectedFiles().first(); + QString sNewFilename = fd.selectedFiles().first(); - if ( !filename.isEmpty() ) { + if ( ! sNewFilename.isEmpty() ) { Preferences::get_instance()->setLastSaveSongAsDirectory( fd.directory().absolutePath( ) ); - QString sNewFilename = filename; - if ( sNewFilename.endsWith( Filesystem::songs_ext ) == false ) { - filename += Filesystem::songs_ext; + if ( ! sNewFilename.endsWith( Filesystem::songs_ext ) ) { + sNewFilename += Filesystem::songs_ext; } +#ifdef H2CORE_HAVE_OSC + // In a session all main samples (last loaded drumkit) are + // taken from the session folder itself (either via a + // symlink or a copy of the whole drumkit). When exporting + // a song, these "local" references have to be replaced by + // global ones (drumkits in the system's or user's data + // folder). + if ( bUnderSessionManagement ) { + pHydrogen->setSessionIsExported( true ); + int nRet = NsmClient::dereferenceDrumkit( pSong ); + if ( nRet == -2 ) { + QMessageBox::warning( this, "Hydrogen", + tr( "Drumkit [%1] used in session could not found on your system. Please install it in to make the exported song work properly." ) + .arg( pSong->getLastLoadedDrumkitName() ) ); + } + } +#endif + // We do not use the CoreActionController::saveSongAs // function directly since action_file_save as does some // additional checks and prompts the user a warning dialog // if required. action_file_save( sNewFilename ); } - - // When Hydrogen is under session management, the file name - // provided by the NSM server has to be preserved. + +#ifdef H2CORE_HAVE_OSC + // When Hydrogen is under session management, we only copy a + // backup of the song to a different place but keep working on + // the original. if ( bUnderSessionManagement ) { - pSong->setFilename( lastFilename ); - h2app->showStatusBarMessage( tr("Song exported as: ") + defaultFilename ); - } else { - h2app->showStatusBarMessage( tr("Song saved as: ") + defaultFilename ); + pSong->setFilename( sLastFilename ); + NsmClient::replaceDrumkitPath( pSong, sLastLoadedDrumkitPath ); + h2app->showStatusBarMessage( tr("Song exported as: ") + sDefaultFilename ); + pHydrogen->setSessionIsExported( false ); } - + else { + h2app->showStatusBarMessage( tr("Song saved as: ") + sDefaultFilename ); + } +#else + h2app->showStatusBarMessage( tr("Song saved as: ") + sDefaultFilename ); +#endif + h2app->updateWindowTitle(); } } @@ -1494,8 +1554,13 @@ void MainForm::onRestartAccelEvent() void MainForm::onBPMPlusAccelEvent() { auto pHydrogen = Hydrogen::get_instance(); auto pAudioEngine = pHydrogen->getAudioEngine(); - pAudioEngine->setNextBpm( pAudioEngine->getBpm() + 0.1 ); - pHydrogen->getSong()->setBpm( pAudioEngine->getBpm() + 0.1 ); + + pHydrogen->getSong()->setBpm( pAudioEngine->getTransportPosition()->getBpm() + 0.1 ); + + pAudioEngine->lock( RIGHT_HERE ); + pAudioEngine->setNextBpm( pAudioEngine->getTransportPosition()->getBpm() + 0.1 ); + pAudioEngine->unlock(); + EventQueue::get_instance()->push_event( EVENT_TEMPO_CHANGED, -1 ); } @@ -1504,8 +1569,13 @@ void MainForm::onBPMPlusAccelEvent() { void MainForm::onBPMMinusAccelEvent() { auto pHydrogen = Hydrogen::get_instance(); auto pAudioEngine = pHydrogen->getAudioEngine(); - pAudioEngine->setNextBpm( pAudioEngine->getBpm() - 0.1 ); - pHydrogen->getSong()->setBpm( pAudioEngine->getBpm() - 0.1 ); + + pHydrogen->getSong()->setBpm( pAudioEngine->getTransportPosition()->getBpm() - 0.1 ); + + pAudioEngine->lock( RIGHT_HERE ); + pAudioEngine->setNextBpm( pAudioEngine->getTransportPosition()->getBpm() - 0.1 ); + pAudioEngine->unlock(); + EventQueue::get_instance()->push_event( EVENT_TEMPO_CHANGED, -1 ); } @@ -1741,6 +1811,17 @@ bool MainForm::eventFilter( QObject *o, QEvent *e ) // special processing for key press QKeyEvent *k = (QKeyEvent *)e; + if ( k->matches( QKeySequence::StandardKey::Undo ) ) { + k->accept(); + action_undo(); + return true; + } else if ( k->matches( QKeySequence::StandardKey::Redo ) ) { + k->accept(); + action_redo(); + return true; + } + + // qDebug( "Got key press for instrument '%c'", k->ascii() ); switch (k->key()) { case Qt::Key_Space: @@ -1833,12 +1914,12 @@ bool MainForm::eventFilter( QObject *o, QEvent *e ) break; case Qt::Key_F9 : // Qt::Key_Left do not work. Some ideas ? - pHydrogen->getCoreActionController()->locateToColumn( pHydrogen->getAudioEngine()->getColumn() - 1 ); + pHydrogen->getCoreActionController()->locateToColumn( pHydrogen->getAudioEngine()->getTransportPosition()->getColumn() - 1 ); return true; break; case Qt::Key_F10 : // Qt::Key_Right do not work. Some ideas ? - pHydrogen->getCoreActionController()->locateToColumn( pHydrogen->getAudioEngine()->getColumn() + 1 ); + pHydrogen->getCoreActionController()->locateToColumn( pHydrogen->getAudioEngine()->getTransportPosition()->getColumn() + 1 ); return true; break; } @@ -2093,7 +2174,7 @@ QString MainForm::getAutoSaveFilename() // In case the user did open a hidden file, the baseName() // will be an empty string. QString sBaseName( fileInfo.completeBaseName() ); - if ( sBaseName.front() == "." ) { + if ( sBaseName.startsWith( "." ) ) { sBaseName.remove( 0, 1 ); } diff --git a/src/gui/src/Mixer/Mixer.cpp b/src/gui/src/Mixer/Mixer.cpp index c9e21007be..a6c7ce6681 100644 --- a/src/gui/src/Mixer/Mixer.cpp +++ b/src/gui/src/Mixer/Mixer.cpp @@ -329,8 +329,7 @@ void Mixer::noteOnClicked( MixerLine* ref ) return; } - const float fPitch = pSelectedInstrument->get_pitch_offset(); - Note *pNote = new Note( pSelectedInstrument, 0, 1.0, 0.f, -1, fPitch ); + Note *pNote = new Note( pSelectedInstrument, 0, 1.0 ); pHydrogen->getAudioEngine()->getSampler()->noteOn(pNote); } @@ -350,8 +349,8 @@ void Mixer::noteOnClicked( MixerLine* ref ) return; } - const float fPitch = 0.0f; - Note *pNote = new Note( pSelectedInstrument, 0, 1.0, 0.f,-1, fPitch ); + Note *pNote = new Note( pSelectedInstrument, 0, 0.0 ); + pNote->set_note_off( true ); pHydrogen->getAudioEngine()->getSampler()->noteOff(pNote); } diff --git a/src/gui/src/Mixer/MixerLine.cpp b/src/gui/src/Mixer/MixerLine.cpp index 86d5737393..fd49228be8 100644 --- a/src/gui/src/Mixer/MixerLine.cpp +++ b/src/gui/src/Mixer/MixerLine.cpp @@ -75,7 +75,10 @@ MixerLine::MixerLine(QWidget* parent, int nInstr) m_pPlaySampleBtn = new Button( this, QSize( 20, 15 ), Button::Type::Push, "play.svg", "", false, QSize( 7, 7 ), tr( "Play sample" ) ); m_pPlaySampleBtn->move( 6, 1 ); m_pPlaySampleBtn->setObjectName( "PlaySampleButton" ); - connect(m_pPlaySampleBtn, SIGNAL( clicked() ), this, SLOT( playSampleBtnClicked() ) ); + connect(m_pPlaySampleBtn, &Button::clicked, + [&]() { emit noteOnClicked(this); }); + connect(m_pPlaySampleBtn, &Button::rightClicked, + [&]() { emit noteOffClicked(this); }); // Trigger sample LED m_pTriggerSampleLED = new LED( this, QSize( 5, 13 ) ); @@ -197,10 +200,6 @@ void MixerLine::updateMixerLine() m_nPeakTimer++; } -void MixerLine::playSampleBtnClicked() { - emit noteOnClicked(this); -} - void MixerLine::muteBtnClicked() { Hydrogen::get_instance()->setIsModified( true ); emit muteBtnClicked(this); diff --git a/src/gui/src/Mixer/MixerLine.h b/src/gui/src/Mixer/MixerLine.h index 902897880a..b7e123599c 100644 --- a/src/gui/src/Mixer/MixerLine.h +++ b/src/gui/src/Mixer/MixerLine.h @@ -134,7 +134,6 @@ class MixerLine: public PixmapWidget, public H2Core::Object void knobChanged(MixerLine *ref, int nKnob); public slots: - void playSampleBtnClicked(); void muteBtnClicked(); void soloBtnClicked(); void faderChanged(WidgetWithInput *ref); diff --git a/src/gui/src/PatternEditor/DrumPatternEditor.cpp b/src/gui/src/PatternEditor/DrumPatternEditor.cpp index da71547ce8..33b3e6edf8 100644 --- a/src/gui/src/PatternEditor/DrumPatternEditor.cpp +++ b/src/gui/src/PatternEditor/DrumPatternEditor.cpp @@ -114,7 +114,7 @@ void DrumPatternEditor::updateEditor( bool bPatternOnly ) resize( m_nEditorWidth, m_nEditorHeight ); // redraw all - createBackground(); + invalidateBackground(); update(); } @@ -284,7 +284,7 @@ void DrumPatternEditor::mousePressEvent( QMouseEvent* ev ) { auto pSong = pHydrogen->getSong(); int nInstruments = pSong->getInstrumentList()->size(); int nRow = static_cast( ev->y() / static_cast(m_nGridHeight) ); - if ( nRow >= nInstruments ) { + if ( nRow >= nInstruments || nRow < 0 ) { return; } @@ -473,10 +473,8 @@ void DrumPatternEditor::addOrDeleteNoteAction( int nColumn, nLength = 1; fProbability = 1.0; } - - float fPitch = 0.f; - Note *pNote = new Note( pSelectedInstrument, nPosition, fVelocity, fPan, nLength, fPitch ); + Note *pNote = new Note( pSelectedInstrument, nPosition, fVelocity, fPan, nLength ); pNote->set_note_off( isNoteOff ); if ( !isNoteOff ) { pNote->set_lead_lag( oldLeadLag ); @@ -494,8 +492,7 @@ void DrumPatternEditor::addOrDeleteNoteAction( int nColumn, } // hear note if ( listen && !isNoteOff ) { - fPitch = pSelectedInstrument->get_pitch_offset(); - Note *pNote2 = new Note( pSelectedInstrument, 0, fVelocity, fPan, nLength, fPitch); + Note *pNote2 = new Note( pSelectedInstrument, 0, fVelocity, fPan, nLength); m_pAudioEngine->getSampler()->noteOn(pNote2); } } @@ -1203,8 +1200,10 @@ void DrumPatternEditor::drawBackground( QPainter& p) int nSelectedInstrument = pHydrogen->getSelectedInstrumentNumber(); p.fillRect(0, 0, m_nActiveWidth, m_nEditorHeight, backgroundColor); - // p.fillRect(m_nActiveWidth, 0, m_nEditorWidth - m_nActiveWidth, m_nEditorHeight, - // backgroundInactiveColor); + if ( m_nActiveWidth < m_nEditorWidth ) { + p.fillRect(m_nActiveWidth, 0, m_nEditorWidth - m_nActiveWidth, m_nEditorHeight, + backgroundInactiveColor); + } for ( int ii = 0; ii < nInstruments; ii++ ) { int y = static_cast(m_nGridHeight) * ii; @@ -1274,6 +1273,7 @@ void DrumPatternEditor::drawBackground( QPainter& p) } void DrumPatternEditor::createBackground() { + m_bBackgroundInvalid = false; // Resize pixmap if pixel ratio has changed qreal pixelRatio = devicePixelRatio(); @@ -1301,7 +1301,7 @@ void DrumPatternEditor::paintEvent( QPaintEvent* ev ) auto pPref = Preferences::get_instance(); qreal pixelRatio = devicePixelRatio(); - if ( pixelRatio != m_pBackgroundPixmap->devicePixelRatio() ) { + if ( pixelRatio != m_pBackgroundPixmap->devicePixelRatio() || m_bBackgroundInvalid ) { createBackground(); } @@ -1699,17 +1699,12 @@ void DrumPatternEditor::functionFillNotesRedoAction( QStringList noteList, int n return; } - const float velocity = 0.8f; - const float fPan = 0.f; - const float fPitch = 0.0f; - const int nLength = -1; - m_pAudioEngine->lock( RIGHT_HERE ); // lock the audio engine for (int i = 0; i < noteList.size(); i++ ) { // create the new note int position = noteList.value(i).toInt(); - Note *pNote = new Note( pSelectedInstrument, position, velocity, fPan, nLength, fPitch ); + Note *pNote = new Note( pSelectedInstrument, position ); pPattern->insert_note( pNote ); } m_pAudioEngine->unlock(); // unlock the audio engine diff --git a/src/gui/src/PatternEditor/NotePropertiesRuler.cpp b/src/gui/src/PatternEditor/NotePropertiesRuler.cpp index 18d90b2c00..319d9a1108 100644 --- a/src/gui/src/PatternEditor/NotePropertiesRuler.cpp +++ b/src/gui/src/PatternEditor/NotePropertiesRuler.cpp @@ -165,7 +165,7 @@ void NotePropertiesRuler::wheelEvent(QWheelEvent *ev ) if ( bValueChanged ) { addUndoAction(); - createBackground(); + invalidateBackground(); update(); } } @@ -286,7 +286,7 @@ void NotePropertiesRuler::selectionMoveUpdateEvent( QMouseEvent *ev ) { } if ( bValueChanged ) { - createBackground(); + invalidateBackground(); update(); } } @@ -294,7 +294,7 @@ void NotePropertiesRuler::selectionMoveUpdateEvent( QMouseEvent *ev ) { void NotePropertiesRuler::selectionMoveEndEvent( QInputEvent *ev ) { //! The "move" has already been reflected in the notes. Now just complete Undo event. addUndoAction(); - createBackground(); + invalidateBackground(); update(); } @@ -369,7 +369,7 @@ void NotePropertiesRuler::propertyDragStart( QMouseEvent *ev ) { setCursor( Qt::CrossCursor ); prepareUndoAction( ev->x() ); - createBackground(); + invalidateBackground(); update(); } @@ -529,7 +529,7 @@ void NotePropertiesRuler::propertyDragUpdate( QMouseEvent *ev ) } m_nDragPreviousColumn = nColumn; - createBackground(); + invalidateBackground(); update(); m_pPatternEditorPanel->getPianoRollEditor()->updateEditor(); @@ -540,7 +540,7 @@ void NotePropertiesRuler::propertyDragEnd() { addUndoAction(); unsetCursor(); - createBackground(); + invalidateBackground(); update(); } @@ -826,7 +826,7 @@ void NotePropertiesRuler::keyPressEvent( QKeyEvent *ev ) m_selection.updateKeyboardCursorPosition( getKeyboardCursorRect() ); if ( bValueChanged ) { - createBackground(); + invalidateBackground(); } update(); @@ -886,7 +886,7 @@ void NotePropertiesRuler::paintEvent( QPaintEvent *ev) auto pPref = Preferences::get_instance(); qreal pixelRatio = devicePixelRatio(); - if ( pixelRatio != m_pBackgroundPixmap->devicePixelRatio() ) { + if ( pixelRatio != m_pBackgroundPixmap->devicePixelRatio() || m_bBackgroundInvalid ) { createBackground(); } @@ -1322,6 +1322,10 @@ void NotePropertiesRuler::createNoteKeyBackground(QPixmap *pixmap) if ( m_pPattern != nullptr ) { auto pSelectedInstrument = Hydrogen::get_instance()->getSelectedInstrument(); + if ( pSelectedInstrument == nullptr ) { + DEBUGLOG( "No instrument selected" ); + return; + } QPen selectedPen( selectedNoteColor() ); selectedPen.setWidth( 2 ); @@ -1416,7 +1420,7 @@ void NotePropertiesRuler::updateEditor( bool ) m_nActiveWidth = m_nEditorWidth; } - createBackground(); + invalidateBackground(); update(); } @@ -1446,6 +1450,7 @@ void NotePropertiesRuler::createBackground() createNoteKeyBackground( m_pBackgroundPixmap ); } update(); + m_bBackgroundInvalid = false; } @@ -1497,7 +1502,7 @@ std::vector NotePropertiesRuler::elementsIn } // Updating selection, we may need to repaint the whole widget. - createBackground(); + invalidateBackground(); update(); return std::move(result); @@ -1523,7 +1528,7 @@ void NotePropertiesRuler::onPreferencesChanged( H2Core::Preferences::Changes cha if ( changes & ( H2Core::Preferences::Changes::Colors | H2Core::Preferences::Changes::Font ) ) { - createBackground(); + invalidateBackground(); update(); } } diff --git a/src/gui/src/PatternEditor/PatternEditor.cpp b/src/gui/src/PatternEditor/PatternEditor.cpp index c4d6b31e1f..01b92aab35 100644 --- a/src/gui/src/PatternEditor/PatternEditor.cpp +++ b/src/gui/src/PatternEditor/PatternEditor.cpp @@ -95,6 +95,7 @@ PatternEditor::PatternEditor( QWidget *pParent, m_pBackgroundPixmap = new QPixmap( m_nEditorWidth * pixelRatio, height() * pixelRatio ); m_pBackgroundPixmap->setDevicePixelRatio( pixelRatio ); + m_bBackgroundInvalid = true; } PatternEditor::~PatternEditor() @@ -812,10 +813,8 @@ void PatternEditor::focusInEvent( QFocusEvent *ev ) { m_pPatternEditorPanel->getPatternEditorRuler()->update(); m_pPatternEditorPanel->getInstrumentList()->update(); } - - // If there are some patterns selected, we have to switch their - // border color inactive <-> active. - createBackground(); + + // Update to show the focus border highlight update(); } @@ -826,12 +825,14 @@ void PatternEditor::focusOutEvent( QFocusEvent *ev ) { m_pPatternEditorPanel->getInstrumentList()->update(); } - // If there are some patterns selected, we have to switch their - // border color inactive <-> active. - createBackground(); + // Update to remove the focus border highlight update(); } +void PatternEditor::invalidateBackground() { + m_bBackgroundInvalid = true; +} + void PatternEditor::createBackground() { } diff --git a/src/gui/src/PatternEditor/PatternEditor.h b/src/gui/src/PatternEditor/PatternEditor.h index b7305c5188..60c891c5dc 100644 --- a/src/gui/src/PatternEditor/PatternEditor.h +++ b/src/gui/src/PatternEditor/PatternEditor.h @@ -272,7 +272,9 @@ public slots: /** Updates #m_pBackgroundPixmap to show the latest content. */ virtual void createBackground(); + void invalidateBackground(); QPixmap *m_pBackgroundPixmap; + bool m_bBackgroundInvalid; /** Indicates whether the mouse pointer entered the widget.*/ bool m_bEntered; diff --git a/src/gui/src/PatternEditor/PatternEditorInstrumentList.cpp b/src/gui/src/PatternEditor/PatternEditorInstrumentList.cpp index c5484ed136..11ecc669ef 100644 --- a/src/gui/src/PatternEditor/PatternEditorInstrumentList.cpp +++ b/src/gui/src/PatternEditor/PatternEditorInstrumentList.cpp @@ -363,10 +363,8 @@ void InstrumentLine::mousePressEvent(QMouseEvent *ev) HydrogenApp::get_instance()->getPatternEditorPanel()->getDrumPatternEditor()->updateEditor(); if ( ev->button() == Qt::LeftButton ) { - const int width = m_pMuteBtn->x() - 5; // clickable field width - const float velocity = std::min((float)ev->x()/(float)width, 1.0f); - const float fPan = 0.f; - const int nLength = -1; + const int nWidth = m_pMuteBtn->x() - 5; // clickable field width + const float fVelocity = std::min((float)ev->x()/(float)nWidth, 1.0f); std::shared_ptr pSong = Hydrogen::get_instance()->getSong(); auto pInstr = pSong->getInstrumentList()->get( m_nInstrumentNumber ); @@ -374,9 +372,8 @@ void InstrumentLine::mousePressEvent(QMouseEvent *ev) ERRORLOG( "No instrument selected" ); return; } - const float fPitch = pInstr->get_pitch_offset(); - Note *pNote = new Note( pInstr, 0, velocity, fPan, nLength, fPitch); + Note *pNote = new Note( pInstr, 0, fVelocity); Hydrogen::get_instance()->getAudioEngine()->getSampler()->noteOn(pNote); } else if (ev->button() == Qt::RightButton ) { diff --git a/src/gui/src/PatternEditor/PatternEditorPanel.cpp b/src/gui/src/PatternEditor/PatternEditorPanel.cpp index 96dd0665ca..65a09d9c7b 100644 --- a/src/gui/src/PatternEditor/PatternEditorPanel.cpp +++ b/src/gui/src/PatternEditor/PatternEditorPanel.cpp @@ -124,12 +124,14 @@ PatternEditorPanel::PatternEditorPanel( QWidget *pParent ) m_pLCDSpinBoxNumerator->move( 36, 0 ); connect( m_pLCDSpinBoxNumerator, &LCDSpinBox::slashKeyPressed, this, &PatternEditorPanel::switchPatternSizeFocus ); connect( m_pLCDSpinBoxNumerator, SIGNAL( valueChanged( double ) ), this, SLOT( patternSizeChanged( double ) ) ); + m_pLCDSpinBoxNumerator->setKeyboardTracking( false ); m_pLCDSpinBoxDenominator = new LCDSpinBox( m_pSizeResol, QSize( 48, 20 ), LCDSpinBox::Type::Int, 1, 192 ); m_pLCDSpinBoxDenominator->setKind( LCDSpinBox::Kind::PatternSizeDenominator ); m_pLCDSpinBoxDenominator->move( 106, 0 ); connect( m_pLCDSpinBoxDenominator, &LCDSpinBox::slashKeyPressed, this, &PatternEditorPanel::switchPatternSizeFocus ); connect( m_pLCDSpinBoxDenominator, SIGNAL( valueChanged( double ) ), this, SLOT( patternSizeChanged( double ) ) ); + m_pLCDSpinBoxDenominator->setKeyboardTracking( false ); QLabel* label1 = new ClickableLabel( m_pSizeResol, QSize( 4, 13 ), "/", ClickableLabel::Color::Dark ); label1->resize( QSize( 20, 17 ) ); @@ -940,8 +942,11 @@ void PatternEditorPanel::patternModifiedEvent() { selectedPatternChangedEvent(); } -void PatternEditorPanel::patternChangedEvent() { - updateEditors( true ); +void PatternEditorPanel::playingPatternsChangedEvent() { + if ( Hydrogen::get_instance()->getPatternMode() == + Song::PatternMode::Stacked ) { + updateEditors( true ); + } } void PatternEditorPanel::songModeActivationEvent() { @@ -981,7 +986,6 @@ void PatternEditorPanel::updatePatternSizeLCD() { } void PatternEditorPanel::patternSizeChanged( double fValue ){ - if ( m_pPattern == nullptr ) { return; } @@ -1014,15 +1018,9 @@ void PatternEditorPanel::patternSizeChanged( double fValue ){ int nNewLength = std::round( static_cast( MAX_NOTES ) / fNewDenominator * fNewNumerator ); - // Delete all notes that are not accessible anymore. - QUndoStack* pUndoStack = HydrogenApp::get_instance()->m_pUndoStack; - pUndoStack->beginMacro( "remove excessive notes after pattern size change" ); - - pUndoStack->push( new SE_patternSizeChangedAction( nNewLength, - m_pPattern->get_length(), - fNewDenominator, - m_pPattern->get_denominator(), - m_nSelectedPatternNumber ) ); + if ( nNewLength == m_pPattern->get_length() ) { + return; + } std::vector excessiveNotes; Pattern::notes_t* pNotes = (Pattern::notes_t *)m_pPattern->get_notes(); @@ -1034,6 +1032,23 @@ void PatternEditorPanel::patternSizeChanged( double fValue ){ } } + // Delete all notes that are not accessible anymore. + QUndoStack* pUndoStack = HydrogenApp::get_instance()->m_pUndoStack; + if ( excessiveNotes.size() != 0 ) { + pUndoStack->beginMacro( QString( "Change pattern size to %1/%2 (trimming %3 notes)" ) + .arg( fNewNumerator ).arg( fNewDenominator ) + .arg( excessiveNotes.size() ) ); + } else { + pUndoStack->beginMacro( QString( "Change pattern size to %1/%2" ) + .arg( fNewNumerator ).arg( fNewDenominator ) ); + } + + pUndoStack->push( new SE_patternSizeChangedAction( nNewLength, + m_pPattern->get_length(), + fNewDenominator, + m_pPattern->get_denominator(), + m_nSelectedPatternNumber ) ); + for ( auto pNote : excessiveNotes ) { // Note is exceeding the new pattern length. It has to be // removed. diff --git a/src/gui/src/PatternEditor/PatternEditorPanel.h b/src/gui/src/PatternEditor/PatternEditorPanel.h index 2b19dd5ce5..acf96544a8 100644 --- a/src/gui/src/PatternEditor/PatternEditorPanel.h +++ b/src/gui/src/PatternEditor/PatternEditorPanel.h @@ -90,7 +90,7 @@ class PatternEditorPanel : public QWidget, protected WidgetWithScalableFont<8, virtual void selectedPatternChangedEvent() override; virtual void selectedInstrumentChangedEvent() override; virtual void patternModifiedEvent() override; - virtual void patternChangedEvent() override; + virtual void playingPatternsChangedEvent() override; virtual void drumkitLoadedEvent() override; virtual void updateSongEvent( int nValue ) override; virtual void songModeActivationEvent() override; diff --git a/src/gui/src/PatternEditor/PatternEditorRuler.cpp b/src/gui/src/PatternEditor/PatternEditorRuler.cpp index eeb86f1b3d..67c22000e5 100644 --- a/src/gui/src/PatternEditor/PatternEditorRuler.cpp +++ b/src/gui/src/PatternEditor/PatternEditorRuler.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #include @@ -119,7 +120,7 @@ void PatternEditorRuler::updatePosition( bool bForce ) { pAudioEngine->unlock(); } - int nTick = pAudioEngine->getPatternTickPosition(); + int nTick = pAudioEngine->getTransportPosition()->getPatternTickPosition(); if ( nTick != m_nTick || bForce ) { int nDiff = m_fGridWidth * (nTick - m_nTick); @@ -306,12 +307,17 @@ void PatternEditorRuler::updateEditor( bool bRedrawAll ) updatePosition(); if (bRedrawAll) { - createBackground(); + invalidateBackground(); update( 0, 0, width(), height() ); } } +void PatternEditorRuler::invalidateBackground() +{ + m_bBackgroundInvalid = true; +} + void PatternEditorRuler::createBackground() { auto pHydrogenApp = HydrogenApp::get_instance(); @@ -394,6 +400,7 @@ void PatternEditorRuler::createBackground() painter.drawLine( 0, m_nRulerHeight, m_nRulerWidth, m_nRulerHeight); painter.drawLine( m_nRulerWidth, 0, m_nRulerWidth, m_nRulerHeight ); + m_bBackgroundInvalid = false; } @@ -413,7 +420,7 @@ void PatternEditorRuler::paintEvent( QPaintEvent *ev) } qreal pixelRatio = devicePixelRatio(); - if ( pixelRatio != m_pBackgroundPixmap->devicePixelRatio() ) { + if ( pixelRatio != m_pBackgroundPixmap->devicePixelRatio() || m_bBackgroundInvalid ) { createBackground(); } @@ -516,7 +523,7 @@ void PatternEditorRuler::updateActiveRange() { if ( m_nWidthActive != nWidthActive ) { m_nWidthActive = nWidthActive; - createBackground(); + invalidateBackground(); update(); } } @@ -533,7 +540,7 @@ void PatternEditorRuler::zoomIn() updateActiveRange(); - createBackground(); + invalidateBackground(); update(); } @@ -551,7 +558,7 @@ void PatternEditorRuler::zoomOut() updateActiveRange(); - createBackground(); + invalidateBackground(); update(); } } diff --git a/src/gui/src/PatternEditor/PatternEditorRuler.h b/src/gui/src/PatternEditor/PatternEditorRuler.h index f85c361a65..aa4bca4ec5 100644 --- a/src/gui/src/PatternEditor/PatternEditorRuler.h +++ b/src/gui/src/PatternEditor/PatternEditorRuler.h @@ -80,6 +80,8 @@ class PatternEditorRuler : public QWidget, protected WidgetWithScalableFont<8, }; void createBackground(); + void invalidateBackground(); + bool m_bBackgroundInvalid; public slots: void updateEditor( bool bRedrawAll = false ); diff --git a/src/gui/src/PatternEditor/PianoRollEditor.cpp b/src/gui/src/PatternEditor/PianoRollEditor.cpp index ae582a9606..63c3b82fe5 100644 --- a/src/gui/src/PatternEditor/PianoRollEditor.cpp +++ b/src/gui/src/PatternEditor/PianoRollEditor.cpp @@ -155,7 +155,7 @@ void PianoRollEditor::paintEvent(QPaintEvent *ev) auto pPref = Preferences::get_instance(); qreal pixelRatio = devicePixelRatio(); - if ( pixelRatio != m_pBackgroundPixmap->devicePixelRatio() ) { + if ( pixelRatio != m_pBackgroundPixmap->devicePixelRatio() || m_bBackgroundInvalid ) { createBackground(); } @@ -354,6 +354,8 @@ void PianoRollEditor::createBackground() p.setPen( QPen( lineColor, 2, Qt::SolidLine ) ); p.drawLine( m_nEditorWidth, 0, m_nEditorWidth, m_nEditorHeight ); + + m_bBackgroundInvalid = false; } @@ -412,6 +414,10 @@ void PianoRollEditor::addOrRemoveNote( int nColumn, int nRealColumn, int nLine, Hydrogen *pHydrogen = Hydrogen::get_instance(); int nSelectedInstrumentnumber = pHydrogen->getSelectedInstrumentNumber(); auto pSelectedInstrument = pHydrogen->getSelectedInstrument(); + if ( pSelectedInstrument == nullptr ) { + DEBUGLOG( "No instrument selected" ); + return; + } Note* pOldNote = m_pPattern->find_note( nColumn, nRealColumn, pSelectedInstrument, notekey, octave ); @@ -444,8 +450,7 @@ void PianoRollEditor::addOrRemoveNote( int nColumn, int nRealColumn, int nLine, // hear note Preferences *pref = Preferences::get_instance(); if ( pref->getHearNewNotes() ) { - const float fPitch = pSelectedInstrument->get_pitch_offset(); - Note *pNote2 = new Note( pSelectedInstrument, 0, fVelocity, fPan, nLength, fPitch ); + Note *pNote2 = new Note( pSelectedInstrument ); pNote2->set_key_octave( notekey, octave ); m_pAudioEngine->getSampler()->noteOn( pNote2 ); } @@ -600,6 +605,10 @@ void PianoRollEditor::mouseDragStartEvent( QMouseEvent *ev ) Hydrogen *pHydrogen = Hydrogen::get_instance(); int nColumn = getColumn( ev->x() ); auto pSelectedInstrument = pHydrogen->getSelectedInstrument(); + if ( pSelectedInstrument == nullptr ) { + DEBUGLOG( "No instrument selected" ); + return; + } int nRow = std::floor(static_cast(ev->y()) / static_cast(m_nGridHeight)); @@ -664,7 +673,7 @@ void PianoRollEditor::addOrDeleteNoteAction( int nColumn, PatternList *pPatternList = pHydrogen->getSong()->getPatternList(); auto pSelectedInstrument = pSong->getInstrumentList()->get( selectedinstrument ); - if ( pSelectedInstrument ) { + if ( pSelectedInstrument == nullptr ) { ERRORLOG( QString( "Instrument [%1] could not be found" ) .arg( selectedinstrument ) ); return; @@ -702,12 +711,12 @@ void PianoRollEditor::addOrDeleteNoteAction( int nColumn, nLength = 1; } - const float fPitch = 0.f; - - if( pPattern ) { - Note *pNote = new Note( pSelectedInstrument, nPosition, fVelocity, fPan, nLength, fPitch ); + if ( pPattern != nullptr ) { + Note *pNote = new Note( pSelectedInstrument, nPosition, fVelocity, fPan, nLength ); pNote->set_note_off( noteOff ); - if(! noteOff) pNote->set_lead_lag( oldLeadLag ); + if( ! noteOff ) { + pNote->set_lead_lag( oldLeadLag ); + } pNote->set_key_octave( pressednotekey, pressedoctave ); pNote->set_probability( fProbability ); pPattern->insert_note( pNote ); @@ -808,6 +817,10 @@ void PianoRollEditor::deleteSelection() Hydrogen *pHydrogen = Hydrogen::get_instance(); int nSelectedInstrumentNumber = pHydrogen->getSelectedInstrumentNumber(); auto pSelectedInstrument = pHydrogen->getSelectedInstrument(); + if ( pSelectedInstrument == nullptr ) { + DEBUGLOG( "No instrument selected" ); + return; + } QUndoStack *pUndo = HydrogenApp::get_instance()->m_pUndoStack; validateSelection(); std::list< QUndoCommand * > actions; @@ -1209,6 +1222,10 @@ std::vector PianoRollEditor::elementsIntersecti int w = 8; int h = m_nGridHeight - 2; auto pSelectedInstrument = Hydrogen::get_instance()->getSelectedInstrument(); + if ( pSelectedInstrument == nullptr ) { + DEBUGLOG( "No instrument selected" ); + return std::move( result ); + } r = r.normalized(); if ( r.top() == r.bottom() && r.left() == r.right() ) { @@ -1250,7 +1267,7 @@ void PianoRollEditor::onPreferencesChanged( H2Core::Preferences::Changes changes { if ( changes & ( H2Core::Preferences::Changes::Colors | H2Core::Preferences::Changes::Font ) ) { - createBackground(); + invalidateBackground(); update(); } } diff --git a/src/gui/src/PlayerControl.cpp b/src/gui/src/PlayerControl.cpp index 4f925c0984..936093f4b8 100644 --- a/src/gui/src/PlayerControl.cpp +++ b/src/gui/src/PlayerControl.cpp @@ -45,6 +45,7 @@ #include #include #include +#include #include #include using namespace H2Core; @@ -340,7 +341,7 @@ PlayerControl::PlayerControl(QWidget *parent) // initialize BPM widget m_pLCDBPMSpinbox->setIsActive( m_pHydrogen->getTempoSource() == H2Core::Hydrogen::Tempo::Song ); - m_pLCDBPMSpinbox->setValue( m_pHydrogen->getAudioEngine()->getBpm() ); + m_pLCDBPMSpinbox->setValue( m_pHydrogen->getAudioEngine()->getTransportPosition()->getBpm() ); updateBPMSpinboxToolTip(); m_pRubberBPMChange = new Button( pBPMPanel, QSize( 13, 42 ), @@ -545,8 +546,9 @@ void PlayerControl::updatePlayerControl() std::shared_ptr song = m_pHydrogen->getSong(); - if ( ! m_pLCDBPMSpinbox->hasFocus() ) { - m_pLCDBPMSpinbox->setValue( m_pHydrogen->getAudioEngine()->getBpm() ); + if ( ! m_pLCDBPMSpinbox->hasFocus() && + ! m_pLCDBPMSpinbox->getIsHovered() ) { + m_pLCDBPMSpinbox->setValue( m_pHydrogen->getAudioEngine()->getTransportPosition()->getBpm() ); } //beatcounter @@ -697,7 +699,8 @@ void PlayerControl::stopBtnClicked() void PlayerControl::midiActivityEvent() { m_pMidiActivityTimer->stop(); m_pMidiActivityLED->setActivated( true ); - m_pMidiActivityTimer->start( m_midiActivityTimeout ); + m_pMidiActivityTimer->start( std::chrono::duration_cast( m_midiActivityTimeout ) + .count() ); } void PlayerControl::deactivateMidiActivityLED() { @@ -763,11 +766,14 @@ void PlayerControl::activateSongMode( bool bActivate ) { } void PlayerControl::bpmChanged( double fNewBpmValue ) { + auto pAudioEngine = m_pHydrogen->getAudioEngine(); if ( m_pLCDBPMSpinbox->getIsActive() ) { // Store it's value in the .h2song file. m_pHydrogen->getSong()->setBpm( static_cast( fNewBpmValue ) ); // Use tempo in the next process cycle of the audio engine. - m_pHydrogen->getAudioEngine()->setNextBpm( static_cast( fNewBpmValue ) ); + pAudioEngine->lock( RIGHT_HERE ); + pAudioEngine->setNextBpm( static_cast( fNewBpmValue ) ); + pAudioEngine->unlock(); } } @@ -815,7 +821,7 @@ void PlayerControl::rubberbandButtonToggle() // recalculation is just triggered if there is a tempo change // in the audio engine. pHydrogen->getAudioEngine()->lock( RIGHT_HERE ); - pHydrogen->recalculateRubberband( pHydrogen->getAudioEngine()->getBpm() ); + pHydrogen->recalculateRubberband( pHydrogen->getAudioEngine()->getTransportPosition()->getBpm() ); pHydrogen->getAudioEngine()->unlock(); pPref->setRubberBandBatchMode(true); (HydrogenApp::get_instance())->showStatusBarMessage( tr("Recalculate all samples using Rubberband ON") ); @@ -927,7 +933,7 @@ void PlayerControl::jackMasterBtnClicked() void PlayerControl::fastForwardBtnClicked() { auto pHydrogen = Hydrogen::get_instance(); - pHydrogen->getCoreActionController()->locateToColumn( pHydrogen->getAudioEngine()->getColumn() + 1 ); + pHydrogen->getCoreActionController()->locateToColumn( pHydrogen->getAudioEngine()->getTransportPosition()->getColumn() + 1 ); } @@ -935,7 +941,7 @@ void PlayerControl::fastForwardBtnClicked() void PlayerControl::rewindBtnClicked() { auto pHydrogen = Hydrogen::get_instance(); - pHydrogen->getCoreActionController()->locateToColumn( pHydrogen->getAudioEngine()->getColumn() - 1 ); + pHydrogen->getCoreActionController()->locateToColumn( pHydrogen->getAudioEngine()->getTransportPosition()->getColumn() - 1 ); } void PlayerControl::loopModeActivationEvent() { @@ -1065,7 +1071,7 @@ void PlayerControl::tempoChangedEvent( int nValue ) * Just update the GUI using the current tempo * of the song. */ - m_pLCDBPMSpinbox->setValue( m_pHydrogen->getAudioEngine()->getBpm() ); + m_pLCDBPMSpinbox->setValue( m_pHydrogen->getAudioEngine()->getTransportPosition()->getBpm() ); if ( ! bIsReadOnly ) { m_pLCDBPMSpinbox->setReadOnly( false ); diff --git a/src/gui/src/PlaylistEditor/PlaylistDialog.cpp b/src/gui/src/PlaylistEditor/PlaylistDialog.cpp index 6d70822b7b..c388d07e0c 100644 --- a/src/gui/src/PlaylistEditor/PlaylistDialog.cpp +++ b/src/gui/src/PlaylistEditor/PlaylistDialog.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -788,13 +789,13 @@ void PlaylistDialog::nodeStopBTN() void PlaylistDialog::ffWDBtnClicked() { Hydrogen* pHydrogen = Hydrogen::get_instance(); - pHydrogen->getCoreActionController()->locateToColumn( pHydrogen->getAudioEngine()->getColumn() + 1 ); + pHydrogen->getCoreActionController()->locateToColumn( pHydrogen->getAudioEngine()->getTransportPosition()->getColumn() + 1 ); } void PlaylistDialog::rewindBtnClicked() { Hydrogen* pHydrogen = Hydrogen::get_instance(); - pHydrogen->getCoreActionController()->locateToColumn( pHydrogen->getAudioEngine()->getColumn() - 1 ); + pHydrogen->getCoreActionController()->locateToColumn( pHydrogen->getAudioEngine()->getTransportPosition()->getColumn() - 1 ); } void PlaylistDialog::on_m_pPlaylistTree_itemDoubleClicked () diff --git a/src/gui/src/SampleEditor/SampleEditor.cpp b/src/gui/src/SampleEditor/SampleEditor.cpp index e2b9c51ac1..b23deeb55f 100644 --- a/src/gui/src/SampleEditor/SampleEditor.cpp +++ b/src/gui/src/SampleEditor/SampleEditor.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include @@ -175,7 +176,6 @@ void SampleEditor::closeEvent(QCloseEvent *event) std::shared_ptr SampleEditor::retrieveSample() const { auto pInstrument = Hydrogen::get_instance()->getSelectedInstrument(); - if ( pInstrument == nullptr ) { ERRORLOG( "No instrument selected" ); return nullptr; @@ -201,7 +201,6 @@ std::shared_ptr SampleEditor::retrieveSample() const { void SampleEditor::getAllFrameInfos() { auto pInstrument = Hydrogen::get_instance()->getSelectedInstrument(); - if ( pInstrument == nullptr ) { ERRORLOG( "No instrument selected" ); return; @@ -429,7 +428,7 @@ void SampleEditor::createNewLayer() pEditSample->set_velocity_envelope( *m_pTargetSampleView->get_velocity() ); pEditSample->set_pan_envelope( *m_pTargetSampleView->get_pan() ); - if( ! pEditSample->load( pAudioEngine->getBpm() ) ){ + if( ! pEditSample->load( pAudioEngine->getTransportPosition()->getBpm() ) ){ ERRORLOG( "Unable to load modified sample" ); return; } @@ -604,9 +603,6 @@ void SampleEditor::on_PlayPushButton_clicked() return; } - const float fPan = 0.f; - const int nLength = -1; - const float fPitch = 0.0f; const int selectedLayer = InstrumentEditorPanel::get_instance()->getSelectedLayer(); std::shared_ptr pSong = Hydrogen::get_instance()->getSong(); @@ -617,7 +613,15 @@ void SampleEditor::on_PlayPushButton_clicked() if ( pInstr == nullptr ) { return; } - Note *pNote = new Note( pInstr, 0, pInstr->get_component( m_nSelectedComponent )->get_layer( selectedLayer )->get_end_velocity() - 0.01, fPan, nLength, fPitch); + auto pCompo = pInstr->get_component( m_nSelectedComponent ); + if ( pCompo == nullptr ) { + return; + } + auto pLayer = pCompo->get_layer( selectedLayer ); + if ( pLayer == nullptr ) { + return; + } + Note *pNote = new Note( pInstr, 0, pLayer->get_end_velocity() - 0.01 ); pNote->set_specific_compo_id( m_nSelectedComponent ); pHydrogen->getAudioEngine()->getSampler()->noteOn(pNote); @@ -632,11 +636,11 @@ void SampleEditor::on_PlayPushButton_clicked() } - m_nRealtimeFrameEnd = pAudioEngine->getRealtimeFrames() + m_nSlframes; + m_nRealtimeFrameEnd = pAudioEngine->getRealtimeFrame() + m_nSlframes; //calculate the new rubberband sample length if( __rubberband.use ){ - m_nRealtimeFrameEndForTarget = pAudioEngine->getRealtimeFrames() + (m_nSlframes * m_fRatio + 0.1); + m_nRealtimeFrameEndForTarget = pAudioEngine->getRealtimeFrame() + (m_nSlframes * m_fRatio + 0.1); }else { m_nRealtimeFrameEndForTarget = m_nRealtimeFrameEnd; @@ -656,6 +660,10 @@ void SampleEditor::on_PlayOrigPushButton_clicked() const int selectedlayer = InstrumentEditorPanel::get_instance()->getSelectedLayer(); std::shared_ptr pSong = Hydrogen::get_instance()->getSong(); auto pInstr = pSong->getInstrumentList()->get( Hydrogen::get_instance()->getSelectedInstrumentNumber() ); + if ( pInstr == nullptr ) { + DEBUGLOG( "No instrument selected" ); + return; + } /* *preview_instrument deletes the last used preview instrument, therefore we have to construct a temporary @@ -674,13 +682,13 @@ void SampleEditor::on_PlayOrigPushButton_clicked() m_pMainSampleWaveDisplay->paintLocatorEvent( StartFrameSpinBox->value() / m_divider + 24 , true); m_pSampleAdjustView->setDetailSamplePosition( __loops.start_frame, m_fZoomfactor , nullptr); m_pTimer->start(40); // update ruler at 25 fps - m_nRealtimeFrameEnd = Hydrogen::get_instance()->getAudioEngine()->getRealtimeFrames() + m_nSlframes; + m_nRealtimeFrameEnd = Hydrogen::get_instance()->getAudioEngine()->getRealtimeFrame() + m_nSlframes; PlayOrigPushButton->setText( QString( "Stop") ); } void SampleEditor::updateMainsamplePositionRuler() { - unsigned long realpos = Hydrogen::get_instance()->getAudioEngine()->getRealtimeFrames(); + unsigned long realpos = Hydrogen::get_instance()->getAudioEngine()->getRealtimeFrame(); if ( realpos < m_nRealtimeFrameEnd ){ unsigned frame = m_nSlframes - ( m_nRealtimeFrameEnd - realpos ); if ( m_bPlayButton == true ){ @@ -703,7 +711,7 @@ void SampleEditor::updateMainsamplePositionRuler() void SampleEditor::updateTargetsamplePositionRuler() { - unsigned long realpos = Hydrogen::get_instance()->getAudioEngine()->getRealtimeFrames(); + unsigned long realpos = Hydrogen::get_instance()->getAudioEngine()->getRealtimeFrame(); unsigned targetSampleLength; if ( __rubberband.use ){ targetSampleLength = m_nSlframes * m_fRatio + 0.1; @@ -938,7 +946,7 @@ void SampleEditor::valueChangedrubberComboBox( const QString ) void SampleEditor::checkRatioSettings() { //calculate ratio - double durationtime = 60.0 / Hydrogen::get_instance()->getAudioEngine()->getBpm() + double durationtime = 60.0 / Hydrogen::get_instance()->getAudioEngine()->getTransportPosition()->getBpm() * __rubberband.divider; double induration = (double) m_nSlframes / (double) m_nSamplerate; if (induration != 0.0) m_fRatio = durationtime / induration; diff --git a/src/gui/src/SongEditor/SongEditor.cpp b/src/gui/src/SongEditor/SongEditor.cpp index afbadc53d3..fde8d6b367 100644 --- a/src/gui/src/SongEditor/SongEditor.cpp +++ b/src/gui/src/SongEditor/SongEditor.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -310,7 +311,7 @@ void SongEditor::setGridWidth( uint width ) m_nGridWidth = width; resize( SongEditor::nMargin + Preferences::get_instance()->getMaxBars() * m_nGridWidth, height() ); - createBackground(); + invalidateBackground(); update(); } } @@ -688,7 +689,7 @@ void SongEditor::focusInEvent( QFocusEvent *ev ) // If there are some patterns selected, we have to switch their // border color inactive <-> active. - createBackground(); + invalidateBackground(); update(); if ( ! HydrogenApp::get_instance()->hideKeyboardCursor() ) { @@ -704,7 +705,7 @@ void SongEditor::focusOutEvent( QFocusEvent *ev ) // If there are some patterns selected, we have to switch their // border color inactive <-> active. - createBackground(); + invalidateBackground(); update(); if ( ! HydrogenApp::get_instance()->hideKeyboardCursor() ) { @@ -923,11 +924,6 @@ void SongEditor::mouseReleaseEvent( QMouseEvent *ev ) } } -void SongEditor::patternModifiedEvent() { - createBackground(); - update(); -} - //! Modify pattern cells by first deleting some, then adding some. //! deleteCells and addCells *may* safely overlap @@ -996,6 +992,10 @@ void SongEditor::updatePosition( float fTick ) { void SongEditor::paintEvent( QPaintEvent *ev ) { + if ( m_bBackgroundInvalid ) { + createBackground(); + } + // ridisegno tutto solo se sono cambiate le note if (m_bSequenceChanged) { m_bSequenceChanged = false; @@ -1097,6 +1097,7 @@ void SongEditor::leaveEvent( QEvent *ev ) { void SongEditor::createBackground() { + m_bBackgroundInvalid = false; auto pPref = H2Core::Preferences::get_instance(); std::shared_ptr pSong = m_pHydrogen->getSong(); @@ -1163,14 +1164,11 @@ void SongEditor::createBackground() //~ celle m_bSequenceChanged = true; -} -void SongEditor::cleanUp(){ +} - delete m_pBackgroundPixmap; - m_pBackgroundPixmap = nullptr; - delete m_pSequencePixmap; - m_pSequencePixmap = nullptr; +void SongEditor::invalidateBackground() { + m_bBackgroundInvalid = true; } // Update the GridCell representation. @@ -1222,6 +1220,7 @@ QPoint SongEditor::movingGridOffset( ) const { void SongEditor::drawSequence() { QPainter p; + p.begin( m_pSequencePixmap ); p.drawPixmap( rect(), *m_pBackgroundPixmap, rect() ); p.end(); @@ -1473,29 +1472,29 @@ SongEditorPatternList::~SongEditorPatternList() } -void SongEditorPatternList::patternChangedEvent() { - createBackground(); +void SongEditorPatternList::playingPatternsChangedEvent() { + invalidateBackground(); update(); } void SongEditorPatternList::setRowSelection( RowSelection rowSelection ) { m_rowSelection = rowSelection; - createBackground(); + invalidateBackground(); update(); } void SongEditorPatternList::patternModifiedEvent() { - createBackground(); + invalidateBackground(); update(); } void SongEditorPatternList::selectedPatternChangedEvent() { - createBackground(); + invalidateBackground(); update(); } -void SongEditorPatternList::stackedPatternsChangedEvent() { - createBackground(); +void SongEditorPatternList::nextPatternsChangedEvent() { + invalidateBackground(); update(); } @@ -1557,7 +1556,7 @@ void SongEditorPatternList::mousePressEvent( QMouseEvent *ev ) } } - createBackground(); + invalidateBackground(); update(); } @@ -1568,7 +1567,7 @@ void SongEditorPatternList::mousePressEvent( QMouseEvent *ev ) void SongEditorPatternList::togglePattern( int row ) { m_pHydrogen->toggleNextPattern( row ); - createBackground(); + invalidateBackground(); update(); } @@ -1638,7 +1637,8 @@ void SongEditorPatternList::paintEvent( QPaintEvent *ev ) qreal pixelRatio = devicePixelRatio(); if ( width() != m_pBackgroundPixmap->width() || height() != m_pBackgroundPixmap->height() || - pixelRatio != m_pBackgroundPixmap->devicePixelRatio() ) { + pixelRatio != m_pBackgroundPixmap->devicePixelRatio() || + m_bBackgroundInvalid ) { createBackground(); } QRectF srcRect( @@ -1690,14 +1690,20 @@ void SongEditorPatternList::songModeActivationEvent() { // Refresh pattern list display if in stacked mode if ( Hydrogen::get_instance()->getPatternMode() == Song::PatternMode::Stacked ) { - createBackground(); + invalidateBackground(); update(); } } +void SongEditorPatternList::invalidateBackground() +{ + m_bBackgroundInvalid = true; +} + void SongEditorPatternList::createBackground() { auto pPref = H2Core::Preferences::get_instance(); + m_bBackgroundInvalid = false; QFont boldTextFont( pPref->getLevel2FontFamily(), getPointSize( pPref->getFontSize() ) ); boldTextFont.setBold( true ); @@ -1822,7 +1828,7 @@ void SongEditorPatternList::createBackground() } void SongEditorPatternList::stackedModeActivationEvent( int ) { - createBackground(); + invalidateBackground(); update(); } @@ -2025,7 +2031,7 @@ void SongEditorPatternList::acceptPatternPropertiesDialogSettings(QString newPat pattern->set_category( newPatternCategory ); pHydrogen->setIsModified( true ); EventQueue::get_instance()->push_event( EVENT_PATTERN_MODIFIED, -1 ); - createBackground(); + invalidateBackground(); update(); } @@ -2040,7 +2046,7 @@ void SongEditorPatternList::revertPatternPropertiesDialogSettings(QString oldPat pattern->set_category( oldPatternCategory ); pHydrogen->setIsModified( true ); EventQueue::get_instance()->push_event( EVENT_PATTERN_MODIFIED, -1 ); - createBackground(); + invalidateBackground(); update(); } @@ -2339,7 +2345,7 @@ void SongEditorPatternList::movePatternLine( int nSourcePattern , int nTargetPat void SongEditorPatternList::leaveEvent( QEvent* ev ) { UNUSED( ev ); m_nRowHovered = -1; - createBackground(); + invalidateBackground(); update(); } @@ -2348,7 +2354,7 @@ void SongEditorPatternList::mouseMoveEvent(QMouseEvent *event) // Update the highlighting of the hovered row. if ( event->pos().y() / m_nGridHeight != m_nRowHovered ) { m_nRowHovered = event->pos().y() / m_nGridHeight; - createBackground(); + invalidateBackground(); update(); } @@ -2396,7 +2402,7 @@ void SongEditorPatternList::onPreferencesChanged( H2Core::Preferences::Changes c if ( changes & ( H2Core::Preferences::Changes::Colors | H2Core::Preferences::Changes::Font ) ) { - createBackground(); + invalidateBackground(); update(); } } @@ -2466,7 +2472,7 @@ void SongEditorPositionRuler::relocationEvent() { void SongEditorPositionRuler::songSizeChangedEvent() { m_nActiveColumns = m_pHydrogen->getSong()->getPatternGroupVector()->size(); - createBackground(); + invalidateBackground(); update(); } @@ -2482,11 +2488,14 @@ void SongEditorPositionRuler::setGridWidth( uint width ) m_nGridWidth = width; resize( SongEditor::nMargin + Preferences::get_instance()->getMaxBars() * m_nGridWidth, height() ); - createBackground(); + invalidateBackground(); update(); } } +void SongEditorPositionRuler::invalidateBackground() { + m_bBackgroundInvalid = true; +} void SongEditorPositionRuler::createBackground() { @@ -2608,6 +2617,8 @@ void SongEditorPositionRuler::createBackground() p.drawLine( 0, 0, width(), 0 ); p.drawLine( 0, height() - 25, width(), height() - 25 ); p.drawLine( 0, height(), width(), height() ); + + m_bBackgroundInvalid = false; } void SongEditorPositionRuler::tempoChangedEvent( int ) { @@ -2623,7 +2634,7 @@ void SongEditorPositionRuler::tempoChangedEvent( int ) { return; } - createBackground(); + invalidateBackground(); update(); } @@ -2633,7 +2644,7 @@ void SongEditorPositionRuler::patternModifiedEvent() { update(); } -void SongEditorPositionRuler::patternChangedEvent() { +void SongEditorPositionRuler::playingPatternsChangedEvent() { // Triggered every time the column of the SongEditor grid // changed. Either by rolling transport or by relocation. update(); @@ -2702,17 +2713,17 @@ bool SongEditorPositionRuler::event( QEvent* ev ) { void SongEditorPositionRuler::songModeActivationEvent() { updatePosition(); - createBackground(); + invalidateBackground(); update(); } void SongEditorPositionRuler::timelineActivationEvent() { - createBackground(); + invalidateBackground(); update(); } void SongEditorPositionRuler::jackTimebaseStateChangedEvent() { - createBackground(); + invalidateBackground(); update(); } @@ -2831,6 +2842,10 @@ void SongEditorPositionRuler::paintEvent( QPaintEvent *ev ) auto pTimeline = m_pHydrogen->getTimeline(); auto pPref = Preferences::get_instance(); auto tempoMarkerVector = pTimeline->getAllTempoMarkers(); + + if ( m_bBackgroundInvalid ) { + createBackground(); + } if (!isVisible()) { return; @@ -3111,20 +3126,22 @@ void SongEditorPositionRuler::updatePosition() auto pPref = Preferences::get_instance(); auto tempoMarkerVector = pTimeline->getAllTempoMarkers(); - float fTick = m_pAudioEngine->getColumn(); - m_pAudioEngine->lock( RIGHT_HERE ); auto pPatternGroupVector = m_pHydrogen->getSong()->getPatternGroupVector(); - m_nColumn = std::max( m_pAudioEngine->getColumn(), 0 ); + m_nColumn = std::max( m_pAudioEngine->getTransportPosition()->getColumn(), 0 ); + + float fTick = static_cast(m_nColumn); if ( pPatternGroupVector->size() > m_nColumn && pPatternGroupVector->at( m_nColumn )->size() > 0 ) { int nLength = pPatternGroupVector->at( m_nColumn )->longest_pattern_length(); - fTick += (float)m_pAudioEngine->getPatternTickPosition() / (float)nLength; + fTick += (float)m_pAudioEngine->getTransportPosition()->getPatternTickPosition() / + (float)nLength; } else { // Empty column. Use the default length. - fTick += (float)m_pAudioEngine->getPatternTickPosition() / (float)MAX_NOTES; + fTick += (float)m_pAudioEngine->getTransportPosition()->getPatternTickPosition() / + (float)MAX_NOTES; } if ( m_pHydrogen->getMode() == Song::Mode::Pattern ) { @@ -3167,7 +3184,7 @@ void SongEditorPositionRuler::updatePosition() void SongEditorPositionRuler::timelineUpdateEvent( int nValue ) { - createBackground(); + invalidateBackground(); update(); } @@ -3178,7 +3195,7 @@ void SongEditorPositionRuler::onPreferencesChanged( H2Core::Preferences::Changes resize( SongEditor::nMargin + Preferences::get_instance()->getMaxBars() * m_nGridWidth, height() ); - createBackground(); + invalidateBackground(); update(); } } diff --git a/src/gui/src/SongEditor/SongEditor.h b/src/gui/src/SongEditor/SongEditor.h index 102abc84a4..6c6d56463b 100644 --- a/src/gui/src/SongEditor/SongEditor.h +++ b/src/gui/src/SongEditor/SongEditor.h @@ -84,9 +84,8 @@ class SongEditor : public QWidget ~SongEditor(); void createBackground(); - void updatePosition( float fTick ); - - void cleanUp(); + void invalidateBackground(); + void updatePosition( float fTick ); int getGridWidth (); void setGridWidth( uint width); @@ -144,6 +143,8 @@ class SongEditor : public QWidget QMenu * m_pPopupMenu; + bool m_bBackgroundInvalid; + //! @name Background pixmap caching //! @@ -216,7 +217,6 @@ class SongEditor : public QWidget /** Cached position of the playhead.*/ float m_fTick; public: - void patternModifiedEvent() override; //! @name Selection interfaces //! see Selection.h for details. @@ -270,6 +270,7 @@ class SongEditorPatternList : public QWidget void updateEditor(); void createBackground(); + void invalidateBackground(); void movePatternLine( int, int ); void acceptPatternPropertiesDialogSettings( QString newPatternName, QString newPatternInfo, QString newPatternCategory, int patternNr ); void revertPatternPropertiesDialogSettings(QString oldPatternName, QString oldPatternInfo, QString oldPatternCategory, int patternNr); @@ -277,11 +278,11 @@ class SongEditorPatternList : public QWidget int getGridHeight() { return m_nGridHeight; } virtual void patternModifiedEvent() override; - virtual void patternChangedEvent() override; + virtual void playingPatternsChangedEvent() override; virtual void songModeActivationEvent() override; virtual void stackedModeActivationEvent( int nValue ) override; virtual void selectedPatternChangedEvent() override; - virtual void stackedPatternsChangedEvent() override; + virtual void nextPatternsChangedEvent() override; public slots: void patternPopup_edit(); @@ -308,6 +309,7 @@ class SongEditorPatternList : public QWidget static const uint m_nInitialHeight = 10; QPixmap * m_pBackgroundPixmap; + bool m_bBackgroundInvalid; QPixmap m_labelBackgroundLight; QPixmap m_labelBackgroundDark; @@ -364,7 +366,7 @@ class SongEditorPositionRuler : public QWidget, protected WidgetWithScalableFon uint getGridWidth(); void setGridWidth (uint width); virtual void tempoChangedEvent( int ) override; - virtual void patternChangedEvent() override; + virtual void playingPatternsChangedEvent() override; virtual void songModeActivationEvent() override; virtual void relocationEvent() override; virtual void songSizeChangedEvent() override; @@ -382,6 +384,7 @@ class SongEditorPositionRuler : public QWidget, protected WidgetWithScalableFon void showBpmWidget( int nColumn ); void onPreferencesChanged( H2Core::Preferences::Changes changes ); void createBackground(); + void invalidateBackground(); private: H2Core::Hydrogen* m_pHydrogen; @@ -419,6 +422,7 @@ class SongEditorPositionRuler : public QWidget, protected WidgetWithScalableFon int m_nActiveColumns; QPixmap * m_pBackgroundPixmap; + bool m_bBackgroundInvalid; bool m_bRightBtnPressed; virtual void mouseMoveEvent(QMouseEvent *ev) override; diff --git a/src/gui/src/SongEditor/SongEditorPanel.cpp b/src/gui/src/SongEditor/SongEditorPanel.cpp index aec9075189..e95359cff0 100644 --- a/src/gui/src/SongEditor/SongEditorPanel.cpp +++ b/src/gui/src/SongEditor/SongEditorPanel.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -449,7 +450,7 @@ void SongEditorPanel::updatePlayHeadPosition() QPoint pos = m_pPositionRuler->pos(); int x = -pos.x(); - int nPlayHeadPosition = pAudioEngine->getColumn() * + int nPlayHeadPosition = pAudioEngine->getTransportPosition()->getColumn() * m_pSongEditor->getGridWidth(); int value = m_pEditorScrollView->horizontalScrollBar()->value(); @@ -548,12 +549,10 @@ void SongEditorPanel::updateAll() auto pHydrogen = Hydrogen::get_instance(); auto pSong = pHydrogen->getSong(); - m_pPatternList->createBackground(); + m_pPatternList->invalidateBackground(); m_pPatternList->update(); - m_pSongEditor->cleanUp(); - - m_pSongEditor->createBackground(); + m_pSongEditor->invalidateBackground(); m_pSongEditor->update(); updatePositionRuler(); @@ -610,7 +609,7 @@ void SongEditorPanel::updatePlaybackTrackIfNecessary() void SongEditorPanel::updatePositionRuler() { - m_pPositionRuler->createBackground(); + m_pPositionRuler->invalidateBackground(); } /// @@ -980,12 +979,22 @@ void SongEditorPanel::zoomOutBtnClicked() void SongEditorPanel::faderChanged( WidgetWithInput *pRef ) { - Hydrogen * pHydrogen = Hydrogen::get_instance(); - Fader* pFader = dynamic_cast( pRef ); - std::shared_ptr pSong = pHydrogen->getSong(); + auto pHydrogen = Hydrogen::get_instance(); + auto pSong = pHydrogen->getSong(); + + if ( pSong == nullptr ) { + return; + } - if( pSong ){ - pSong->setPlaybackTrackVolume( pFader->getValue() ); + Fader* pFader = dynamic_cast( pRef ); + const float fNewValue = std::round( pFader->getValue() * 100 ) / 100; + + if ( pSong->getPlaybackTrackVolume() != fNewValue ) { + pSong->setPlaybackTrackVolume( fNewValue ); + HydrogenApp::get_instance()->showStatusBarMessage( + tr( "Playback volume set to" ) + .append( QString( " [%1]" ).arg( fNewValue ) ), + "SongEditorPanel:PlaybackTrackVolume" ); } } @@ -1140,7 +1149,7 @@ void SongEditorPanel::gridCellToggledEvent() { updateAll(); } -void SongEditorPanel::patternChangedEvent() { +void SongEditorPanel::playingPatternsChangedEvent() { // Triggered every time the column of the SongEditor grid // changed. Either by rolling transport or by relocation. // In Song mode, we may scroll to change position in the Song Editor. diff --git a/src/gui/src/SongEditor/SongEditorPanel.h b/src/gui/src/SongEditor/SongEditorPanel.h index 52ff3a87f8..d150eef463 100644 --- a/src/gui/src/SongEditor/SongEditorPanel.h +++ b/src/gui/src/SongEditor/SongEditorPanel.h @@ -98,7 +98,7 @@ class SongEditorPanel : public QWidget, public EventListener, public H2Core::O virtual void jackTimebaseStateChangedEvent() override; - virtual void patternChangedEvent() override; + virtual void playingPatternsChangedEvent() override; virtual void patternEditorLockedEvent() override; virtual void stackedModeActivationEvent( int ) override; diff --git a/src/gui/src/SoundLibrary/SoundLibraryPanel.cpp b/src/gui/src/SoundLibrary/SoundLibraryPanel.cpp index 4425bc39bf..c388cd79ca 100644 --- a/src/gui/src/SoundLibrary/SoundLibraryPanel.cpp +++ b/src/gui/src/SoundLibrary/SoundLibraryPanel.cpp @@ -602,6 +602,10 @@ QString SoundLibraryPanel::getDrumkitPath( const QString& sDrumkitLabel ) const void SoundLibraryPanel::change_background_color() { auto pSelectedInstrument = Hydrogen::get_instance()->getSelectedInstrument(); + if ( pSelectedInstrument == nullptr ) { + DEBUGLOG( "No instrument selected" ); + return; + } QString sDrumkitPath = pSelectedInstrument->get_drumkit_path(); QString sDrumkitLabel = getDrumkitLabel( sDrumkitPath ); diff --git a/src/gui/src/Widgets/Button.cpp b/src/gui/src/Widgets/Button.cpp index b06ec5ce34..510936398c 100644 --- a/src/gui/src/Widgets/Button.cpp +++ b/src/gui/src/Widgets/Button.cpp @@ -316,6 +316,9 @@ void Button::setAction( std::shared_ptr pAction ) { } void Button::mousePressEvent(QMouseEvent*ev) { + if ( ev->button() == Qt::RightButton ) { + emit rightClicked(); + } /* * Shift + Left-Click activate the midi learn widget diff --git a/src/gui/src/Widgets/Button.h b/src/gui/src/Widgets/Button.h index 09dd7222f8..21212d0286 100644 --- a/src/gui/src/Widgets/Button.h +++ b/src/gui/src/Widgets/Button.h @@ -145,9 +145,7 @@ private slots: void onClick(); signals: - void clicked(Button *pBtn); - void rightClicked(Button *pBtn); - void mousePress(Button *pBtn); + void rightClicked(); private: void updateStyleSheet(); diff --git a/src/gui/src/Widgets/LCDSpinBox.cpp b/src/gui/src/Widgets/LCDSpinBox.cpp index 32c3ca6399..5fe7f9c3ba 100644 --- a/src/gui/src/Widgets/LCDSpinBox.cpp +++ b/src/gui/src/Widgets/LCDSpinBox.cpp @@ -122,7 +122,14 @@ void LCDSpinBox::wheelEvent( QWheelEvent *ev ) { void LCDSpinBox::keyPressEvent( QKeyEvent *ev ) { double fOldValue = value(); - + + // Pass Undo/Redo commands up to the parent + if ( ev->matches( QKeySequence::StandardKey::Undo ) + || ev->matches( QKeySequence::StandardKey::Redo ) ) { + ev->ignore(); + return; + } + if ( m_kind == Kind::PatternSizeDenominator && ( ev->key() == Qt::Key_Up || ev->key() == Qt::Key_Down || ev->key() == Qt::Key_PageUp || ev->key() == Qt::Key_PageDown ) ) { diff --git a/src/gui/src/Widgets/LCDSpinBox.h b/src/gui/src/Widgets/LCDSpinBox.h index 229be2f7df..09317042d0 100644 --- a/src/gui/src/Widgets/LCDSpinBox.h +++ b/src/gui/src/Widgets/LCDSpinBox.h @@ -80,6 +80,8 @@ class LCDSpinBox : public QDoubleSpinBox, public H2Core::Object void setSize( QSize size ); + bool getIsHovered() const; + public slots: void onPreferencesChanged( H2Core::Preferences::Changes changes ); void setValue( double fValue ); @@ -130,4 +132,7 @@ inline void LCDSpinBox::setKind( Kind kind ) { inline bool LCDSpinBox::getIsActive() const { return m_bIsActive; } +inline bool LCDSpinBox::getIsHovered() const { + return m_bEntered; +} #endif diff --git a/src/gui/src/Widgets/LED.cpp b/src/gui/src/Widgets/LED.cpp index b9ec597c80..6b1b50dd36 100644 --- a/src/gui/src/Widgets/LED.cpp +++ b/src/gui/src/Widgets/LED.cpp @@ -127,7 +127,7 @@ void MetronomeLED::metronomeEvent( int nValue ) { update(); - m_pTimer->start( m_activityTimeout ); + m_pTimer->start( std::chrono::duration_cast( m_activityTimeout ).count() ); } void MetronomeLED::turnOff() { diff --git a/src/gui/src/Widgets/MidiTable.cpp b/src/gui/src/Widgets/MidiTable.cpp index 51d7e10514..8f803ec8f4 100644 --- a/src/gui/src/Widgets/MidiTable.cpp +++ b/src/gui/src/Widgets/MidiTable.cpp @@ -368,5 +368,11 @@ void MidiTable::updateRow( int nRow ) { } else { ERRORLOG( QString( "Unable to find MIDI action [%1]" ).arg( sActionType ) ); } + + // Relative changes should allow for both increasing and + // decreasing the pattern number. + if ( sActionType == "SELECT_NEXT_PATTERN_RELATIVE" ) { + pActionSpinner1->setMinimum( -1 * pActionSpinner1->maximum() ); + } } } diff --git a/src/gui/src/main.cpp b/src/gui/src/main.cpp index a957af1717..160f83abdf 100644 --- a/src/gui/src/main.cpp +++ b/src/gui/src/main.cpp @@ -531,11 +531,6 @@ int main(int argc, char *argv[]) // Tell the core that the GUI is now fully loaded and ready. pHydrogen->setGUIState( H2Core::Hydrogen::GUIState::ready ); -#ifdef H2CORE_HAVE_OSC - if ( NsmClient::get_instance() != nullptr ) { - NsmClient::get_instance()->sendDirtyState( false ); - } -#endif if ( sShotList != QString() ) { ShotList *sl = new ShotList( sShotList ); @@ -549,7 +544,24 @@ int main(int argc, char *argv[]) // the previous session. if ( pHydrogen->getSong()->getFilename() != H2Core::Filesystem::empty_song_path() ) { +#ifdef H2CORE_HAVE_OSC + // Mark empty song created in a new NSM session modified + // in order to emphasis that an initial song save is + // required to generate the song file and link the + // associated drumkit in the session folder. + if ( NsmClient::get_instance() != nullptr && + NsmClient::get_instance()->getIsNewSession() ) { + + NsmClient::get_instance()->sendDirtyState( true ); + pHydrogen->setIsModified( true ); + } + else { + NsmClient::get_instance()->sendDirtyState( false ); + pHydrogen->setIsModified( false ); + } +#else pHydrogen->setIsModified( false ); +#endif } H2Core::Logger::setCrashContext( nullptr ); diff --git a/src/player/main.cpp b/src/player/main.cpp index 4ea2bcca79..600a6d711b 100644 --- a/src/player/main.cpp +++ b/src/player/main.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -119,7 +120,8 @@ int main(int argc, char** argv){ break; case 'f': - cout << "Frames = " << hydrogen->getAudioEngine()->getFrames() << endl; + cout << "Frame = " << hydrogen->getAudioEngine()-> + getTransportPosition()->getFrame() << endl; break; case 'd': diff --git a/src/tests/AdsrTest.cpp b/src/tests/AdsrTest.cpp index 2404269edf..c3ca8cee5d 100644 --- a/src/tests/AdsrTest.cpp +++ b/src/tests/AdsrTest.cpp @@ -90,6 +90,7 @@ static void checkAllEqual( float *pfA, float fValue, int nFrames ) { /* Test basic ADSR functionality: apply an ADSR envelope to DC data, and check the properties of each phase. */ void ADSRTest::testBasicADSR() { + ___INFOLOG( "" ); const int N = 256; const float fSustain = 0.75; float a[5*N], b[5*N]; @@ -127,11 +128,13 @@ void ADSRTest::testBasicADSR() { for ( int n = 4*N; n < 5*N - 1; n++ ) { CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE( "idle", 0.0, a[n], delta ); } + ___INFOLOG( "passed" ); } /* Test that we get an equivalent envelope when it's computed in chunks rather than all at once. */ void ADSRTest::testBufferChunks() { + ___INFOLOG( "" ); const int N = 256; const int nChunk = 16; const float fSustain = 0.75; @@ -156,11 +159,13 @@ void ADSRTest::testBufferChunks() { checkEqual( a + n, c + n, nChunk ); } } + ___INFOLOG( "passed" ); } void ADSRTest::testEarlyRelease() { + ___INFOLOG( "" ); const int N = 256; const float fSustain = 0.75; float a[5*N], b[5*N]; @@ -221,11 +226,13 @@ void ADSRTest::testEarlyRelease() { /* Idle */ checkAllEqual( a + nReleaseEnd, 0.0, 5 * N - nReleaseEnd ); + ___INFOLOG( "passed" ); } void ADSRTest::testAttack() { + ___INFOLOG( "" ); m_adsr->attack(); /* Attack */ @@ -240,11 +247,13 @@ void ADSRTest::testAttack() /* Sustain */ CPPUNIT_ASSERT_DOUBLES_EQUAL( 0.8, getValue( 4.0 ), delta ); + ___INFOLOG( "passed" ); } void ADSRTest::testRelease() { + ___INFOLOG( "" ); getValue( 1.1 ); // move past Attack getValue( 2.1 ); // move past Decay getValue( 0.1 ); // calculate and store sustain @@ -259,4 +268,5 @@ void ADSRTest::testRelease() /* Idle */ CPPUNIT_ASSERT_DOUBLES_EQUAL( 0.0, getValue( 2.0 ), delta ); + ___INFOLOG( "passed" ); } diff --git a/src/tests/AudioBenchmark.cpp b/src/tests/AudioBenchmark.cpp index 75e78ddc02..84ecbeca9b 100644 --- a/src/tests/AudioBenchmark.cpp +++ b/src/tests/AudioBenchmark.cpp @@ -25,6 +25,8 @@ #include #include #include +#include +#include #include #include #include @@ -54,7 +56,7 @@ static long long exportCurrentSong( const QString &fileName, int nSampleRate ) pHydrogen->startExportSession( nSampleRate, 16 ); pHydrogen->startExportSong( fileName ); - long long nStartFrames = pHydrogen->getAudioEngine()->getFrames(); + long long nStartFrame = pHydrogen->getAudioEngine()->getTransportPosition()->getFrame(); bool done = false; while ( ! done ) { @@ -70,7 +72,7 @@ static long long exportCurrentSong( const QString &fileName, int nSampleRate ) pHydrogen->stopExportSession(); - return pHydrogen->getAudioEngine()->getFrames() - nStartFrames; + return pHydrogen->getAudioEngine()->getTransportPosition()->getFrame() - nStartFrame; } static QString showNumber( double f ) { @@ -167,6 +169,7 @@ void AudioBenchmark::audioBenchmark(void) if ( !bEnabled ) { return; } + ___INFOLOG( "" ); Hydrogen *pHydrogen = Hydrogen::get_instance(); qDebug() << "Benchmark ADSR method:"; @@ -215,4 +218,5 @@ void AudioBenchmark::audioBenchmark(void) timeExport( 48000 ); qDebug() << "---"; + ___INFOLOG( "passed" ); } diff --git a/src/tests/AutomationPathSerializerTest.cpp b/src/tests/AutomationPathSerializerTest.cpp index 45252b1c0f..3dd64bffb6 100644 --- a/src/tests/AutomationPathSerializerTest.cpp +++ b/src/tests/AutomationPathSerializerTest.cpp @@ -41,6 +41,7 @@ class AutomationPathSerializerTest : public CppUnit::TestCase { void testRead() { + ___INFOLOG( "" ); QDomDocument doc; QString xml = ""; CPPUNIT_ASSERT(doc.setContent(xml, false)); @@ -56,11 +57,13 @@ class AutomationPathSerializerTest : public CppUnit::TestCase { CPPUNIT_ASSERT_EQUAL(expect, path); CPPUNIT_ASSERT_EQUAL(4.0f, path.get_value(2.0f)); CPPUNIT_ASSERT_EQUAL(-2.0f, path.get_value(4.0f)); + ___INFOLOG( "passed" ); } void testWrite() { + ___INFOLOG( "" ); AutomationPath path(-1, 1, 0); path.add_point(0.0f, 0.0f); path.add_point(1.0f, 1.0f); @@ -82,11 +85,13 @@ class AutomationPathSerializerTest : public CppUnit::TestCase { expect.toString(0).toStdString(), doc.toString(0).toStdString() ); + ___INFOLOG( "passed" ); } void testRoundtripReadWrite() { + ___INFOLOG( "" ); AutomationPath p1(0, 10, 0); p1.add_point(0.0f, 4.0f); p1.add_point(1.0f, 8.0f); @@ -104,6 +109,7 @@ class AutomationPathSerializerTest : public CppUnit::TestCase { serializer.read_automation_path(node, p2); CPPUNIT_ASSERT_EQUAL(p1, p2); + ___INFOLOG( "passed" ); } }; diff --git a/src/tests/AutomationPathTest.cpp b/src/tests/AutomationPathTest.cpp index c27081da35..b61d96c056 100644 --- a/src/tests/AutomationPathTest.cpp +++ b/src/tests/AutomationPathTest.cpp @@ -70,6 +70,7 @@ class AutomationPathTest : public CppUnit::TestCase { /* Test whether AutomationPaths are constructed correctly */ void testConstruction() { + ___INFOLOG( "" ); AutomationPath p(0.2f, 0.8f, 0.6f); CPPUNIT_ASSERT(p.empty()); @@ -88,6 +89,7 @@ class AutomationPathTest : public CppUnit::TestCase { 0.6, static_cast(p.get_default()), delta); + ___INFOLOG( "passed" ); } @@ -95,6 +97,7 @@ class AutomationPathTest : public CppUnit::TestCase { default value */ void testEmptyPath() { + ___INFOLOG( "" ); AutomationPath p1(0.0f, 1.0f, 0.0f); CPPUNIT_ASSERT_DOUBLES_EQUAL( @@ -128,12 +131,14 @@ class AutomationPathTest : public CppUnit::TestCase { 1.0, static_cast(p3.get_value(7.0f)), delta); + ___INFOLOG( "passed" ); } /* Test getting value of an anchor point */ void testOnePoint() { + ___INFOLOG( "" ); AutomationPath p(0.0f, 1.0f, 1.0f); p.add_point(1.0f, 0.5f); @@ -142,6 +147,7 @@ class AutomationPathTest : public CppUnit::TestCase { 0.5, static_cast(p.get_value(1.0f)), delta); + ___INFOLOG( "passed" ); } @@ -149,6 +155,7 @@ class AutomationPathTest : public CppUnit::TestCase { i.e if returned value is defined by first point */ void testValueBeforeFirstPoint() { + ___INFOLOG( "" ); AutomationPath p(0.0f, 1.0f, 1.0f); p.add_point(1.0f, 0.5f); @@ -160,6 +167,7 @@ class AutomationPathTest : public CppUnit::TestCase { 0.5, static_cast(p.get_value(0.0f)), delta); + ___INFOLOG( "passed" ); } @@ -167,6 +175,7 @@ class AutomationPathTest : public CppUnit::TestCase { is defined by that value */ void testValueAfterLastPoint() { + ___INFOLOG( "" ); AutomationPath p(0.0f, 1.0f, 1.0f); p.add_point(1.0f, 0.4f); @@ -176,12 +185,14 @@ class AutomationPathTest : public CppUnit::TestCase { 0.6, static_cast(p.get_value(3.0f)), delta); + ___INFOLOG( "passed" ); } /* Test getting value between two anchor points */ void testMidpointValue() { + ___INFOLOG( "" ); AutomationPath p(0.0f, 1.0f, 1.0f); p.add_point(1.0f, 0.2f); @@ -191,21 +202,25 @@ class AutomationPathTest : public CppUnit::TestCase { 0.3, static_cast(p.get_value(1.5f)), delta); + ___INFOLOG( "passed" ); } /* Test operator== and operator!= */ void testEmptyPathsEqual() { + ___INFOLOG( "" ); AutomationPath p1(-2.0f, 2.0f, 1.0f); AutomationPath p2(-2.0f, 2.0f, 1.0f); CPPUNIT_ASSERT(p1 == p2); CPPUNIT_ASSERT(!(p1 != p2)); + ___INFOLOG( "passed" ); } void testPathsEqual() { + ___INFOLOG( "" ); AutomationPath p1(-4.0f, 3.0f, 1.5f); p1.add_point(1.0f, 0.0f); p1.add_point(2.0f, 2.0f); @@ -216,19 +231,23 @@ class AutomationPathTest : public CppUnit::TestCase { CPPUNIT_ASSERT(p1 == p2); CPPUNIT_ASSERT(!(p1 != p2)); + ___INFOLOG( "passed" ); } void testEmptyPathsNotEqual() { + ___INFOLOG( "" ); AutomationPath p1(-2.0f, 2.0f, 1.0f); AutomationPath p2(-1.0f, 1.0f, 0.0f); CPPUNIT_ASSERT(p1 != p2); CPPUNIT_ASSERT(!(p1 == p2)); + ___INFOLOG( "passed" ); } void testPathsNotEqual() { + ___INFOLOG( "" ); AutomationPath p1(-2.0f, 2.0f, 1.0f); p1.add_point(1.0f, 0.0f); @@ -237,10 +256,12 @@ class AutomationPathTest : public CppUnit::TestCase { CPPUNIT_ASSERT(p1 != p2); CPPUNIT_ASSERT(!(p1 == p2)); + ___INFOLOG( "passed" ); } void testIterator() { + ___INFOLOG( "" ); typedef std::pair pair; AutomationPath p(0.0f, 4.0f, 1.0f); p.add_point(0.0f, 0.0f); @@ -261,11 +282,13 @@ class AutomationPathTest : public CppUnit::TestCase { i++; CPPUNIT_ASSERT(i == p.end()); + ___INFOLOG( "passed" ); } void testFindPointInEmptyPath() { + ___INFOLOG( "" ); AutomationPath p(0.0f, 1.0f, 1.0f); auto iter = p.find(0.0f); @@ -273,10 +296,12 @@ class AutomationPathTest : public CppUnit::TestCase { auto iter2 = p.find(22.0f); CPPUNIT_ASSERT(iter2 == p.end()); + ___INFOLOG( "passed" ); } void testFindPoint() { + ___INFOLOG( "" ); AutomationPath p(0.0f, 1.0f, 1.0f); p.add_point(4.0f, 0.5f); @@ -288,11 +313,13 @@ class AutomationPathTest : public CppUnit::TestCase { auto iter3 = p.find(3.6f); CPPUNIT_ASSERT(iter3 == p.begin()); + ___INFOLOG( "passed" ); } void testFindNotFound() { + ___INFOLOG( "" ); AutomationPath p(0.0f, 1.0f, 1.0f); p.add_point(2.0f, 0.2f); @@ -301,11 +328,13 @@ class AutomationPathTest : public CppUnit::TestCase { auto iter2 = p.find(2.6f); CPPUNIT_ASSERT(iter2 == p.end()); + ___INFOLOG( "passed" ); } void testMovePoint() { + ___INFOLOG( "" ); typedef std::pair pair; AutomationPath p(0.0f, 1.0f, 1.0f); p.add_point(5.0f, 0.5f); @@ -318,11 +347,13 @@ class AutomationPathTest : public CppUnit::TestCase { pair(6.0f, 1.0f), *out ); + ___INFOLOG( "passed" ); } void testRemovePoint() { + ___INFOLOG( "" ); AutomationPath p(1.0f, 1.0f, 1.0f); p.add_point(0.0f, 0.0f); @@ -335,5 +366,6 @@ class AutomationPathTest : public CppUnit::TestCase { static_cast(p.get_value(0.0f)), delta); + ___INFOLOG( "passed" ); } }; diff --git a/src/tests/CoreActionControllerTest.cpp b/src/tests/CoreActionControllerTest.cpp index c9d341dcb5..994863a980 100644 --- a/src/tests/CoreActionControllerTest.cpp +++ b/src/tests/CoreActionControllerTest.cpp @@ -53,6 +53,7 @@ void CoreActionControllerTest::tearDown() { } void CoreActionControllerTest::testSessionManagement() { + ___INFOLOG( "" ); // --------------------------------------------------------------- // Test CoreActionController::newSong() @@ -126,9 +127,11 @@ void CoreActionControllerTest::testSessionManagement() { // --------------------------------------------------------------- CPPUNIT_ASSERT( fileProperName.remove() ); + ___INFOLOG( "passed" ); } void CoreActionControllerTest::testIsSongPathValid() { + ___INFOLOG( "" ); // Is not absolute. CPPUNIT_ASSERT( !Filesystem::isSongPathValid( "test.h2song" ) ); @@ -139,4 +142,5 @@ void CoreActionControllerTest::testIsSongPathValid() { QString sValidPath = QString( "%1/test.h2song" ).arg( QDir::tempPath() ); CPPUNIT_ASSERT( Filesystem::isSongPathValid( sValidPath ) ); + ___INFOLOG( "passed" ); } diff --git a/src/tests/EventQueueTest.cpp b/src/tests/EventQueueTest.cpp index 79d3db80dc..38c7f95204 100644 --- a/src/tests/EventQueueTest.cpp +++ b/src/tests/EventQueueTest.cpp @@ -65,6 +65,7 @@ class EventQueueTest : public CppUnit::TestCase { } void testPushPop() { + ___INFOLOG( "" ); Event ev; // Fill the event queue to the maximum permissible size, drain the queue and then do it again. @@ -81,9 +82,11 @@ class EventQueueTest : public CppUnit::TestCase { ev = m_pQ->pop_event(); CPPUNIT_ASSERT( ev.type == EVENT_NONE ); } + ___INFOLOG( "passed" ); } void testOverflow() { + ___INFOLOG( "" ); Event ev; // Overfill queue @@ -97,9 +100,11 @@ class EventQueueTest : public CppUnit::TestCase { } ev = m_pQ->pop_event(); CPPUNIT_ASSERT( ev.type == EVENT_NONE ); + ___INFOLOG( "passed" ); } void testThreadedAccess() { + ___INFOLOG( "" ); pthread_t threads[ nThreads ]; int counters[ nThreads ]; int threadIds[ nThreads ]; @@ -132,6 +137,7 @@ class EventQueueTest : public CppUnit::TestCase { } Event ev = m_pQ->pop_event(); CPPUNIT_ASSERT( ev.type == EVENT_NONE ); + ___INFOLOG( "passed" ); } }; diff --git a/src/tests/FilesystemTest.cpp b/src/tests/FilesystemTest.cpp index ae0144e036..7ff8fffd11 100644 --- a/src/tests/FilesystemTest.cpp +++ b/src/tests/FilesystemTest.cpp @@ -49,6 +49,7 @@ void FilesystemTest::tearDown() { void FilesystemTest::testPermissions(){ #ifndef WIN32 + ___INFOLOG( "" ); CPPUNIT_ASSERT( ! Filesystem::file_exists( m_sNotExistingPath, true ) ); CPPUNIT_ASSERT( Filesystem::file_exists( m_sNoAccessPath, true ) ); CPPUNIT_ASSERT( ! Filesystem::file_readable( m_sNoAccessPath, true ) ); @@ -59,5 +60,6 @@ void FilesystemTest::testPermissions(){ CPPUNIT_ASSERT( Filesystem::file_exists( m_sTmpPath, true ) ); CPPUNIT_ASSERT( Filesystem::file_readable( m_sTmpPath, true ) ); CPPUNIT_ASSERT( Filesystem::file_writable( m_sTmpPath, true ) ); + ___INFOLOG( "passed" ); #endif } diff --git a/src/tests/FunctionalTests.cpp b/src/tests/FunctionalTests.cpp index 0550749510..271869ffde 100644 --- a/src/tests/FunctionalTests.cpp +++ b/src/tests/FunctionalTests.cpp @@ -69,6 +69,7 @@ class FunctionalTest : public CppUnit::TestCase { some basic core classes.*/ void testPrintMessages() { + ___INFOLOG( "" ); auto sSongFile = H2TEST_FILE( "functional/test.h2song" ); auto sDrumkitFile = H2TEST_FILE( "/drumkits/baseKit" ); @@ -147,169 +148,119 @@ class FunctionalTest : public CppUnit::TestCase { // std::cout << pSong->toQString( "", false ).toLocal8Bit().data() << std::endl; // std::cout << pPlaylist->toQString( "", false ).toLocal8Bit().data(); + ___INFOLOG( "passed" ); } void testExportAudio() - { + { + ___INFOLOG( "" ); auto songFile = H2TEST_FILE("functional/test.h2song"); auto outFile = Filesystem::tmp_file_path("test.wav"); auto refFile = H2TEST_FILE("functional/test.ref.flac"); - exportSong( songFile, outFile ); + TestHelper::exportSong( songFile, outFile ); H2TEST_ASSERT_AUDIO_FILES_EQUAL( refFile, outFile ); Filesystem::rm( outFile ); + ___INFOLOG( "passed" ); } void testExportMIDISMF1Single() { + ___INFOLOG( "" ); auto songFile = H2TEST_FILE("functional/test.h2song"); auto outFile = Filesystem::tmp_file_path("smf1single.test.mid"); auto refFile = H2TEST_FILE("functional/smf1single.test.ref.mid"); SMF1WriterSingle writer; - exportMIDI( songFile, outFile, writer ); + TestHelper::exportMIDI( songFile, outFile, writer ); H2TEST_ASSERT_FILES_EQUAL( refFile, outFile ); Filesystem::rm( outFile ); + ___INFOLOG( "passed" ); } void testExportMIDISMF1Multi() { + ___INFOLOG( "" ); auto songFile = H2TEST_FILE("functional/test.h2song"); auto outFile = Filesystem::tmp_file_path("smf1multi.test.mid"); auto refFile = H2TEST_FILE("functional/smf1multi.test.ref.mid"); SMF1WriterMulti writer; - exportMIDI( songFile, outFile, writer ); + TestHelper::exportMIDI( songFile, outFile, writer ); H2TEST_ASSERT_FILES_EQUAL( refFile, outFile ); Filesystem::rm( outFile ); + ___INFOLOG( "passed" ); } void testExportMIDISMF0() { + ___INFOLOG( "" ); auto songFile = H2TEST_FILE("functional/test.h2song"); auto outFile = Filesystem::tmp_file_path("smf0.test.mid"); auto refFile = H2TEST_FILE("functional/smf0.test.ref.mid"); SMF0Writer writer; - exportMIDI( songFile, outFile, writer ); + TestHelper::exportMIDI( songFile, outFile, writer ); H2TEST_ASSERT_FILES_EQUAL( refFile, outFile ); Filesystem::rm( outFile ); + ___INFOLOG( "passed" ); } /* SKIP void testExportMuteGroupsAudio() { + ___INFOLOG( "" ); auto songFile = H2TEST_FILE("functional/mutegroups.h2song"); auto outFile = Filesystem::tmp_file_path("mutegroups.wav"); auto refFile = H2TEST_FILE("functional/mutegroups.ref.flac"); - exportSong( songFile, outFile ); + TestHelper::exportSong( songFile, outFile ); H2TEST_ASSERT_AUDIO_FILES_EQUAL( refFile, outFile ); Filesystem::rm( outFile ); + ___INFOLOG( "passed" ); } */ void testExportVelocityAutomationAudio() { + ___INFOLOG( "" ); auto songFile = H2TEST_FILE("functional/velocityautomation.h2song"); auto outFile = Filesystem::tmp_file_path("velocityautomation.wav"); auto refFile = H2TEST_FILE("functional/velocityautomation.ref.flac"); - exportSong( songFile, outFile ); + TestHelper::exportSong( songFile, outFile ); H2TEST_ASSERT_AUDIO_FILES_EQUAL( refFile, outFile ); Filesystem::rm( outFile ); + ___INFOLOG( "passed" ); } void testExportVelocityAutomationMIDISMF1() { + ___INFOLOG( "" ); auto songFile = H2TEST_FILE("functional/velocityautomation.h2song"); auto outFile = Filesystem::tmp_file_path("smf1.velocityautomation.mid"); auto refFile = H2TEST_FILE("functional/smf1.velocityautomation.ref.mid"); SMF1WriterSingle writer; - exportMIDI( songFile, outFile, writer ); + TestHelper::exportMIDI( songFile, outFile, writer ); H2TEST_ASSERT_FILES_EQUAL( refFile, outFile ); Filesystem::rm( outFile ); + ___INFOLOG( "passed" ); } void testExportVelocityAutomationMIDISMF0() { + ___INFOLOG( "" ); auto songFile = H2TEST_FILE("functional/velocityautomation.h2song"); auto outFile = Filesystem::tmp_file_path("smf0.velocityautomation.mid"); auto refFile = H2TEST_FILE("functional/smf0.velocityautomation.ref.mid"); SMF0Writer writer; - exportMIDI( songFile, outFile, writer ); + TestHelper::exportMIDI( songFile, outFile, writer ); H2TEST_ASSERT_FILES_EQUAL( refFile, outFile ); Filesystem::rm( outFile ); - } - -private: - /** - * \brief Export Hydrogon song to audio file - * \param songFile Path to Hydrogen file - * \param fileName Output file name - **/ - void exportSong( const QString &songFile, const QString &fileName ) - { - auto t0 = std::chrono::high_resolution_clock::now(); - - Hydrogen *pHydrogen = Hydrogen::get_instance(); - EventQueue *pQueue = EventQueue::get_instance(); - - std::shared_ptr pSong = Song::load( songFile ); - CPPUNIT_ASSERT( pSong != nullptr ); - - if( !pSong ) { - return; - } - - pHydrogen->setSong( pSong ); - - auto pInstrumentList = pSong->getInstrumentList(); - for (auto i = 0; i < pInstrumentList->size(); i++) { - pInstrumentList->get(i)->set_currently_exported( true ); - } - - pHydrogen->startExportSession( 44100, 16 ); - pHydrogen->startExportSong( fileName ); - - bool done = false; - while ( ! done ) { - Event event = pQueue->pop_event(); - - if (event.type == EVENT_PROGRESS && event.value == 100) { - done = true; - } - else if ( event.type == EVENT_NONE ) { - usleep(100 * 1000); - } - } - pHydrogen->stopExportSession(); - - auto t1 = std::chrono::high_resolution_clock::now(); - double t = std::chrono::duration( t1 - t0 ).count(); - ___INFOLOG( QString("Audio export took %1 seconds").arg(t) ); - } - - /** - * \brief Export Hydrogon song to MIDI file - * \param songFile Path to Hydrogen file - * \param fileName Output file name - **/ - void exportMIDI( const QString &songFile, const QString &fileName, SMFWriter& writer ) - { - auto t0 = std::chrono::high_resolution_clock::now(); - - std::shared_ptr pSong = Song::load( songFile ); - CPPUNIT_ASSERT( pSong != nullptr ); - - writer.save( fileName, pSong ); - - auto t1 = std::chrono::high_resolution_clock::now(); - double t = std::chrono::duration( t1 - t0 ).count(); - ___INFOLOG( QString("MIDI track export took %1 seconds").arg(t) ); + ___INFOLOG( "passed" ); } diff --git a/src/tests/InstrumentListTest.cpp b/src/tests/InstrumentListTest.cpp index a0d6a451be..618d834105 100644 --- a/src/tests/InstrumentListTest.cpp +++ b/src/tests/InstrumentListTest.cpp @@ -40,17 +40,20 @@ class InstrumentListTest : public CppUnit::TestCase { public: void test_one_instrument() { + ___INFOLOG( "" ); InstrumentList list; auto pKick = std::make_shared( EMPTY_INSTR_ID, "Kick" ); pKick->set_midi_out_note(42); list.add(pKick); CPPUNIT_ASSERT( !list.has_all_midi_notes_same() ); + ___INFOLOG( "passed" ); } void test1() { + ___INFOLOG( "" ); InstrumentList list; auto pKick = std::make_shared( EMPTY_INSTR_ID, "Kick" ); @@ -67,11 +70,13 @@ class InstrumentListTest : public CppUnit::TestCase { CPPUNIT_ASSERT_EQUAL( 3, list.size() ); CPPUNIT_ASSERT( list.has_all_midi_notes_same() ); + ___INFOLOG( "passed" ); } void test2() { + ___INFOLOG( "" ); InstrumentList list; auto pKick = std::make_shared( EMPTY_INSTR_ID, "Kick" ); @@ -92,11 +97,13 @@ class InstrumentListTest : public CppUnit::TestCase { CPPUNIT_ASSERT_EQUAL( 4, list.size() ); CPPUNIT_ASSERT( !list.has_all_midi_notes_same() ); + ___INFOLOG( "passed" ); } void test3() { + ___INFOLOG( "" ); InstrumentList list; list.add( std::make_shared() ); @@ -108,11 +115,13 @@ class InstrumentListTest : public CppUnit::TestCase { CPPUNIT_ASSERT_EQUAL( 36, list.get(0)->get_midi_out_note() ); CPPUNIT_ASSERT_EQUAL( 37, list.get(1)->get_midi_out_note() ); CPPUNIT_ASSERT_EQUAL( 38, list.get(2)->get_midi_out_note() ); + ___INFOLOG( "passed" ); } //test is_valid_index void test4() { + ___INFOLOG( "" ); InstrumentList list; list.add( std::make_shared() ); @@ -120,6 +129,7 @@ class InstrumentListTest : public CppUnit::TestCase { CPPUNIT_ASSERT( list.is_valid_index(0) ); CPPUNIT_ASSERT( !list.is_valid_index(1) ); CPPUNIT_ASSERT( !list.is_valid_index(-42) ); + ___INFOLOG( "passed" ); } }; diff --git a/src/tests/LicenseTest.cpp b/src/tests/LicenseTest.cpp index 2982357ff5..410b08fccf 100644 --- a/src/tests/LicenseTest.cpp +++ b/src/tests/LicenseTest.cpp @@ -26,6 +26,7 @@ using namespace H2Core; void LicenseTest::testParsing() { + ___INFOLOG( "" ); License licenseCC0_0("cc0"); CPPUNIT_ASSERT( licenseCC0_0.getType() == License::CC_0 ); @@ -96,9 +97,11 @@ void LicenseTest::testParsing() { QString sLicense2_serialized( license2_parsed.getLicenseString() ); CPPUNIT_ASSERT( sLicense2_raw == sLicense2_serialized ); CPPUNIT_ASSERT( license2_parsed.getType() == License::CC_BY ); + ___INFOLOG( "passed" ); } void LicenseTest::testOperators() { + ___INFOLOG( "" ); License licenseCC0_0("cc0"); License licenseCC0_1("CC-0"); License licenseCC_BY("CC BY"); @@ -114,4 +117,5 @@ void LicenseTest::testOperators() { License licenseBSD_2("bsd"); CPPUNIT_ASSERT( licenseBSD_0 == licenseBSD_1 ); CPPUNIT_ASSERT( licenseBSD_1 != licenseBSD_2 ); + ___INFOLOG( "passed" ); } diff --git a/src/tests/MemoryLeakageTest.cpp b/src/tests/MemoryLeakageTest.cpp index 9b1877b014..5419747099 100644 --- a/src/tests/MemoryLeakageTest.cpp +++ b/src/tests/MemoryLeakageTest.cpp @@ -27,6 +27,7 @@ void MemoryLeakageTest::testConstructors() { + ___INFOLOG( "" ); auto mapSnapshot = H2Core::Base::getObjectMap(); int nAliveReference = H2Core::Base::getAliveObjectCount(); @@ -241,9 +242,11 @@ void MemoryLeakageTest::testConstructors() { pDrumkitProper = nullptr; pSongProper = nullptr; CPPUNIT_ASSERT( nAliveReference == H2Core::Base::getAliveObjectCount() ); + ___INFOLOG( "passed" ); } void MemoryLeakageTest::testLoading() { + ___INFOLOG( "" ); H2Core::XMLDoc doc; H2Core::XMLNode node; @@ -445,6 +448,7 @@ void MemoryLeakageTest::testLoading() { pCoreActionController->setDrumkit( pDrumkit ); CPPUNIT_ASSERT( nLoaded == H2Core::Base::getAliveObjectCount() ); } + ___INFOLOG( "passed" ); } void MemoryLeakageTest::tearDown() { diff --git a/src/tests/MidiNoteTest.cpp b/src/tests/MidiNoteTest.cpp index 33949285fa..c1c04f237a 100644 --- a/src/tests/MidiNoteTest.cpp +++ b/src/tests/MidiNoteTest.cpp @@ -79,6 +79,7 @@ class MidiNoteTest : public CppUnit::TestCase { void testLoadNewSong() { + ___INFOLOG( "" ); /* Read song with instruments that have assigned distinct * MIDI notes. Check that loading that song does not * change that mapping */ @@ -94,6 +95,7 @@ class MidiNoteTest : public CppUnit::TestCase { ASSERT_INSTRUMENT_MIDI_NOTE( "Snare Rock", 40, instruments->get(1) ); ASSERT_INSTRUMENT_MIDI_NOTE( "Crash", 49, instruments->get(2) ); ASSERT_INSTRUMENT_MIDI_NOTE( "Ride Rock", 59, instruments->get(3) ); + ___INFOLOG( "passed" ); } private: diff --git a/src/tests/NetwrokTest.cpp b/src/tests/NetworkTest.cpp similarity index 91% rename from src/tests/NetwrokTest.cpp rename to src/tests/NetworkTest.cpp index ac1fd16c58..99df630f92 100644 --- a/src/tests/NetwrokTest.cpp +++ b/src/tests/NetworkTest.cpp @@ -23,7 +23,12 @@ #include "NetworkTest.h" #include +#include +#include + void NetworkTest::testSslSupport(){ + ___INFOLOG( "" ); CPPUNIT_ASSERT( QSslSocket::supportsSsl() ); + ___INFOLOG( "passed" ); } diff --git a/src/tests/NoteTest.cpp b/src/tests/NoteTest.cpp index dc6a4b9e80..c636809a9a 100644 --- a/src/tests/NoteTest.cpp +++ b/src/tests/NoteTest.cpp @@ -37,16 +37,19 @@ class NoteTest : public CppUnit::TestCase { void testProbability() { + ___INFOLOG( "" ); Note n(nullptr, 0, 1.0f, 0.f, 1, 1.0f); n.set_probability(0.75f); CPPUNIT_ASSERT_EQUAL(0.75f, n.get_probability()); Note other(&n, nullptr); CPPUNIT_ASSERT_EQUAL(0.75f, other.get_probability()); + ___INFOLOG( "passed" ); } void testSerializeProbability() { + ___INFOLOG( "" ); QDomDocument doc; QDomElement root = doc.createElement("note"); XMLNode node(root); @@ -71,6 +74,7 @@ class NoteTest : public CppUnit::TestCase { delete in; delete out; + ___INFOLOG( "passed" ); } }; diff --git a/src/tests/OscServerTest.cpp b/src/tests/OscServerTest.cpp index f40be69dac..0525d083c0 100644 --- a/src/tests/OscServerTest.cpp +++ b/src/tests/OscServerTest.cpp @@ -89,6 +89,7 @@ void OscServerTest::tearDown(){ } void OscServerTest::testSessionManagement(){ + ___INFOLOG( "" ); // Create an object with which we will send messages to the custom // OSC server. @@ -115,6 +116,7 @@ void OscServerTest::testSessionManagement(){ m_sValidPath.toLocal8Bit().data()); WAIT(m_sValidPath == m_pHydrogen->getSong()->getFilename()); CPPUNIT_ASSERT( m_sValidPath == m_pHydrogen->getSong()->getFilename() ); + ___INFOLOG( "passed" ); } #endif diff --git a/src/tests/PatternTest.cpp b/src/tests/PatternTest.cpp index aad98eaa6b..68cc7202c9 100644 --- a/src/tests/PatternTest.cpp +++ b/src/tests/PatternTest.cpp @@ -29,6 +29,7 @@ using namespace H2Core; void PatternTest::testPurgeInstrument() { + ___INFOLOG( "" ); auto pInstrument = std::make_shared(); Note *pNote = new Note( pInstrument, 1, 1.0, 0.f, 1, 1.0 ); @@ -40,4 +41,5 @@ void PatternTest::testPurgeInstrument() CPPUNIT_ASSERT( pPattern->find_note( 1, -1, pInstrument) == nullptr ); delete pPattern; + ___INFOLOG( "passed" ); } diff --git a/src/tests/SampleTest.cpp b/src/tests/SampleTest.cpp index 3228499af3..7802447248 100644 --- a/src/tests/SampleTest.cpp +++ b/src/tests/SampleTest.cpp @@ -33,6 +33,7 @@ class SampleTest : public CppUnit::TestCase { void testLoadInvalidSample() { + ___INFOLOG( "" ); std::shared_ptr pSample; //TC1: Sample does not exist @@ -44,5 +45,6 @@ class SampleTest : public CppUnit::TestCase { //TC2: Sample does exist, but is not a valid sample pSample = H2Core::Sample::load( H2TEST_FILE("drumkits/baseKit/drumkit.xml") ); CPPUNIT_ASSERT(pSample == nullptr); + ___INFOLOG( "passed" ); } }; diff --git a/src/tests/TestHelper.cpp b/src/tests/TestHelper.cpp index 86c8f9b48a..11de948560 100644 --- a/src/tests/TestHelper.cpp +++ b/src/tests/TestHelper.cpp @@ -26,12 +26,17 @@ #include "core/Hydrogen.h" #include "core/Helpers/Filesystem.h" #include "core/Preferences/Preferences.h" +#include +#include #include #include #include #include #include +#include + +#include static const QString APP_DATA_DIR = "/data/"; static const QString TEST_DATA_DIR = "/src/tests/data/"; @@ -222,3 +227,89 @@ void TestHelper::varyAudioDriverConfig( int nIndex ) { H2Core::Hydrogen::get_instance()->restartDrivers(); } + +void TestHelper::exportSong( const QString& sSongFile, const QString& sFileName ) +{ + auto t0 = std::chrono::high_resolution_clock::now(); + + auto pHydrogen = H2Core::Hydrogen::get_instance(); + auto pQueue = H2Core::EventQueue::get_instance(); + + auto pSong = H2Core::Song::load( sSongFile ); + CPPUNIT_ASSERT( pSong != nullptr ); + + pHydrogen->setSong( pSong ); + + auto pInstrumentList = pSong->getInstrumentList(); + for (auto i = 0; i < pInstrumentList->size(); i++) { + pInstrumentList->get(i)->set_currently_exported( true ); + } + + pHydrogen->startExportSession( 44100, 16 ); + pHydrogen->startExportSong( sFileName ); + + bool bDone = false; + while ( ! bDone ) { + H2Core::Event event = pQueue->pop_event(); + + if (event.type == H2Core::EVENT_PROGRESS && event.value == 100) { + bDone = true; + } + else if ( event.type == H2Core::EVENT_NONE ) { + usleep(100 * 1000); + } + } + pHydrogen->stopExportSession(); + + auto t1 = std::chrono::high_resolution_clock::now(); + double t = std::chrono::duration( t1 - t0 ).count(); + ___INFOLOG( QString("Audio export took %1 seconds").arg(t) ); +} + +void TestHelper::exportSong( const QString& sFileName ) +{ + auto t0 = std::chrono::high_resolution_clock::now(); + + auto pHydrogen = H2Core::Hydrogen::get_instance(); + auto pQueue = H2Core::EventQueue::get_instance(); + auto pSong = pHydrogen->getSong(); + + auto pInstrumentList = pSong->getInstrumentList(); + for (auto i = 0; i < pInstrumentList->size(); i++) { + pInstrumentList->get(i)->set_currently_exported( true ); + } + + pHydrogen->startExportSession( 44100, 16 ); + pHydrogen->startExportSong( sFileName ); + + bool bDone = false; + while ( ! bDone ) { + H2Core::Event event = pQueue->pop_event(); + + if (event.type == H2Core::EVENT_PROGRESS && event.value == 100) { + bDone = true; + } + else if ( event.type == H2Core::EVENT_NONE ) { + usleep(100 * 1000); + } + } + pHydrogen->stopExportSession(); + + auto t1 = std::chrono::high_resolution_clock::now(); + double t = std::chrono::duration( t1 - t0 ).count(); + ___INFOLOG( QString("Audio export took %1 seconds").arg(t) ); +} + +void TestHelper::exportMIDI( const QString& sSongFile, const QString& sFileName, H2Core::SMFWriter& writer ) +{ + auto t0 = std::chrono::high_resolution_clock::now(); + + auto pSong = H2Core::Song::load( sSongFile ); + CPPUNIT_ASSERT( pSong != nullptr ); + + writer.save( sFileName, pSong ); + + auto t1 = std::chrono::high_resolution_clock::now(); + double t = std::chrono::duration( t1 - t0 ).count(); + ___INFOLOG( QString("MIDI track export took %1 seconds").arg(t) ); +} diff --git a/src/tests/TestHelper.h b/src/tests/TestHelper.h index 25ed8a7061..2f1964d7e9 100644 --- a/src/tests/TestHelper.h +++ b/src/tests/TestHelper.h @@ -25,6 +25,7 @@ #include #include +#include class TestHelper { static TestHelper* m_pInstance; @@ -51,9 +52,32 @@ class TestHelper { * \return true on success */ static void varyAudioDriverConfig( int nIndex ); + + /** + * Export Hydrogon song @a sSongFile to audio file @a sFileName; + * + * \param sSongFile Path to Hydrogen file + * \param sFileName Output file name + */ + static void exportSong( const QString& sSongFile, const QString& sFileName ); + /** + * Export the current song within Hydrogen to audio file @a sFileName; + * + * \param sFileName Output file name + */ + static void exportSong( const QString& sFileName ); + + /** + * Export Hydrogon song @a sSongFile to MIDI file @a sFileName + * using writer @a writer. + * \param sSongFile Path to Hydrogen file + * \param sFileName Output file name + * \param writer Writer. + **/ + static void exportMIDI( const QString& sSongFile, const QString& sFileName, H2Core::SMFWriter& writer ); - static void createInstance(); - static TestHelper* get_instance(); + static void createInstance(); + static TestHelper* get_instance(); }; inline TestHelper* TestHelper::get_instance() diff --git a/src/tests/TimeTest.cpp b/src/tests/TimeTest.cpp index bf7afa4582..e00593f802 100644 --- a/src/tests/TimeTest.cpp +++ b/src/tests/TimeTest.cpp @@ -72,6 +72,7 @@ float TimeTest::locateAndLookupTime( int nPatternPos ){ } void TimeTest::testElapsedTime(){ + ___INFOLOG( "" ); CPPUNIT_ASSERT( std::abs( locateAndLookupTime( 0 ) - 0 ) < 0.0001 ); CPPUNIT_ASSERT( std::abs( locateAndLookupTime( 1 ) - 2 ) < 0.0001 ); @@ -85,4 +86,5 @@ void TimeTest::testElapsedTime(){ CPPUNIT_ASSERT( std::abs( locateAndLookupTime( 1 ) - 2 ) < 0.0001 ); CPPUNIT_ASSERT( std::abs( locateAndLookupTime( 5 ) - 10.8 ) < 0.0001 ); CPPUNIT_ASSERT( std::abs( locateAndLookupTime( 2 ) - 4 ) < 0.0001 ); + ___INFOLOG( "passed" ); } diff --git a/src/tests/Translations.cpp b/src/tests/Translations.cpp index 8d18f06f19..1e79d36fd4 100644 --- a/src/tests/Translations.cpp +++ b/src/tests/Translations.cpp @@ -36,6 +36,7 @@ class UITranslationTest : public CppUnit::TestCase { } void testLanguageSelection() { + ___INFOLOG( "" ); // Find translations. i18n_dir() contains only the source *.ts // files. Find the *.qm files for the translations relative to // the build directory the tests are run from. @@ -99,7 +100,8 @@ class UITranslationTest : public CppUnit::TestCase { QLocale l( sTr ); CPPUNIT_ASSERT( l.language() == QLocale::French ); } - + + ___INFOLOG( "passed" ); } }; diff --git a/src/tests/TransportTest.cpp b/src/tests/TransportTest.cpp index c3e0bc0b7f..c627ecb986 100644 --- a/src/tests/TransportTest.cpp +++ b/src/tests/TransportTest.cpp @@ -21,7 +21,7 @@ */ #include -#include +#include #include #include #include @@ -31,18 +31,11 @@ #include "TransportTest.h" #include "TestHelper.h" +#include "assertions/AudioFile.h" + using namespace H2Core; void TransportTest::setUp(){ - - // We need a song that has at least the maximum pattern group - // number provided in testElapsedTime(). An empty one won't do it. - m_pSongDemo = Song::load( QString( "%1/GM_kit_demo3.h2song" ).arg( Filesystem::demos_dir() ) ); - - m_pSongSizeChanged = Song::load( QString( H2TEST_FILE( "song/AE_songSizeChanged.h2song" ) ) ); - - CPPUNIT_ASSERT( m_pSongDemo != nullptr ); - CPPUNIT_ASSERT( m_pSongSizeChanged != nullptr ); Preferences::get_instance()->m_bUseMetronome = false; } @@ -54,40 +47,74 @@ void TransportTest::tearDown() { // scrolling. As the TestRunner itself does not seem to support // fixtures, we flush the logger in here. H2Core::Logger::get_instance()->flush(); + + // Reset to default audio driver config + auto pPref = H2Core::Preferences::get_instance(); + pPref->m_nBufferSize = 1024; + pPref->m_nSampleRate = 44100; } void TransportTest::testFrameToTickConversion() { + ___INFOLOG( "" ); auto pHydrogen = Hydrogen::get_instance(); - auto pAudioEngine = pHydrogen->getAudioEngine(); - pHydrogen->getCoreActionController()->openSong( m_pSongDemo ); - - for ( int ii = 0; ii < 15; ++ii ) { + auto pSongDemo = Song::load( QString( "%1/GM_kit_demo3.h2song" ) + .arg( Filesystem::demos_dir() ) ); + CPPUNIT_ASSERT( pSongDemo != nullptr ); + pHydrogen->getCoreActionController()->openSong( pSongDemo ); + + const std::vector indices{ 0, 5, 7, 12 }; + for ( const int ii : indices ) { TestHelper::varyAudioDriverConfig( ii ); - bool bNoMismatch = pAudioEngine->testFrameToTickConversion(); - CPPUNIT_ASSERT( bNoMismatch ); + perform( &AudioEngineTests::testFrameToTickConversion ); } + ___INFOLOG( "passed" ); } void TransportTest::testTransportProcessing() { + ___INFOLOG( "" ); auto pHydrogen = Hydrogen::get_instance(); - auto pAudioEngine = pHydrogen->getAudioEngine(); - pHydrogen->getCoreActionController()->openSong( m_pSongDemo ); + auto pSongDemo = Song::load( QString( "%1/GM_kit_demo3.h2song" ) + .arg( Filesystem::demos_dir() ) ); + CPPUNIT_ASSERT( pSongDemo != nullptr ); + pHydrogen->getCoreActionController()->openSong( pSongDemo ); - for ( int ii = 0; ii < 15; ++ii ) { + const std::vector indices{ 1, 9, 14 }; + for ( const int ii : indices ) { TestHelper::varyAudioDriverConfig( ii ); - bool bNoMismatch = pAudioEngine->testTransportProcessing(); - CPPUNIT_ASSERT( bNoMismatch ); + perform( &AudioEngineTests::testTransportProcessing ); } + ___INFOLOG( "passed" ); +} + +void TransportTest::testTransportProcessingTimeline() { + ___INFOLOG( "" ); + auto pHydrogen = Hydrogen::get_instance(); + + auto pSongTransportProcessingTimeline = + Song::load( QString( H2TEST_FILE( "song/AE_transportProcessingTimeline.h2song" ) ) ); + CPPUNIT_ASSERT( pSongTransportProcessingTimeline != nullptr ); + pHydrogen->getCoreActionController()-> + openSong( pSongTransportProcessingTimeline ); + + const std::vector indices{ 2, 9, 10 }; + for ( const int ii : indices ) { + TestHelper::varyAudioDriverConfig( ii ); + perform( &AudioEngineTests::testTransportProcessingTimeline ); + } + ___INFOLOG( "passed" ); } void TransportTest::testTransportRelocation() { + ___INFOLOG( "" ); auto pHydrogen = Hydrogen::get_instance(); - auto pAudioEngine = pHydrogen->getAudioEngine(); auto pCoreActionController = pHydrogen->getCoreActionController(); - pCoreActionController->openSong( m_pSongDemo ); + auto pSongDemo = Song::load( QString( "%1/GM_kit_demo3.h2song" ) + .arg( Filesystem::demos_dir() ) ); + CPPUNIT_ASSERT( pSongDemo != nullptr ); + pCoreActionController->openSong( pSongDemo ); pCoreActionController->activateTimeline( true ); pCoreActionController->addTempoMarker( 0, 120 ); @@ -100,72 +127,187 @@ void TransportTest::testTransportRelocation() { pCoreActionController->addTempoMarker( 7, 240.46 ); pCoreActionController->addTempoMarker( 8, 200.1 ); - for ( int ii = 0; ii < 15; ++ii ) { + const std::vector indices{ 0, 5, 6 }; + for ( const int ii : indices ) { TestHelper::varyAudioDriverConfig( ii ); - bool bNoMismatch = pAudioEngine->testTransportRelocation(); - CPPUNIT_ASSERT( bNoMismatch ); + perform( &AudioEngineTests::testTransportRelocation ); } pCoreActionController->activateTimeline( false ); -} + ___INFOLOG( "passed" ); +} -void TransportTest::testComputeTickInterval() { - auto pHydrogen = Hydrogen::get_instance(); - auto pAudioEngine = pHydrogen->getAudioEngine(); +void TransportTest::testLoopMode() { + ___INFOLOG( "" ); - pHydrogen->getCoreActionController()->openSong( m_pSongDemo ); + const QString sSongFile = H2TEST_FILE( "song/AE_loopMode.h2song" ); - for ( int ii = 0; ii < 15; ++ii ) { + auto pHydrogen = H2Core::Hydrogen::get_instance(); + auto pCoreActionController = pHydrogen->getCoreActionController(); + + auto pSong = H2Core::Song::load( sSongFile ); + CPPUNIT_ASSERT( pSong != nullptr ); + + pCoreActionController->openSong( pSong ); + + const std::vector indices{ 0, 1, 12 }; + for ( const int ii : indices ) { TestHelper::varyAudioDriverConfig( ii ); - bool bNoMismatch = pAudioEngine->testComputeTickInterval(); - CPPUNIT_ASSERT( bNoMismatch ); + perform( &AudioEngineTests::testLoopMode ); } -} + ___INFOLOG( "passed" ); +} void TransportTest::testSongSizeChange() { + ___INFOLOG( "" ); auto pHydrogen = Hydrogen::get_instance(); - auto pAudioEngine = pHydrogen->getAudioEngine(); + auto pCoreActionController = pHydrogen->getCoreActionController(); - pHydrogen->getCoreActionController()->openSong( m_pSongSizeChanged ); + auto pSongSizeChanged = + Song::load( QString( H2TEST_FILE( "song/AE_songSizeChanged.h2song" ) ) ); + CPPUNIT_ASSERT( pSongSizeChanged != nullptr ); + pCoreActionController->openSong( pSongSizeChanged ); + + // Depending on buffer size and sample rate transport might be + // loop when toggling a pattern at the end of the song. If there + // were tempo markers present, the chunk of the interval covered + // by AudioEngine::computeTickInterval being looped would have a + // different tickSize than its first part. This is itself no + // problem but it would make the test much more complex as we test + // against those calculated intervals to remain constant. + pCoreActionController->activateTimeline( false ); - for ( int ii = 0; ii < 15; ++ii ) { + const std::vector indices{ 0, 1, 2, 3 }; + for ( const int ii : indices ) { + TestHelper::varyAudioDriverConfig( ii ); + // For larger sample rates no notes will remain in the // AudioEngine::m_songNoteQueue after one process step. if ( H2Core::Preferences::get_instance()->m_nSampleRate <= 48000 ) { - TestHelper::varyAudioDriverConfig( ii ); - bool bNoMismatch = pAudioEngine->testSongSizeChange(); - CPPUNIT_ASSERT( bNoMismatch ); + perform( &AudioEngineTests::testSongSizeChange ); } } - - pHydrogen->getCoreActionController()->activateLoopMode( false ); + + pCoreActionController->activateLoopMode( false ); + ___INFOLOG( "passed" ); } void TransportTest::testSongSizeChangeInLoopMode() { + ___INFOLOG( "" ); auto pHydrogen = Hydrogen::get_instance(); - auto pAudioEngine = pHydrogen->getAudioEngine(); - pHydrogen->getCoreActionController()->openSong( m_pSongDemo ); + auto pSongDemo = Song::load( QString( "%1/GM_kit_demo3.h2song" ) + .arg( Filesystem::demos_dir() ) ); + CPPUNIT_ASSERT( pSongDemo != nullptr ); + pHydrogen->getCoreActionController()->openSong( pSongDemo ); - for ( int ii = 0; ii < 15; ++ii ) { + const std::vector indices{ 0, 5, 7, 13 }; + for ( const int ii : indices ) { TestHelper::varyAudioDriverConfig( ii ); - bool bNoMismatch = pAudioEngine->testSongSizeChangeInLoopMode(); - CPPUNIT_ASSERT( bNoMismatch ); + perform( &AudioEngineTests::testSongSizeChangeInLoopMode ); } -} + ___INFOLOG( "passed" ); +} + +void TransportTest::testPlaybackTrack() { + ___INFOLOG( "" ); + + QString sSongFile = H2TEST_FILE( "song/AE_playbackTrack.h2song" ); + QString sOutFile = Filesystem::tmp_file_path("testPlaybackTrack.wav"); + QString sRefFile = H2TEST_FILE("song/res/playbackTrack.flac"); + + TestHelper::exportSong( sSongFile, sOutFile ); + H2TEST_ASSERT_AUDIO_FILES_EQUAL( sRefFile, sOutFile ); + Filesystem::rm( sOutFile ); + ___INFOLOG( "passed" ); +} + +void TransportTest::testSampleConsistency() { + ___INFOLOG( "" ); + + const QString sSongFile = H2TEST_FILE( "song/AE_sampleConsistency.h2song" ); + const QString sDrumkitDir = H2TEST_FILE( "drumkits/sampleKit/" ); + const QString sOutFile = Filesystem::tmp_file_path("testsampleConsistency.wav"); + const QString sRefFile = H2TEST_FILE("drumkits/sampleKit/longSample.flac"); + + auto pHydrogen = H2Core::Hydrogen::get_instance(); + auto pCoreActionController = pHydrogen->getCoreActionController(); + + auto pSong = H2Core::Song::load( sSongFile ); + + CPPUNIT_ASSERT( pSong != nullptr ); + + pHydrogen->setSong( pSong ); + + // Apply drumkit containing the long sample to be tested. + pCoreActionController->setDrumkit( sDrumkitDir, true ); + + TestHelper::exportSong( sOutFile ); + H2TEST_ASSERT_AUDIO_FILES_DATA_EQUAL( sRefFile, sOutFile ); + Filesystem::rm( sOutFile ); + ___INFOLOG( "passed" ); +} void TransportTest::testNoteEnqueuing() { + ___INFOLOG( "" ); + auto pHydrogen = Hydrogen::get_instance(); + + auto pSongNoteEnqueuing = + Song::load( QString( H2TEST_FILE( "song/AE_noteEnqueuing.h2song" ) ) ); + CPPUNIT_ASSERT( pSongNoteEnqueuing != nullptr ); + pHydrogen->getCoreActionController()->openSong( pSongNoteEnqueuing ); + + // This test is quite time consuming. + std::vector indices{ 1, 9, 12 }; + for ( auto ii : indices ) { + TestHelper::varyAudioDriverConfig( ii ); + perform( &AudioEngineTests::testNoteEnqueuing ); + } + ___INFOLOG( "passed" ); +} + +void TransportTest::testNoteEnqueuingTimeline() { + ___INFOLOG( "" ); auto pHydrogen = Hydrogen::get_instance(); - auto pAudioEngine = pHydrogen->getAudioEngine(); + auto pSong = Song::load( QString( H2TEST_FILE( "song/AE_noteEnqueuingTimeline.h2song" ) ) ); + + CPPUNIT_ASSERT( pSong != nullptr ); - pHydrogen->getCoreActionController()->openSong( m_pSongSizeChanged ); + pHydrogen->getCoreActionController()->openSong( pSong ); // This test is quite time consuming. - std::vector indices{ 0, 1, 2, 5, 7, 9, 12, 15 }; + std::vector indices{ 0, 5, 7 }; for ( auto ii : indices ) { TestHelper::varyAudioDriverConfig( ii ); - bool bNoMismatch = pAudioEngine->testNoteEnqueuing(); - CPPUNIT_ASSERT( bNoMismatch ); + perform( &AudioEngineTests::testNoteEnqueuingTimeline ); } -} + ___INFOLOG( "passed" ); +} + +void TransportTest::testHumanization() { + ___INFOLOG( "" ); + auto pHydrogen = Hydrogen::get_instance(); + + auto pSongHumanization = + Song::load( QString( H2TEST_FILE( "song/AE_humanization.h2song" ) ) ); + CPPUNIT_ASSERT( pSongHumanization != nullptr ); + pHydrogen->getCoreActionController()->openSong( pSongHumanization ); + + // This test is quite time consuming. + std::vector indices{ 1, 10 }; + for ( auto ii : indices ) { + TestHelper::varyAudioDriverConfig( ii ); + perform( &AudioEngineTests::testHumanization ); + } + ___INFOLOG( "passed" ); +} + +void TransportTest::perform( std::function func ) { + try { + func(); + } catch ( std::exception& err ) { + CppUnit::Message msg( err.what() ); + throw CppUnit::Exception( msg ); + } +} diff --git a/src/tests/TransportTest.h b/src/tests/TransportTest.h index a958990126..88a3378501 100644 --- a/src/tests/TransportTest.h +++ b/src/tests/TransportTest.h @@ -20,6 +20,8 @@ * */ +#include + #include #include @@ -29,17 +31,22 @@ class TransportTest : public CppUnit::TestFixture { CPPUNIT_TEST_SUITE( TransportTest ); CPPUNIT_TEST( testFrameToTickConversion ); CPPUNIT_TEST( testTransportProcessing ); + CPPUNIT_TEST( testTransportProcessingTimeline ); CPPUNIT_TEST( testTransportRelocation ); - CPPUNIT_TEST( testComputeTickInterval ); + CPPUNIT_TEST( testLoopMode ); CPPUNIT_TEST( testSongSizeChange ); CPPUNIT_TEST( testSongSizeChangeInLoopMode ); +#ifndef WIN32 + CPPUNIT_TEST( testPlaybackTrack ); + CPPUNIT_TEST( testSampleConsistency ); +#endif CPPUNIT_TEST( testNoteEnqueuing ); + CPPUNIT_TEST( testNoteEnqueuingTimeline ); + CPPUNIT_TEST( testHumanization ); CPPUNIT_TEST_SUITE_END(); - private: - std::shared_ptr m_pSongDemo; - std::shared_ptr m_pSongSizeChanged; - + void perform( std::function func ); + public: void setUp(); void tearDown(); @@ -47,9 +54,22 @@ class TransportTest : public CppUnit::TestFixture { void testFrameToTickConversion(); void testTransportProcessing(); + void testTransportProcessingTimeline(); void testTransportRelocation(); - void testComputeTickInterval(); + void testLoopMode(); void testSongSizeChange(); void testSongSizeChangeInLoopMode(); + /** + * Checks whether the playback track is rendered properly and + * whether it doesn't get affected by tempo markers. + */ + void testPlaybackTrack(); + void testSampleConsistency(); void testNoteEnqueuing(); + /** + * Checks whether the order of notes enqueued and processed by the + * Sampler is consistent on tempo change. + */ + void testNoteEnqueuingTimeline(); + void testHumanization(); }; diff --git a/src/tests/XmlTest.cpp b/src/tests/XmlTest.cpp index fe0c9e11cb..cdd48aa934 100644 --- a/src/tests/XmlTest.cpp +++ b/src/tests/XmlTest.cpp @@ -78,6 +78,7 @@ static bool check_samples_data( std::shared_ptr dk, bool loaded void XmlTest::testDrumkit() { + ___INFOLOG( "" ); QString sDrumkitPath = H2Core::Filesystem::tmp_dir()+"dk0"; std::shared_ptr pDrumkitLoaded = nullptr; @@ -154,10 +155,12 @@ void XmlTest::testDrumkit() // Cleanup H2Core::Filesystem::rm( sDrumkitPath, true ); + ___INFOLOG( "passed" ); } void XmlTest::testShippedDrumkits() { + ___INFOLOG( "" ); H2Core::XMLDoc doc; for ( auto ii : H2Core::Filesystem::sys_drumkit_list() ) { CPPUNIT_ASSERT( doc.read( QString( "%1%2/drumkit.xml" ) @@ -166,6 +169,7 @@ void XmlTest::testShippedDrumkits() H2Core::Filesystem::drumkit_xsd_path() ) ); } + ___INFOLOG( "passed" ); } //Load drumkit which includes instrument with invalid ADSR values. @@ -174,6 +178,7 @@ void XmlTest::testShippedDrumkits() // correct ADSR values. void XmlTest::testDrumkit_UpgradeInvalidADSRValues() { + ___INFOLOG( "" ); auto pTestHelper = TestHelper::get_instance(); std::shared_ptr pDrumkit = nullptr; @@ -213,9 +218,11 @@ void XmlTest::testDrumkit_UpgradeInvalidADSRValues() H2TEST_FILE( "/drumkits/invAdsrKit/drumkit.xml" ), true ) ); CPPUNIT_ASSERT( H2Core::Filesystem::rm( backupFiles[ 0 ], false ) ); + ___INFOLOG( "passed" ); } void XmlTest::testDrumkitUpgrade() { + ___INFOLOG( "" ); // For all drumkits in the legacy folder, check whether there are // invalid. Then, we upgrade them to the most recent version and // check whether there are valid and if a second upgrade is yields @@ -321,10 +328,12 @@ void XmlTest::testDrumkitUpgrade() { H2Core::Filesystem::rm( firstUpgrade.path(), true, true ); H2Core::Filesystem::rm( secondUpgrade.path(), true, true ); } + ___INFOLOG( "passed" ); } void XmlTest::testPattern() { + ___INFOLOG( "" ); QString sPatternPath = H2Core::Filesystem::tmp_dir()+"pat.h2pattern"; H2Core::Pattern* pPatternLoaded = nullptr; @@ -366,17 +375,21 @@ void XmlTest::testPattern() delete pPatternLoaded; delete pPatternCopied; delete pPatternNew; + ___INFOLOG( "passed" ); } void XmlTest::checkTestPatterns() { + ___INFOLOG( "" ); H2Core::XMLDoc doc; CPPUNIT_ASSERT( doc.read( H2TEST_FILE( "/pattern/pat.h2pattern" ), H2Core::Filesystem::pattern_xsd_path() ) ); + ___INFOLOG( "passed" ); } void XmlTest::testPlaylist() { + ___INFOLOG( "" ); QString sPath = H2Core::Filesystem::tmp_dir()+"playlist.h2playlist"; H2Core::Playlist::create_instance(); @@ -391,6 +404,7 @@ void XmlTest::testPlaylist() delete pPlaylistLoaded; delete pPlaylistCurrent; + ___INFOLOG( "passed" ); } void XmlTest::tearDown() { diff --git a/src/tests/assertions/AudioFile.cpp b/src/tests/assertions/AudioFile.cpp index 82b0c269d0..8aa876010c 100644 --- a/src/tests/assertions/AudioFile.cpp +++ b/src/tests/assertions/AudioFile.cpp @@ -27,14 +27,15 @@ static constexpr qint64 BUFFER_SIZE = 4096; -void H2Test::checkAudioFilesEqual(const QString &expected, const QString &actual, CppUnit::SourceLine sourceLine) +void H2Test::checkAudioFilesEqual(const QString& sExpected, const QString& sActual, CppUnit::SourceLine sourceLine) { SF_INFO info1 = {0}; std::unique_ptr - f1{ sf_open( expected.toLocal8Bit().data(), SFM_READ, &info1), sf_close }; + f1{ sf_open( sExpected.toLocal8Bit().data(), SFM_READ, &info1), sf_close }; if ( f1 == nullptr ) { CppUnit::Message msg( - "Can't open reference file", + QString( "Can't open reference file [%1]" ).arg( sExpected ) + .toLocal8Bit().data(), sf_strerror( nullptr ) ); throw CppUnit::Exception(msg, sourceLine); @@ -42,10 +43,11 @@ void H2Test::checkAudioFilesEqual(const QString &expected, const QString &actual SF_INFO info2 = {0}; std::unique_ptr - f2{ sf_open( actual.toLocal8Bit().data(), SFM_READ, &info2), sf_close }; + f2{ sf_open( sActual.toLocal8Bit().data(), SFM_READ, &info2), sf_close }; if ( f2 == nullptr ) { CppUnit::Message msg( - "Can't open results file", + QString( "Can't open results file [%1]" ).arg( sActual ) + .toLocal8Bit().data(), sf_strerror( nullptr ) ); throw CppUnit::Exception(msg, sourceLine); @@ -54,8 +56,8 @@ void H2Test::checkAudioFilesEqual(const QString &expected, const QString &actual if ( info1.frames != info2.frames ) { CppUnit::Message msg( "Number of samples different", - std::string("Expected: ") + expected.toStdString(), - std::string("Actual : ") + actual.toStdString() ); + std::string("Expected: ") + sExpected.toStdString(), + std::string("Actual : ") + sActual.toStdString() ); throw CppUnit::Exception(msg, sourceLine); } @@ -80,8 +82,8 @@ void H2Test::checkAudioFilesEqual(const QString &expected, const QString &actual auto diffLocation = offset + i + 1; CppUnit::Message msg( std::string("Files differ at sample ") + std::to_string(diffLocation), - std::string("Expected: ") + expected.toStdString(), - std::string("Actual : ") + actual.toStdString() ); + std::string("Expected: ") + sExpected.toStdString(), + std::string("Actual : ") + sActual.toStdString() ); throw CppUnit::Exception(msg, sourceLine); } @@ -91,3 +93,110 @@ void H2Test::checkAudioFilesEqual(const QString &expected, const QString &actual remainingSamples -= read1; } } + +void H2Test::checkAudioFilesDataEqual(const QString& sExpected, const QString& sActual, CppUnit::SourceLine sourceLine) +{ + SF_INFO expectedInfo = {0}; + std::unique_ptr + f1{ sf_open( sExpected.toLocal8Bit().data(), SFM_READ, &expectedInfo), sf_close }; + if ( f1 == nullptr ) { + CppUnit::Message msg( + QString( "Can't open reference file [%1]" ).arg( sExpected ) + .toLocal8Bit().data(), + sf_strerror( nullptr ) + ); + throw CppUnit::Exception(msg, sourceLine); + } + + SF_INFO actualInfo = {0}; + std::unique_ptr + f2{ sf_open( sActual.toLocal8Bit().data(), SFM_READ, &actualInfo), sf_close }; + if ( f2 == nullptr ) { + CppUnit::Message msg( + QString( "Can't open results file [%1]" ).arg( sActual ) + .toLocal8Bit().data(), + sf_strerror( nullptr ) + ); + throw CppUnit::Exception(msg, sourceLine); + } + + if ( expectedInfo.frames > actualInfo.frames ) { + CppUnit::Message msg( + QString( "Number of samples retrieved from expected file larger than in actual one.\nExpected [%1]: %2\nActual [%3]: %4" ) + .arg( sExpected ).arg( expectedInfo.frames ) + .arg( sActual ).arg( actualInfo.frames ) + .toLocal8Bit().data() ); + throw CppUnit::Exception(msg, sourceLine); + } + + auto totalSamples = actualInfo.frames * actualInfo.channels; + auto expectedSamples = expectedInfo.frames * expectedInfo.channels; + auto samplesRead = 0; + auto offset = 0LL; + sf_count_t toRead; + while ( samplesRead < totalSamples ) { + short buf1[ BUFFER_SIZE ]; + short buf2[ BUFFER_SIZE ]; + + toRead = qMin( totalSamples - samplesRead, (sf_count_t)BUFFER_SIZE ); + if ( samplesRead < expectedSamples && + samplesRead + toRead >= expectedSamples ) { + // Read the remainder of the expected sample before + // skipping it. + toRead = qMin( expectedSamples - samplesRead, (sf_count_t)BUFFER_SIZE ); + } + + if ( samplesRead < expectedSamples ) { + auto read1 = sf_read_short( f1.get(), buf1, toRead); + if ( read1 != toRead ) { + throw CppUnit::Exception( CppUnit::Message( "Short read or read error 1" ), + sourceLine ); + } + } + + auto read2= sf_read_short( f2.get(), buf2, toRead); + if ( read2 != toRead ) { + throw CppUnit::Exception( CppUnit::Message( "Short read or read error 2" ), + sourceLine ); + } + + for ( sf_count_t i = 0; i < toRead; ++i ) { + // Bit-precise floating point on all platforms is + // unneeded, and arbitrarily small differences can create + // rounding differences. Allow results to differ by 1 + // either way to account for this. + if ( samplesRead < expectedSamples ) { + // There were enough samples in the expected audio + // file left to read them + int delta = (int)buf1[i] - (int)buf2[i]; + if ( delta < -1 || delta > 1 ) { + auto diffLocation = offset + i + 1; + CppUnit::Message msg( + std::string("Files differ at sample ") + std::to_string(diffLocation), + std::string("Expected: ") + sExpected.toStdString(), + std::string("Actual : ") + sActual.toStdString() ); + throw CppUnit::Exception(msg, sourceLine); + + } + } + else { + // The samples in the expected file are already + // exhausted. In order for still treat it equivalent + // to the actual one, the require the latter to only + // contain zeros from now on. + if ( buf2[0] != 0 ) { + auto diffLocation = offset + i + 1; + CppUnit::Message msg( + std::string("Got [") + std::to_string(diffLocation) + + std::string( "] instead of 0 in" ), + std::string("Actual : ") + sActual.toStdString() ); + throw CppUnit::Exception(msg, sourceLine); + } + } + + } + + offset += toRead; + samplesRead += toRead; + } +} diff --git a/src/tests/assertions/AudioFile.h b/src/tests/assertions/AudioFile.h index 43c11eefc1..f601d79fed 100644 --- a/src/tests/assertions/AudioFile.h +++ b/src/tests/assertions/AudioFile.h @@ -29,6 +29,7 @@ namespace H2Test { void checkAudioFilesEqual(const QString &expected, const QString &actual, CppUnit::SourceLine sourceLine); + void checkAudioFilesDataEqual(const QString &expected, const QString &actual, CppUnit::SourceLine sourceLine); } @@ -38,5 +39,12 @@ namespace H2Test { #define H2TEST_ASSERT_AUDIO_FILES_EQUAL(expected, actual) \ H2Test::checkAudioFilesEqual(expected, actual, CPPUNIT_SOURCELINE()) +/** + * \brief Assert that two files' contents are the same expect for + * tailing 0s in @a actual + **/ +#define H2TEST_ASSERT_AUDIO_FILES_DATA_EQUAL(expected, actual) \ + H2Test::checkAudioFilesDataEqual(expected, actual, CPPUNIT_SOURCELINE()) + #endif diff --git a/src/tests/data/drumkits/sampleKit/drumkit.xml b/src/tests/data/drumkits/sampleKit/drumkit.xml new file mode 100644 index 0000000000..cd51b139f4 --- /dev/null +++ b/src/tests/data/drumkits/sampleKit/drumkit.xml @@ -0,0 +1,79 @@ + + + + SampleKit + Philipp Mueller + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'DejaVu Sans'; font-size:10pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Lucida Grande';">Testing kit</span></p></body></html> + GPL + + undefined license + + + 0 + Main + 1 + + + + + 18 + Sample + 1 + false + false + 1 + 1 + 0 + 0 + 1 + false + false + 1 + 0 + 0 + 0 + 1 + 999 + -1 + -1 + 54 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + longSample.flac + 0 + 1 + 1 + 0 + + + + + diff --git a/src/tests/data/drumkits/sampleKit/longSample.flac b/src/tests/data/drumkits/sampleKit/longSample.flac new file mode 100644 index 0000000000..1916f719b0 Binary files /dev/null and b/src/tests/data/drumkits/sampleKit/longSample.flac differ diff --git a/src/tests/data/song/AE_humanization.h2song b/src/tests/data/song/AE_humanization.h2song new file mode 100644 index 0000000000..8dd2988475 --- /dev/null +++ b/src/tests/data/song/AE_humanization.h2song @@ -0,0 +1,5375 @@ + + + 1.1.1-'1c713b6e' + 120 + 0.5 + 0.5 + Untitled Song + hydrogen + ... + undefined license + false + true + + false + 0 + 0 + false + false + song + RATIO_STRAIGHT_POLYGONAL + 1.33333 + 0 + 0 + 0 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + + + 0 + Main + 1 + + + + + 0 + Kick + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 1 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 999 + -1 + -1 + 36 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Kick-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Soft.wav + 0.202899 + 0.369565 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Med.wav + 0.369565 + 0.731884 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Hard.wav + 0.731884 + 0.865942 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Hardest.wav + 0.865942 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 1 + Stick + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.99569 + false + false + 1 + 0.7 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 37 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + SideStick-Softest.wav + 0 + 0.188406 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Soft.wav + 0.188406 + 0.402174 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Med.wav + 0.402174 + 0.597826 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Hard.wav + 0.597826 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 2 + Snare + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1.02155 + false + false + 1 + 0.7 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 38 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Snare-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Soft.wav + 0.202899 + 0.376812 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Med.wav + 0.376812 + 0.568841 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Hard.wav + 0.568841 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 3 + Hand Clap + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1.02155 + false + false + 0.7 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 39 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HandClap.wav + 0 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 4 + Snare Rimshot + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 1 + 0.7 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 40 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + SnareRimshot-Softest.wav + 0 + 0.188406 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Soft.wav + 0.188406 + 0.380435 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Med.wav + 0.380435 + 0.594203 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Hard.wav + 0.594203 + 0.728261 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Hardest.wav + 0.728261 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 5 + Floor Tom + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1.04741 + false + false + 0.44 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 41 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + TomFloor-Softest.wav + 0 + 0.199275 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Soft.wav + 0.199275 + 0.394928 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Med.wav + 0.394928 + 0.608696 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Hard.wav + 0.608696 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 6 + Hat Closed + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.685345 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 42 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatClosed-Softest.wav + 0 + 0.173913 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Soft.wav + 0.173913 + 0.373188 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Med.wav + 0.373188 + 0.576087 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Hard.wav + 0.576087 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 7 + Tom 2 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 0.76 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 43 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Tom2-Softest.wav + 0 + 0.177536 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Soft.wav + 0.177536 + 0.373188 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Med.wav + 0.373188 + 0.576087 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Hard.wav + 0.576087 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 8 + Hat Pedal + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.685345 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 44 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatPedal-Softest.wav + 0 + 0.210145 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Soft.wav + 0.210145 + 0.384058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Med.wav + 0.384058 + 0.594203 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Hard.wav + 0.594203 + 0.797101 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Hardest.wav + 0.797101 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 9 + Tom 1 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 1 + 0.8 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 45 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Tom1-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Soft.wav + 0.202899 + 0.394928 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Med.wav + 0.394928 + 0.605072 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Hard.wav + 0.605072 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 10 + Hat Open + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.698276 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 46 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatOpen-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Soft.wav + 0.202899 + 0.398551 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Med.wav + 0.398551 + 0.605072 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Hard.wav + 0.605072 + 0.797101 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Hardest.wav + 0.797101 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 11 + Cowbell + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.568965 + false + false + 1 + 0.32 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 56 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Cowbell-Softest.wav + 0 + 0.184783 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Soft.wav + 0.184783 + 0.384058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Med.wav + 0.384058 + 0.576087 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Hard.wav + 0.576087 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 12 + Ride + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.633621 + false + false + 0.56 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 51 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Ride-Softest.wav + 0 + 0.195652 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Soft.wav + 0.195652 + 0.376812 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Med.wav + 0.376812 + 0.572464 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Hard.wav + 0.572464 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 13 + Crash + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.517241 + false + false + 1 + 0.74 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 49 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Crash-Softest.wav + 0 + 0.199275 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Soft.wav + 0.199275 + 0.384058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Med.wav + 0.384058 + 0.59058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Hard.wav + 0.59058 + 0.789855 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Hardest.wav + 0.789855 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 14 + Ride 2 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 0.5 + 1 + 0 + 0 + 0.6 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 59 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + 24Ride-5.wav + 0.8 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-4.wav + 0.6 + 0.8 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-3.wav + 0.4 + 0.6 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-2.wav + 0.2 + 0.4 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-1.wav + 0 + 0.2 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 15 + Splash + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.543103 + false + false + 0.98 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 55 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Splash-Softest.wav + 0 + 0.195652 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Soft.wav + 0.195652 + 0.362319 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Med.wav + 0.362319 + 0.568841 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Hard.wav + 0.568841 + 0.75 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Hardest.wav + 0.75 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 16 + Hat Semi-Open + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.69 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 82 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatSemiOpen-Softest.wav + 0 + 0.188406 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Soft.wav + 0.188406 + 0.380435 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Med.wav + 0.380435 + 0.57971 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Hard.wav + 0.57971 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 17 + Bell + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.534828 + false + false + 0.56 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 53 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Bell-Softest.wav + 0 + 0.210145 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Soft.wav + 0.210145 + 0.405797 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Med.wav + 0.405797 + 0.605072 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Hard.wav + 0.605072 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + + + Default properties + + not_categorized + 384 + 4 + + + 0 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 3 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 6 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 9 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 12 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 15 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 18 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 21 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 24 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 27 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 30 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 33 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 36 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 39 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 42 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 45 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 48 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 51 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 54 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 57 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 60 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 63 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 66 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 69 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 72 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 75 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 78 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 81 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 84 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 87 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 90 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 93 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 96 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 99 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 102 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 105 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 108 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 111 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 114 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 117 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 120 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 123 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 126 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 129 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 132 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 135 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 138 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 141 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 144 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 147 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 150 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 153 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 156 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 159 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 162 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 165 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 168 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 171 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 174 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 177 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 180 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 183 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 186 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 189 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 192 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 195 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 198 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 201 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 204 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 207 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 210 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 213 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 216 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 219 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 222 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 225 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 228 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 231 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 234 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 237 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 240 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 243 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 246 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 249 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 252 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 255 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 258 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 261 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 264 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 267 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 270 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 273 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 276 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 279 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 282 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 285 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 288 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 291 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 294 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 297 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 300 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 303 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 306 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 309 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 312 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 315 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 318 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 321 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 324 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 327 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 330 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 333 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 336 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 339 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 342 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 345 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 348 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 351 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 354 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 357 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 360 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 363 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 366 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 369 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 372 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 375 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 378 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 381 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + + + Altered properties + + not_categorized + 384 + 4 + + + 0 + 0.68 + 0.27 + 0.74 + 0 + D-1 + -1 + 0 + false + 1 + + + 3 + 0.66 + 0.27 + 0.72 + 0 + E0 + -1 + 0 + false + 1 + + + 6 + 0.58 + 0.29 + 0.76 + 0 + Ef1 + -1 + 0 + false + 1 + + + 9 + 0.54 + 0.31 + 0.76 + 0 + D0 + -1 + 0 + false + 1 + + + 12 + 0.52 + 0.32 + 0.76 + 0 + Cs0 + -1 + 0 + false + 1 + + + 15 + 0.52 + 0.25 + 0.76 + 0 + Cs2 + -1 + 0 + false + 1 + + + 18 + 0.52 + 0.27 + 0.72 + 0 + Cs2 + -1 + 0 + false + 1 + + + 21 + 0.52 + 0.28 + 0.7 + 0 + Cs2 + -1 + 0 + false + 1 + + + 24 + 0.52 + 0.32 + 0.7 + 0 + Cs3 + -1 + 0 + false + 1 + + + 27 + 0.46 + 0.33 + 0.68 + 0 + Cs2 + -1 + 0 + false + 1 + + + 30 + 0.44 + 0.36 + 0.66 + 0 + D1 + -1 + 0 + false + 1 + + + 33 + 0.44 + 0.37 + 0.62 + 0 + D0 + -1 + 0 + false + 1 + + + 36 + 0.44 + 0.38 + 0.6 + 0 + E0 + -1 + 0 + false + 1 + + + 39 + 0.44 + 0.38 + 0.58 + 0 + E0 + -1 + 0 + false + 1 + + + 42 + 0.44 + 0.38 + 0.56 + 0 + Fs0 + -1 + 0 + false + 1 + + + 45 + 0.44 + 0.38 + 0.52 + 0 + Fs0 + -1 + 0 + false + 1 + + + 48 + 0.44 + 0.38 + 0.5 + 0 + Fs0 + -1 + 0 + false + 1 + + + 51 + 0.44 + 0.39 + 0.5 + 0 + Fs0 + -1 + 0 + false + 1 + + + 54 + 0.44 + 0.42 + 0.5 + 0 + Fs0 + -1 + 0 + false + 1 + + + 57 + 0.44 + 0.43 + 0.5 + 0 + Fs0 + -1 + 0 + false + 1 + + + 60 + 0.44 + 0.44 + 0.5 + 0 + F1 + -1 + 0 + false + 1 + + + 63 + 0.44 + 0.44 + 0.5 + 0 + E2 + -1 + 0 + false + 1 + + + 66 + 0.48 + 0.44 + 0.5 + 0 + Ef2 + -1 + 0 + false + 1 + + + 69 + 0.5 + 0.44 + 0.5 + 0 + Ef2 + -1 + 0 + false + 1 + + + 72 + 0.54 + 0.44 + 0.5 + 0 + Ef1 + -1 + 0 + false + 1 + + + 75 + 0.6 + 0.44 + 0.5 + 0 + Ef1 + -1 + 0 + false + 1 + + + 78 + 0.64 + 0.44 + 0.48 + 0 + Ef0 + -1 + 0 + false + 1 + + + 81 + 0.7 + 0.45 + 0.48 + 0 + Ef0 + -1 + 0 + false + 1 + + + 84 + 0.72 + 0.47 + 0.48 + 0 + Ef0 + -1 + 0 + false + 1 + + + 87 + 0.72 + 0.48 + 0.48 + 0 + Ef-1 + -1 + 0 + false + 1 + + + 90 + 0.72 + 0.49 + 0.48 + 0 + Ef-1 + -1 + 0 + false + 1 + + + 93 + 0.68 + 0.5 + 0.48 + 0 + Ef-1 + -1 + 0 + false + 1 + + + 96 + 0.64 + 0.52 + 0.48 + 0 + E-1 + -1 + 0 + false + 1 + + + 99 + 0.62 + 0.53 + 0.46 + 0 + Fs-1 + -1 + 0 + false + 1 + + + 102 + 0.58 + 0.54 + 0.42 + 0 + G-1 + -1 + 0 + false + 1 + + + 105 + 0.54 + 0.54 + 0.4 + 0 + Af0 + -1 + 0 + false + 1 + + + 108 + 0.54 + 0.54 + 0.38 + 0 + Af0 + -1 + 0 + false + 1 + + + 111 + 0.54 + 0.54 + 0.36 + 0 + Af0 + -1 + 0 + false + 1 + + + 114 + 0.62 + 0.54 + 0.32 + 0 + Af1 + -1 + 0 + false + 1 + + + 117 + 0.72 + 0.54 + 0.3 + 0 + G1 + -1 + 0 + false + 1 + + + 120 + 0.78 + 0.54 + 0.28 + 0 + G1 + -1 + 0 + false + 1 + + + 123 + 0.8 + 0.54 + 0.26 + 0 + Fs1 + -1 + 0 + false + 1 + + + 126 + 0.8 + 0.54 + 0.2 + 0 + F0 + -1 + 0 + false + 1 + + + 129 + 0.8 + 0.54 + 0.2 + 0 + Ef0 + -1 + 0 + false + 1 + + + 132 + 0.8 + 0.54 + 0.18 + 0 + Ef0 + -1 + 0 + false + 1 + + + 135 + 0.78 + 0.54 + 0.16 + 0 + Ef-1 + -1 + 0 + false + 1 + + + 138 + 0.72 + 0.54 + 0.14 + 0 + D-1 + -1 + 0 + false + 1 + + + 141 + 0.64 + 0.54 + 0.1 + 0 + D-1 + -1 + 0 + false + 1 + + + 144 + 0.56 + 0.54 + 0.08 + 0 + D0 + -1 + 0 + false + 1 + + + 147 + 0.54 + 0.54 + 0.08 + 0 + D0 + -1 + 0 + false + 1 + + + 150 + 0.54 + 0.54 + 0.08 + 0 + Ef0 + -1 + 0 + false + 1 + + + 153 + 0.54 + 0.54 + 0.08 + 0 + Ef0 + -1 + 0 + false + 1 + + + 156 + 0.6 + 0.54 + 0.08 + 0 + E1 + -1 + 0 + false + 1 + + + 159 + 0.64 + 0.54 + 0.08 + 0 + F0 + -1 + 0 + false + 1 + + + 162 + 0.7 + 0.54 + 0.08 + 0 + Fs0 + -1 + 0 + false + 1 + + + 165 + 0.72 + 0.54 + 0.08 + 0 + G0 + -1 + 0 + false + 1 + + + 168 + 0.72 + 0.54 + 0.08 + 0 + G-1 + -1 + 0 + false + 1 + + + 171 + 0.72 + 0.54 + 0.08 + 0 + G-1 + -1 + 0 + false + 1 + + + 174 + 0.72 + 0.54 + 0.08 + 0 + G-1 + -1 + 0 + false + 1 + + + 177 + 0.72 + 0.54 + 0.1 + 0 + Fs0 + -1 + 0 + false + 1 + + + 180 + 0.72 + 0.54 + 0.12 + 0 + Fs0 + -1 + 0 + false + 1 + + + 183 + 0.72 + 0.54 + 0.14 + 0 + F0 + -1 + 0 + false + 1 + + + 186 + 0.66 + 0.54 + 0.14 + 0 + E1 + -1 + 0 + false + 1 + + + 189 + 0.58 + 0.54 + -0.3 + 0 + Ef2 + -1 + 0 + false + 1 + + + 192 + 0.54 + 0.54 + -0.22 + 0 + D2 + -1 + 0 + false + 1 + + + 195 + 0.48 + 0.54 + -0.3 + 0 + D2 + -1 + 0 + false + 1 + + + 198 + 0.44 + 0.54 + -0.34 + 0 + D1 + -1 + 0 + false + 1 + + + 201 + 0.44 + 0.54 + -0.38 + 0 + D0 + -1 + 0 + false + 1 + + + 204 + 0.44 + 0.54 + -0.38 + 0 + D0 + -1 + 0 + false + 1 + + + 207 + 0.48 + 0.54 + -0.4 + 0 + D0 + -1 + 0 + false + 1 + + + 210 + 0.5 + 0.55 + -0.46 + 0 + D-1 + -1 + 0 + false + 1 + + + 213 + 0.5 + 0.55 + -0.46 + 0 + D0 + -1 + 0 + false + 1 + + + 216 + 0.48 + 0.55 + -0.48 + 0 + D0 + -1 + 0 + false + 1 + + + 219 + 0.36 + 0.55 + -0.48 + 0 + Ef0 + -1 + 0 + false + 1 + + + 222 + 0.18 + 0.57 + -0.48 + 0 + E0 + -1 + 0 + false + 1 + + + 225 + -0.0599999 + 0.57 + -0.48 + 0 + E1 + -1 + 0 + false + 1 + + + 228 + -0.36 + 0.57 + -0.48 + 0 + Fs2 + -1 + 0 + false + 1 + + + 231 + -0.5 + 0.57 + -0.48 + 0 + Fs2 + -1 + 0 + false + 1 + + + 234 + -0.52 + 0.57 + -0.48 + 0 + Fs3 + -1 + 0 + false + 1 + + + 237 + -0.52 + 0.57 + -0.48 + 0 + Fs1 + -1 + 0 + false + 1 + + + 240 + -0.52 + 0.57 + -0.48 + 0 + F0 + -1 + 0 + false + 1 + + + 243 + -0.48 + 0.57 + -0.48 + 0 + F0 + -1 + 0 + false + 1 + + + 246 + -0.46 + 0.57 + -0.48 + 0 + E0 + -1 + 0 + false + 1 + + + 249 + -0.44 + 0.57 + -0.48 + 0 + Ef0 + -1 + 0 + false + 1 + + + 252 + -0.44 + 0.57 + -0.48 + 0 + Ef0 + -1 + 0 + false + 1 + + + 255 + -0.44 + 0.57 + -0.48 + 0 + Ef0 + -1 + 0 + false + 1 + + + 258 + -0.54 + 0.57 + -0.48 + 0 + Ef1 + -1 + 0 + false + 1 + + + 261 + -0.62 + 0.57 + -0.48 + 0 + Ef1 + -1 + 0 + false + 1 + + + 264 + -0.66 + 0.57 + -0.48 + 0 + Ef2 + -1 + 0 + false + 1 + + + 267 + -0.66 + 0.57 + -0.48 + 0 + E2 + -1 + 0 + false + 1 + + + 270 + -0.58 + 0.57 + -0.5 + 0 + E1 + -1 + 0 + false + 1 + + + 273 + -0.48 + 0.57 + -0.54 + 0 + F0 + -1 + 0 + false + 1 + + + 276 + -0.46 + 0.57 + -0.54 + 0 + Fs0 + -1 + 0 + false + 1 + + + 279 + -0.46 + 0.57 + -0.58 + 0 + Fs0 + -1 + 0 + false + 1 + + + 282 + -0.46 + 0.57 + -0.6 + 0 + Fs0 + -1 + 0 + false + 1 + + + 285 + -0.46 + 0.57 + -0.62 + 0 + F0 + -1 + 0 + false + 1 + + + 288 + -0.5 + 0.57 + -0.66 + 0 + Fs1 + -1 + 0 + false + 1 + + + 291 + -0.62 + 0.59 + -0.68 + 0 + F1 + -1 + 0 + false + 1 + + + 294 + -0.7 + 0.6 + -0.7 + 0 + E2 + -1 + 0 + false + 1 + + + 297 + -0.76 + 0.63 + -0.72 + 0 + F2 + -1 + 0 + false + 1 + + + 300 + -0.76 + 0.64 + -0.76 + 0 + E1 + -1 + 0 + false + 1 + + + 303 + -0.68 + 0.83 + -0.76 + 0 + E0 + -1 + 0 + false + 1 + + + 306 + -0.64 + 0.85 + -0.44 + 0 + Ef0 + -1 + 0 + false + 1 + + + 309 + -0.56 + 0.87 + -0.36 + 0 + Ef0 + -1 + 0 + false + 1 + + + 312 + -0.56 + 0.83 + -0.32 + 0 + D-1 + -1 + 0 + false + 1 + + + 315 + -0.56 + 0.85 + -0.32 + 0 + D-1 + -1 + 0 + false + 1 + + + 318 + -0.56 + 0.89 + -0.32 + 0 + D0 + -1 + 0 + false + 1 + + + 321 + -0.56 + 0.91 + -0.32 + 0 + Ef0 + -1 + 0 + false + 1 + + + 324 + -0.56 + 0.93 + -0.32 + 0 + Ef0 + -1 + 0 + false + 1 + + + 327 + -0.56 + 0.94 + -0.32 + 0 + E1 + -1 + 0 + false + 1 + + + 330 + -0.56 + 0.94 + -0.32 + 0 + E1 + -1 + 0 + false + 1 + + + 333 + -0.56 + 0.94 + -0.32 + 0 + F1 + -1 + 0 + false + 1 + + + 336 + -0.6 + 0.94 + -0.32 + 0 + Fs0 + -1 + 0 + false + 1 + + + 339 + -0.58 + 0.94 + -0.32 + 0 + Fs0 + -1 + 0 + false + 1 + + + 342 + -0.58 + 0.94 + -0.32 + 0 + Fs0 + -1 + 0 + false + 1 + + + 345 + -0.58 + 0.94 + -0.6 + 0 + F-1 + -1 + 0 + false + 1 + + + 348 + -0.62 + 0.94 + -0.6 + 0 + F-1 + -1 + 0 + false + 1 + + + 351 + -0.72 + 0.94 + -0.6 + 0 + E0 + -1 + 0 + false + 1 + + + 354 + -0.72 + 0.94 + -0.6 + 0 + Ef0 + -1 + 0 + false + 1 + + + 357 + -0.72 + 0.94 + -0.6 + 0 + Ef1 + -1 + 0 + false + 1 + + + 360 + -0.66 + 0.94 + -0.6 + 0 + Ef1 + -1 + 0 + false + 1 + + + 363 + -0.6 + 0.94 + -0.6 + 0 + Ef0 + -1 + 0 + false + 1 + + + 366 + -0.6 + 0.94 + -0.6 + 0 + Ef0 + -1 + 0 + false + 1 + + + 369 + -0.6 + 0.93 + -0.6 + 0 + Ef-1 + -1 + 0 + false + 1 + + + 372 + -0.62 + 0.93 + -0.6 + 0 + F-1 + -1 + 0 + false + 1 + + + 375 + -0.66 + 0.93 + -0.6 + 0 + G0 + -1 + 0 + false + 1 + + + 378 + -0.72 + 0.93 + -0.56 + 0 + G0 + -1 + 0 + false + 1 + + + 381 + -0.6 + 0.93 + -0.56 + 0 + Fs1 + -1 + 0 + false + 1 + + + + + Pattern 3 + + not_categorized + 192 + 4 + + + + Pattern 4 + + not_categorized + 192 + 4 + + + + Pattern 5 + + not_categorized + 192 + 4 + + + + Pattern 6 + + not_categorized + 192 + 4 + + + + Pattern 7 + + not_categorized + 192 + 4 + + + + Pattern 8 + + not_categorized + 192 + 4 + + + + Pattern 9 + + not_categorized + 192 + 4 + + + + Pattern 10 + + not_categorized + 192 + 4 + + + + + + + Default properties + + + + + no plugin + - + false + 0 + + + no plugin + - + false + 0 + + + no plugin + - + false + 0 + + + no plugin + - + false + 0 + + + + + + + + diff --git a/src/tests/data/song/AE_loopMode.h2song b/src/tests/data/song/AE_loopMode.h2song new file mode 100644 index 0000000000..b565ac67fd --- /dev/null +++ b/src/tests/data/song/AE_loopMode.h2song @@ -0,0 +1,2494 @@ + + + 1.1.1-'10ea69f2' + 120 + 0.5 + 0.5 + Untitled Song + hydrogen + ... + undefined license + true + true + + false + 0 + 0 + false + false + song + RATIO_STRAIGHT_POLYGONAL + 1.33333 + 0 + 0 + 0 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + + + 0 + Main + 1 + + + + + 0 + Kick + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 1 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 999 + -1 + -1 + 36 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Kick-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Soft.wav + 0.202899 + 0.369565 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Med.wav + 0.369565 + 0.731884 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Hard.wav + 0.731884 + 0.865942 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Hardest.wav + 0.865942 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 1 + Stick + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.99569 + false + false + 1 + 0.7 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 999 + -1 + -1 + 37 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + SideStick-Softest.wav + 0 + 0.188406 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Soft.wav + 0.188406 + 0.402174 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Med.wav + 0.402174 + 0.597826 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Hard.wav + 0.597826 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 2 + Snare + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1.02155 + false + false + 1 + 0.7 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 999 + -1 + -1 + 38 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Snare-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Soft.wav + 0.202899 + 0.376812 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Med.wav + 0.376812 + 0.568841 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Hard.wav + 0.568841 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 3 + Hand Clap + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1.02155 + false + false + 0.7 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 999 + -1 + -1 + 39 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HandClap.wav + 0 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 4 + Snare Rimshot + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 1 + 0.7 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 999 + -1 + -1 + 40 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + SnareRimshot-Softest.wav + 0 + 0.188406 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Soft.wav + 0.188406 + 0.380435 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Med.wav + 0.380435 + 0.594203 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Hard.wav + 0.594203 + 0.728261 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Hardest.wav + 0.728261 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 5 + Floor Tom + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1.04741 + false + false + 0.44 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 41 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + TomFloor-Softest.wav + 0 + 0.199275 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Soft.wav + 0.199275 + 0.394928 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Med.wav + 0.394928 + 0.608696 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Hard.wav + 0.608696 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 6 + Hat Closed + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.685345 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 42 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatClosed-Softest.wav + 0 + 0.173913 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Soft.wav + 0.173913 + 0.373188 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Med.wav + 0.373188 + 0.576087 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Hard.wav + 0.576087 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 7 + Tom 2 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 0.76 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 43 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Tom2-Softest.wav + 0 + 0.177536 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Soft.wav + 0.177536 + 0.373188 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Med.wav + 0.373188 + 0.576087 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Hard.wav + 0.576087 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 8 + Hat Pedal + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.685345 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 44 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatPedal-Softest.wav + 0 + 0.210145 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Soft.wav + 0.210145 + 0.384058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Med.wav + 0.384058 + 0.594203 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Hard.wav + 0.594203 + 0.797101 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Hardest.wav + 0.797101 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 9 + Tom 1 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 1 + 0.8 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 45 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Tom1-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Soft.wav + 0.202899 + 0.394928 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Med.wav + 0.394928 + 0.605072 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Hard.wav + 0.605072 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 10 + Hat Open + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.698276 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 46 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatOpen-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Soft.wav + 0.202899 + 0.398551 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Med.wav + 0.398551 + 0.605072 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Hard.wav + 0.605072 + 0.797101 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Hardest.wav + 0.797101 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 11 + Cowbell + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.568965 + false + false + 1 + 0.32 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 56 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Cowbell-Softest.wav + 0 + 0.184783 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Soft.wav + 0.184783 + 0.384058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Med.wav + 0.384058 + 0.576087 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Hard.wav + 0.576087 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 12 + Ride + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.633621 + false + false + 0.56 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 51 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Ride-Softest.wav + 0 + 0.195652 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Soft.wav + 0.195652 + 0.376812 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Med.wav + 0.376812 + 0.572464 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Hard.wav + 0.572464 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 13 + Crash + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.517241 + false + false + 1 + 0.74 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 49 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Crash-Softest.wav + 0 + 0.199275 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Soft.wav + 0.199275 + 0.384058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Med.wav + 0.384058 + 0.59058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Hard.wav + 0.59058 + 0.789855 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Hardest.wav + 0.789855 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 14 + Ride 2 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 0.5 + 1 + 0 + 0 + 0.6 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 59 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + 24Ride-5.wav + 0.8 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-4.wav + 0.6 + 0.8 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-3.wav + 0.4 + 0.6 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-2.wav + 0.2 + 0.4 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-1.wav + 0 + 0.2 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 15 + Splash + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.543103 + false + false + 0.98 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 55 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Splash-Softest.wav + 0 + 0.195652 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Soft.wav + 0.195652 + 0.362319 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Med.wav + 0.362319 + 0.568841 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Hard.wav + 0.568841 + 0.75 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Hardest.wav + 0.75 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 16 + Hat Semi-Open + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.69 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 82 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatSemiOpen-Softest.wav + 0 + 0.188406 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Soft.wav + 0.188406 + 0.380435 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Med.wav + 0.380435 + 0.57971 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Hard.wav + 0.57971 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 17 + Bell + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.534828 + false + false + 0.56 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 53 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Bell-Softest.wav + 0 + 0.210145 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Soft.wav + 0.210145 + 0.405797 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Med.wav + 0.405797 + 0.605072 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Hard.wav + 0.605072 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + + + Pattern 1 + + not_categorized + 96 + 4 + + + 0 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 6 + 0 + 0.8 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 12 + 0 + 0.8 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 18 + 0 + 0.8 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 24 + 0 + 0.8 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 30 + 0 + 0.8 + 0 + 0 + C0 + -1 + 5 + false + 1 + + + 36 + 0 + 0.8 + 0 + 0 + C0 + -1 + 6 + false + 1 + + + 42 + 0 + 0.8 + 0 + 0 + C0 + -1 + 7 + false + 1 + + + 48 + 0 + 0.8 + 0 + 0 + C0 + -1 + 8 + false + 1 + + + 54 + 0 + 0.8 + 0 + 0 + C0 + -1 + 9 + false + 1 + + + 60 + 0 + 0.8 + 0 + 0 + C0 + -1 + 10 + false + 1 + + + 66 + 0 + 0.8 + 0 + 0 + C0 + -1 + 11 + false + 1 + + + 72 + 0 + 0.8 + 0 + 0 + C0 + -1 + 12 + false + 1 + + + 78 + 0 + 0.8 + 0 + 0 + C0 + -1 + 13 + false + 1 + + + 84 + 0 + 0.8 + 0 + 0 + C0 + -1 + 14 + false + 1 + + + 90 + 0 + 0.8 + 0 + 0 + C0 + -1 + 15 + false + 1 + + + + + Pattern 2 + + not_categorized + 192 + 4 + + + + Pattern 3 + + not_categorized + 192 + 4 + + + + Pattern 4 + + not_categorized + 192 + 4 + + + + Pattern 5 + + not_categorized + 192 + 4 + + + + Pattern 6 + + not_categorized + 192 + 4 + + + + Pattern 7 + + not_categorized + 192 + 4 + + + + Pattern 8 + + not_categorized + 192 + 4 + + + + Pattern 9 + + not_categorized + 192 + 4 + + + + Pattern 10 + + not_categorized + 192 + 4 + + + + + + + Pattern 1 + + + + + no plugin + - + false + 0 + + + no plugin + - + false + 0 + + + no plugin + - + false + 0 + + + no plugin + - + false + 0 + + + + + + + + diff --git a/src/tests/data/song/AE_noteEnqueuing.h2song b/src/tests/data/song/AE_noteEnqueuing.h2song new file mode 100644 index 0000000000..09b1bcdc6b --- /dev/null +++ b/src/tests/data/song/AE_noteEnqueuing.h2song @@ -0,0 +1,3007 @@ + + + 1.1.1-'be336a1d' + 100 + 0.73 + 0.5 + Untitled Song + Unknown + Empty song. + Unknown license + false + true + + false + 0 + 0 + false + true + song + RATIO_STRAIGHT_POLYGONAL + 1.33333 + 0.07 + 0.1 + 0 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + + + 0 + Main + 1 + + + + + 0 + Kick + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 1 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 999 + -1 + -1 + 36 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Kick-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Soft.wav + 0.202899 + 0.369565 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Med.wav + 0.369565 + 0.731884 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Hard.wav + 0.731884 + 0.865942 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Hardest.wav + 0.865942 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 1 + Stick + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.99569 + false + false + 1 + 0.7 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 37 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + SideStick-Softest.wav + 0 + 0.188406 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Soft.wav + 0.188406 + 0.402174 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Med.wav + 0.402174 + 0.597826 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Hard.wav + 0.597826 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 2 + Snare + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1.02155 + false + false + 1 + 0.7 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 38 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Snare-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Soft.wav + 0.202899 + 0.376812 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Med.wav + 0.376812 + 0.568841 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Hard.wav + 0.568841 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 3 + Hand Clap + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1.02155 + false + false + 0.7 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 39 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HandClap.wav + 0 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 4 + Snare Rimshot + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 1 + 0.7 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 40 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + SnareRimshot-Softest.wav + 0 + 0.188406 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Soft.wav + 0.188406 + 0.380435 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Med.wav + 0.380435 + 0.594203 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Hard.wav + 0.594203 + 0.728261 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Hardest.wav + 0.728261 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 5 + Floor Tom + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1.04741 + false + false + 0.44 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 41 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + TomFloor-Softest.wav + 0 + 0.199275 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Soft.wav + 0.199275 + 0.394928 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Med.wav + 0.394928 + 0.608696 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Hard.wav + 0.608696 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 6 + Hat Closed + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.685345 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 42 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatClosed-Softest.wav + 0 + 0.173913 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Soft.wav + 0.173913 + 0.373188 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Med.wav + 0.373188 + 0.576087 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Hard.wav + 0.576087 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 7 + Tom 2 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 0.76 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 43 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Tom2-Softest.wav + 0 + 0.177536 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Soft.wav + 0.177536 + 0.373188 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Med.wav + 0.373188 + 0.576087 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Hard.wav + 0.576087 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 8 + Hat Pedal + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.685345 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 44 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatPedal-Softest.wav + 0 + 0.210145 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Soft.wav + 0.210145 + 0.384058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Med.wav + 0.384058 + 0.594203 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Hard.wav + 0.594203 + 0.797101 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Hardest.wav + 0.797101 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 9 + Tom 1 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 1 + 0.8 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 45 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Tom1-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Soft.wav + 0.202899 + 0.394928 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Med.wav + 0.394928 + 0.605072 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Hard.wav + 0.605072 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 10 + Hat Open + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.698276 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 46 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatOpen-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Soft.wav + 0.202899 + 0.398551 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Med.wav + 0.398551 + 0.605072 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Hard.wav + 0.605072 + 0.797101 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Hardest.wav + 0.797101 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 11 + Cowbell + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.568965 + false + false + 1 + 0.32 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 56 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Cowbell-Softest.wav + 0 + 0.184783 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Soft.wav + 0.184783 + 0.384058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Med.wav + 0.384058 + 0.576087 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Hard.wav + 0.576087 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 12 + Ride + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.633621 + false + false + 0.56 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 51 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Ride-Softest.wav + 0 + 0.195652 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Soft.wav + 0.195652 + 0.376812 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Med.wav + 0.376812 + 0.572464 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Hard.wav + 0.572464 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 13 + Crash + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.517241 + false + false + 1 + 0.74 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 49 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Crash-Softest.wav + 0 + 0.199275 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Soft.wav + 0.199275 + 0.384058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Med.wav + 0.384058 + 0.59058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Hard.wav + 0.59058 + 0.789855 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Hardest.wav + 0.789855 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 14 + Ride 2 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 0.5 + 1 + 0 + 0 + 0.6 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 59 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + 24Ride-5.wav + 0.8 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-4.wav + 0.6 + 0.8 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-3.wav + 0.4 + 0.6 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-2.wav + 0.2 + 0.4 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-1.wav + 0 + 0.2 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 15 + Splash + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.543103 + false + false + 0.98 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 57 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Splash-Softest.wav + 0 + 0.195652 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Soft.wav + 0.195652 + 0.362319 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Med.wav + 0.362319 + 0.568841 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Hard.wav + 0.568841 + 0.75 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Hardest.wav + 0.75 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 16 + Hat Semi-Open + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.69 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 82 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatSemiOpen-Softest.wav + 0 + 0.188406 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Soft.wav + 0.188406 + 0.380435 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Med.wav + 0.380435 + 0.57971 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Hard.wav + 0.57971 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 17 + Bell + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.534828 + false + false + 0.56 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 81 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Bell-Softest.wav + 0 + 0.210145 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Soft.wav + 0.210145 + 0.405797 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Med.wav + 0.405797 + 0.605072 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Hard.wav + 0.605072 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + + + Crash + + not_categorized + 48 + 4 + + + 0 + 0 + 0.8 + 0 + 0 + C0 + -1 + 13 + false + 1 + + + + + Ride + + not_categorized + 432 + 4 + + + 0 + 0 + 0.8 + 0 + 0 + C0 + -1 + 14 + false + 1 + + + 96 + 0 + 0.8 + 0 + 0 + C0 + -1 + 12 + false + 1 + + + + + Toms 1 + + not_categorized + 192 + 4 + + + 0 + 0 + 1 + 0.6 + 0 + C2 + -1 + 9 + false + 1 + + + 48 + 0 + 0.8 + 0.28 + 0 + C1 + -1 + 9 + false + 1 + + + 96 + 0 + 0.57 + -0.24 + 0 + C0 + -1 + 9 + false + 1 + + + 144 + 0 + 0.35 + -0.52 + 0 + C-1 + -1 + 9 + false + 1 + + + + + Toms 2 + + not_categorized + 192 + 4 + + + 0 + 0 + 0.45 + 0.2 + 0 + C2 + -1 + 7 + false + 1 + + + 0 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 6 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 12 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 18 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 24 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 30 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 36 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 42 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 48 + 0 + 0.6 + 0.48 + 0 + C1 + -1 + 7 + false + 1 + + + 48 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 54 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 60 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 66 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 72 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 78 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 84 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 90 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 96 + 0 + 0.77 + 0.66 + 0 + C0 + -1 + 7 + false + 1 + + + 96 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 102 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 108 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 114 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 120 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 126 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 132 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 138 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 144 + 0 + 0.95 + 0.88 + 0 + C-1 + -1 + 7 + false + 1 + + + 144 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 150 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 156 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 162 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 168 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 174 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 180 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 186 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + + + Kick & Snare + + not_categorized + 96 + 4 + + + 0 + 0 + 0.78 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 6 + 0 + 0.8 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 12 + 0 + 0.8 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 18 + 0 + 0.8 + 0 + 0 + C0 + -1 + 17 + false + 1 + + + 24 + 0 + 0.8 + 0 + 0 + C0 + -1 + 15 + false + 1 + + + 30 + 0 + 0.8 + 0 + 0 + C0 + -1 + 11 + false + 1 + + + 36 + 0 + 0.8 + 0 + 0 + C0 + -1 + 8 + false + 1 + + + 48 + 0 + 0.8 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 60 + 0 + 0.8 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + + + Hat + + not_categorized + 192 + 4 + + + 0 + 0 + 0.8 + 0 + 0 + C0 + -1 + 10 + false + 1 + + + 48 + 0 + 0.8 + 0 + 0 + C0 + -1 + 8 + false + 1 + + + 96 + 0 + 0.8 + 0 + 0 + C0 + -1 + 6 + false + 1 + + + 144 + 0 + 0.8 + 0 + 0 + C0 + -1 + 16 + false + 1 + + + + + Pattern 7 + + not_categorized + 192 + 4 + + + + Pattern 8 + + not_categorized + 192 + 4 + + + + Pattern 9 + + not_categorized + 192 + 4 + + + + Pattern 10 + + not_categorized + 192 + 4 + + + + + + + Crash + + + Ride + + + Toms 1 + + + Toms 2 + + + Kick & Snare + + + Hat + + + + + no plugin + - + false + 0 + + + no plugin + - + false + 0 + + + no plugin + - + false + 0 + + + no plugin + - + false + 0 + + + + + 0 + 100 + + + 2 + 180 + + + 4 + 70 + + + + + + + diff --git a/src/tests/data/song/AE_noteEnqueuingTimeline.h2song b/src/tests/data/song/AE_noteEnqueuingTimeline.h2song new file mode 100644 index 0000000000..f9a29b283e --- /dev/null +++ b/src/tests/data/song/AE_noteEnqueuingTimeline.h2song @@ -0,0 +1,5567 @@ + + + 1.1.1-'bc740855' + 120 + 1 + 0.5 + Untitled Song + hydrogen + ... + undefined license + false + true + + false + 0 + 0 + false + true + song + RATIO_STRAIGHT_POLYGONAL + 1.33333 + 0 + 0 + 0 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + + + 0 + Main + 1 + + + + + 0 + 1 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 1 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 999 + -1 + -1 + 36 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Kick-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Soft.wav + 0.202899 + 0.369565 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Med.wav + 0.369565 + 0.731884 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Hard.wav + 0.731884 + 0.865942 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Hardest.wav + 0.865942 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 1 + 2 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.99569 + false + false + 1 + 0.7 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 999 + -1 + -1 + 37 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + SideStick-Softest.wav + 0 + 0.188406 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Soft.wav + 0.188406 + 0.402174 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Med.wav + 0.402174 + 0.597826 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Hard.wav + 0.597826 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 2 + 3 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1.02155 + false + false + 1 + 0.7 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 999 + -1 + -1 + 38 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Snare-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Soft.wav + 0.202899 + 0.376812 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Med.wav + 0.376812 + 0.568841 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Hard.wav + 0.568841 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 3 + 4 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1.02155 + false + false + 0.7 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 999 + -1 + -1 + 39 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HandClap.wav + 0 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 4 + 5 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 1 + 0.7 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 999 + -1 + -1 + 40 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + SnareRimshot-Softest.wav + 0 + 0.188406 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Soft.wav + 0.188406 + 0.380435 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Med.wav + 0.380435 + 0.594203 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Hard.wav + 0.594203 + 0.728261 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Hardest.wav + 0.728261 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 5 + Floor Tom + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1.04741 + false + false + 0.44 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 41 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + TomFloor-Softest.wav + 0 + 0.199275 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Soft.wav + 0.199275 + 0.394928 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Med.wav + 0.394928 + 0.608696 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Hard.wav + 0.608696 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 6 + Hat Closed + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.685345 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 42 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatClosed-Softest.wav + 0 + 0.173913 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Soft.wav + 0.173913 + 0.373188 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Med.wav + 0.373188 + 0.576087 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Hard.wav + 0.576087 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 7 + Tom 2 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 0.76 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 43 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Tom2-Softest.wav + 0 + 0.177536 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Soft.wav + 0.177536 + 0.373188 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Med.wav + 0.373188 + 0.576087 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Hard.wav + 0.576087 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 8 + Hat Pedal + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.685345 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 44 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatPedal-Softest.wav + 0 + 0.210145 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Soft.wav + 0.210145 + 0.384058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Med.wav + 0.384058 + 0.594203 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Hard.wav + 0.594203 + 0.797101 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Hardest.wav + 0.797101 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 9 + Tom 1 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 1 + 0.8 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 45 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Tom1-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Soft.wav + 0.202899 + 0.394928 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Med.wav + 0.394928 + 0.605072 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Hard.wav + 0.605072 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 10 + Hat Open + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.698276 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 46 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatOpen-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Soft.wav + 0.202899 + 0.398551 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Med.wav + 0.398551 + 0.605072 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Hard.wav + 0.605072 + 0.797101 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Hardest.wav + 0.797101 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 11 + Cowbell + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.568965 + false + false + 1 + 0.32 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 56 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Cowbell-Softest.wav + 0 + 0.184783 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Soft.wav + 0.184783 + 0.384058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Med.wav + 0.384058 + 0.576087 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Hard.wav + 0.576087 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 12 + Ride + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.633621 + false + false + 0.56 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 51 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Ride-Softest.wav + 0 + 0.195652 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Soft.wav + 0.195652 + 0.376812 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Med.wav + 0.376812 + 0.572464 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Hard.wav + 0.572464 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 13 + Crash + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.517241 + false + false + 1 + 0.74 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 49 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Crash-Softest.wav + 0 + 0.199275 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Soft.wav + 0.199275 + 0.384058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Med.wav + 0.384058 + 0.59058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Hard.wav + 0.59058 + 0.789855 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Hardest.wav + 0.789855 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 14 + Ride 2 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 0.5 + 1 + 0 + 0 + 0.6 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 59 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + 24Ride-5.wav + 0.8 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-4.wav + 0.6 + 0.8 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-3.wav + 0.4 + 0.6 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-2.wav + 0.2 + 0.4 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-1.wav + 0 + 0.2 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 15 + Splash + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.543103 + false + false + 0.98 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 55 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Splash-Softest.wav + 0 + 0.195652 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Soft.wav + 0.195652 + 0.362319 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Med.wav + 0.362319 + 0.568841 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Hard.wav + 0.568841 + 0.75 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Hardest.wav + 0.75 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 16 + Hat Semi-Open + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.69 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 82 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatSemiOpen-Softest.wav + 0 + 0.188406 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Soft.wav + 0.188406 + 0.380435 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Med.wav + 0.380435 + 0.57971 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Hard.wav + 0.57971 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 17 + Bell + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.534828 + false + false + 0.56 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 999 + -1 + -1 + 53 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Bell-Softest.wav + 0 + 0.210145 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Soft.wav + 0.210145 + 0.405797 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Med.wav + 0.405797 + 0.605072 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Hard.wav + 0.605072 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + + + 1 + + not_categorized + 48 + 4 + + + 0 + 0 + 0.14 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 1 + 0 + 0.2 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 2 + 0 + 0.22 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 3 + 0 + 0.23 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 4 + 0 + 0.24 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 5 + 0 + 0.25 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 6 + 0 + 0.28 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 7 + 0 + 0.29 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 8 + 0 + 0.3 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 9 + 0 + 0.33 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 10 + 0 + 0.34 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 11 + 0 + 0.36 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 12 + 0 + 0.37 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 13 + 0 + 0.39 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 14 + 0 + 0.4 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 15 + 0 + 0.41 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 16 + 0 + 0.43 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 17 + 0 + 0.44 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 18 + 0 + 0.45 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 19 + 0 + 0.46 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 20 + 0 + 0.48 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 21 + 0 + 0.49 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 22 + 0 + 0.51 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 23 + 0 + 0.52 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 24 + 0 + 0.53 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 25 + 0 + 0.55 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 26 + 0 + 0.55 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 27 + 0 + 0.57 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 28 + 0 + 0.58 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 29 + 0 + 0.58 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 30 + 0 + 0.62 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 31 + 0 + 0.65 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 32 + 0 + 0.64 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 33 + 0 + 0.65 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 34 + 0 + 0.66 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 35 + 0 + 0.67 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 36 + 0 + 0.67 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 37 + 0 + 0.69 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 38 + 0 + 0.71 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 39 + 0 + 0.72 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 40 + 0 + 0.72 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 41 + 0 + 0.73 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 42 + 0 + 0.75 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 43 + 0 + 0.76 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 44 + 0 + 0.76 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 45 + 0 + 0.77 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 46 + 0 + 0.77 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 47 + 0 + 0.78 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + + + 2 + + not_categorized + 58 + 4 + + + 0 + 0 + 0.03 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 1 + 0 + 0.12 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 2 + 0 + 0.12 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 3 + 0 + 0.13 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 4 + 0 + 0.14 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 5 + 0 + 0.14 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 6 + 0 + 0.15 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 7 + 0 + 0.17 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 8 + 0 + 0.17 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 9 + 0 + 0.18 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 10 + 0 + 0.19 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 11 + 0 + 0.19 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 12 + 0 + 0.2 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 13 + 0 + 0.22 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 14 + 0 + 0.23 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 15 + 0 + 0.24 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 16 + 0 + 0.25 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 17 + 0 + 0.25 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 18 + 0 + 0.27 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 19 + 0 + 0.27 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 20 + 0 + 0.28 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 21 + 0 + 0.29 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 22 + 0 + 0.29 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 23 + 0 + 0.3 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 24 + 0 + 0.32 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 25 + 0 + 0.32 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 26 + 0 + 0.33 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 27 + 0 + 0.34 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 28 + 0 + 0.35 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 29 + 0 + 0.36 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 30 + 0 + 0.38 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 31 + 0 + 0.38 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 32 + 0 + 0.4 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 33 + 0 + 0.42 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 34 + 0 + 0.43 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 35 + 0 + 0.44 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 36 + 0 + 0.45 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 37 + 0 + 0.45 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 38 + 0 + 0.47 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 39 + 0 + 0.48 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 40 + 0 + 0.49 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 41 + 0 + 0.5 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 42 + 0 + 0.5 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 43 + 0 + 0.5 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 44 + 0 + 0.52 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 45 + 0 + 0.52 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 46 + 0 + 0.52 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 47 + 0 + 0.53 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 48 + 0 + 0.53 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 49 + 0 + 0.55 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 50 + 0 + 0.56 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 51 + 0 + 0.57 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 52 + 0 + 0.58 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 53 + 0 + 0.59 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 54 + 0 + 0.62 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 55 + 0 + 0.63 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 56 + 0 + 0.67 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + 57 + 0 + 0.67 + 0 + 0 + C0 + -1 + 1 + false + 1 + + + + + 3 + + not_categorized + 48 + 4 + + + 0 + 0 + 0.05 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 1 + 0 + 0.05 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 2 + 0 + 0.06 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 3 + 0 + 0.07 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 4 + 0 + 0.08 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 5 + 0 + 0.08 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 6 + 0 + 0.09 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 7 + 0 + 0.1 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 8 + 0 + 0.1 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 9 + 0 + 0.12 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 10 + 0 + 0.13 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 11 + 0 + 0.13 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 12 + 0 + 0.15 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 13 + 0 + 0.15 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 14 + 0 + 0.17 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 15 + 0 + 0.19 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 16 + 0 + 0.2 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 17 + 0 + 0.2 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 18 + 0 + 0.22 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 19 + 0 + 0.23 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 20 + 0 + 0.24 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 21 + 0 + 0.25 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 22 + 0 + 0.27 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 23 + 0 + 0.29 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 24 + 0 + 0.3 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 25 + 0 + 0.33 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 26 + 0 + 0.34 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 27 + 0 + 0.34 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 28 + 0 + 0.36 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 29 + 0 + 0.39 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 30 + 0 + 0.41 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 31 + 0 + 0.43 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 32 + 0 + 0.45 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 33 + 0 + 0.46 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 34 + 0 + 0.48 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 35 + 0 + 0.49 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 36 + 0 + 0.5 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 37 + 0 + 0.52 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 38 + 0 + 0.53 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 39 + 0 + 0.54 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 40 + 0 + 0.55 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 41 + 0 + 0.56 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 42 + 0 + 0.57 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 43 + 0 + 0.58 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 44 + 0 + 0.58 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 45 + 0 + 0.6 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 46 + 0 + 0.6 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + 47 + 0 + 0.63 + 0 + 0 + C0 + -1 + 2 + false + 1 + + + + + 4 + + not_categorized + 67 + 4 + + + 0 + 0 + 0.02 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 1 + 0 + 0.13 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 2 + 0 + 0.14 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 3 + 0 + 0.15 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 4 + 0 + 0.17 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 5 + 0 + 0.17 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 6 + 0 + 0.18 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 7 + 0 + 0.18 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 8 + 0 + 0.19 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 9 + 0 + 0.2 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 10 + 0 + 0.21 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 11 + 0 + 0.22 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 12 + 0 + 0.23 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 13 + 0 + 0.24 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 14 + 0 + 0.27 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 15 + 0 + 0.27 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 16 + 0 + 0.27 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 17 + 0 + 0.29 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 18 + 0 + 0.29 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 19 + 0 + 0.3 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 20 + 0 + 0.32 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 21 + 0 + 0.32 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 22 + 0 + 0.34 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 23 + 0 + 0.34 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 24 + 0 + 0.35 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 25 + 0 + 0.37 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 26 + 0 + 0.37 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 27 + 0 + 0.38 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 28 + 0 + 0.38 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 29 + 0 + 0.39 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 30 + 0 + 0.4 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 31 + 0 + 0.42 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 32 + 0 + 0.42 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 33 + 0 + 0.42 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 34 + 0 + 0.43 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 35 + 0 + 0.43 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 36 + 0 + 0.44 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 37 + 0 + 0.45 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 38 + 0 + 0.45 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 39 + 0 + 0.47 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 40 + 0 + 0.48 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 41 + 0 + 0.48 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 42 + 0 + 0.49 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 43 + 0 + 0.5 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 44 + 0 + 0.52 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 45 + 0 + 0.53 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 46 + 0 + 0.54 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 47 + 0 + 0.55 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 48 + 0 + 0.55 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 49 + 0 + 0.57 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 50 + 0 + 0.57 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 51 + 0 + 0.59 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 52 + 0 + 0.6 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 53 + 0 + 0.62 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 54 + 0 + 0.63 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 55 + 0 + 0.63 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 56 + 0 + 0.64 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 57 + 0 + 0.65 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 58 + 0 + 0.67 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 59 + 0 + 0.67 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 60 + 0 + 0.69 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 61 + 0 + 0.7 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 62 + 0 + 0.72 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 63 + 0 + 0.72 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 64 + 0 + 0.73 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 65 + 0 + 0.73 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + 66 + 0 + 0.74 + 0 + 0 + C0 + -1 + 3 + false + 1 + + + + + 5 + + not_categorized + 48 + 4 + + + 0 + 0 + 0.11 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 1 + 0 + 0.18 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 2 + 0 + 0.19 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 3 + 0 + 0.21 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 4 + 0 + 0.22 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 5 + 0 + 0.25 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 6 + 0 + 0.25 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 7 + 0 + 0.26 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 8 + 0 + 0.27 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 9 + 0 + 0.29 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 10 + 0 + 0.3 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 11 + 0 + 0.32 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 12 + 0 + 0.33 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 13 + 0 + 0.35 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 14 + 0 + 0.36 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 15 + 0 + 0.37 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 16 + 0 + 0.39 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 17 + 0 + 0.41 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 18 + 0 + 0.43 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 19 + 0 + 0.45 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 20 + 0 + 0.48 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 21 + 0 + 0.49 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 22 + 0 + 0.51 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 23 + 0 + 0.53 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 24 + 0 + 0.54 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 25 + 0 + 0.55 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 26 + 0 + 0.55 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 27 + 0 + 0.55 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 28 + 0 + 0.56 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 29 + 0 + 0.57 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 30 + 0 + 0.57 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 31 + 0 + 0.59 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 32 + 0 + 0.59 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 33 + 0 + 0.61 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 34 + 0 + 0.61 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 35 + 0 + 0.62 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 36 + 0 + 0.64 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 37 + 0 + 0.66 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 38 + 0 + 0.66 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 39 + 0 + 0.67 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 40 + 0 + 0.67 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 41 + 0 + 0.67 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 42 + 0 + 0.69 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 43 + 0 + 0.69 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 44 + 0 + 0.69 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 45 + 0 + 0.7 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 46 + 0 + 0.71 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + 47 + 0 + 0.71 + 0 + 0 + C0 + -1 + 4 + false + 1 + + + + + Pattern 6 + + not_categorized + 192 + 4 + + + + Pattern 7 + + not_categorized + 192 + 4 + + + + Pattern 8 + + not_categorized + 192 + 4 + + + + Pattern 9 + + not_categorized + 192 + 4 + + + + Pattern 10 + + not_categorized + 192 + 4 + + + + + + + 1 + + + 2 + + + 3 + + + 4 + + + 5 + + + + + no plugin + - + false + 0 + + + no plugin + - + false + 0 + + + no plugin + - + false + 0 + + + no plugin + - + false + 0 + + + + + 0 + 100 + + + 1 + 280 + + + 2 + 90 + + + 3 + 300 + + + 4 + 30 + + + + + + + diff --git a/src/tests/data/song/AE_playbackTrack.h2song b/src/tests/data/song/AE_playbackTrack.h2song new file mode 100644 index 0000000000..d90d5cc985 --- /dev/null +++ b/src/tests/data/song/AE_playbackTrack.h2song @@ -0,0 +1,2341 @@ + + + 1.1.1-'bc740855' + 120 + 1 + 0.5 + Untitled Song + hydrogen + ... + undefined license + false + true + ./res/playbackTrack.flac + true + 1 + 0 + false + true + song + RATIO_STRAIGHT_POLYGONAL + 1.33333 + 0 + 0 + 0 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + + + 0 + Main + 1 + + + + + 0 + Kick + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 1 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 999 + -1 + -1 + 36 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Kick-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Soft.wav + 0.202899 + 0.369565 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Med.wav + 0.369565 + 0.731884 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Hard.wav + 0.731884 + 0.865942 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Hardest.wav + 0.865942 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 1 + Stick + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.99569 + false + false + 1 + 0.7 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 37 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + SideStick-Softest.wav + 0 + 0.188406 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Soft.wav + 0.188406 + 0.402174 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Med.wav + 0.402174 + 0.597826 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Hard.wav + 0.597826 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 2 + Snare + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1.02155 + false + false + 1 + 0.7 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 38 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Snare-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Soft.wav + 0.202899 + 0.376812 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Med.wav + 0.376812 + 0.568841 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Hard.wav + 0.568841 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 3 + Hand Clap + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1.02155 + false + false + 0.7 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 39 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HandClap.wav + 0 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 4 + Snare Rimshot + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 1 + 0.7 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 40 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + SnareRimshot-Softest.wav + 0 + 0.188406 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Soft.wav + 0.188406 + 0.380435 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Med.wav + 0.380435 + 0.594203 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Hard.wav + 0.594203 + 0.728261 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Hardest.wav + 0.728261 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 5 + Floor Tom + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1.04741 + false + false + 0.44 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 41 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + TomFloor-Softest.wav + 0 + 0.199275 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Soft.wav + 0.199275 + 0.394928 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Med.wav + 0.394928 + 0.608696 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Hard.wav + 0.608696 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 6 + Hat Closed + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.685345 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 42 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatClosed-Softest.wav + 0 + 0.173913 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Soft.wav + 0.173913 + 0.373188 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Med.wav + 0.373188 + 0.576087 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Hard.wav + 0.576087 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 7 + Tom 2 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 0.76 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 43 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Tom2-Softest.wav + 0 + 0.177536 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Soft.wav + 0.177536 + 0.373188 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Med.wav + 0.373188 + 0.576087 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Hard.wav + 0.576087 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 8 + Hat Pedal + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.685345 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 44 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatPedal-Softest.wav + 0 + 0.210145 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Soft.wav + 0.210145 + 0.384058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Med.wav + 0.384058 + 0.594203 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Hard.wav + 0.594203 + 0.797101 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Hardest.wav + 0.797101 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 9 + Tom 1 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 1 + 0.8 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 45 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Tom1-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Soft.wav + 0.202899 + 0.394928 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Med.wav + 0.394928 + 0.605072 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Hard.wav + 0.605072 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 10 + Hat Open + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.698276 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 46 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatOpen-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Soft.wav + 0.202899 + 0.398551 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Med.wav + 0.398551 + 0.605072 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Hard.wav + 0.605072 + 0.797101 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Hardest.wav + 0.797101 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 11 + Cowbell + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.568965 + false + false + 1 + 0.32 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 56 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Cowbell-Softest.wav + 0 + 0.184783 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Soft.wav + 0.184783 + 0.384058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Med.wav + 0.384058 + 0.576087 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Hard.wav + 0.576087 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 12 + Ride + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.633621 + false + false + 0.56 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 51 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Ride-Softest.wav + 0 + 0.195652 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Soft.wav + 0.195652 + 0.376812 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Med.wav + 0.376812 + 0.572464 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Hard.wav + 0.572464 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 13 + Crash + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.517241 + false + false + 1 + 0.74 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 49 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Crash-Softest.wav + 0 + 0.199275 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Soft.wav + 0.199275 + 0.384058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Med.wav + 0.384058 + 0.59058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Hard.wav + 0.59058 + 0.789855 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Hardest.wav + 0.789855 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 14 + Ride 2 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 0.5 + 1 + 0 + 0 + 0.6 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 59 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + 24Ride-5.wav + 0.8 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-4.wav + 0.6 + 0.8 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-3.wav + 0.4 + 0.6 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-2.wav + 0.2 + 0.4 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-1.wav + 0 + 0.2 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 15 + Splash + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.543103 + false + false + 0.98 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 55 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Splash-Softest.wav + 0 + 0.195652 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Soft.wav + 0.195652 + 0.362319 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Med.wav + 0.362319 + 0.568841 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Hard.wav + 0.568841 + 0.75 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Hardest.wav + 0.75 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 16 + Hat Semi-Open + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.69 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 82 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatSemiOpen-Softest.wav + 0 + 0.188406 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Soft.wav + 0.188406 + 0.380435 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Med.wav + 0.380435 + 0.57971 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Hard.wav + 0.57971 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 17 + Bell + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.534828 + false + false + 0.56 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 999 + -1 + -1 + 53 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Bell-Softest.wav + 0 + 0.210145 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Soft.wav + 0.210145 + 0.405797 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Med.wav + 0.405797 + 0.605072 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Hard.wav + 0.605072 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + + + Pattern 1 + + not_categorized + 48 + 4 + + + + Pattern 2 + + not_categorized + 192 + 4 + + + + Pattern 3 + + not_categorized + 192 + 4 + + + + Pattern 4 + + not_categorized + 192 + 4 + + + + Pattern 5 + + not_categorized + 192 + 4 + + + + Pattern 6 + + not_categorized + 192 + 4 + + + + Pattern 7 + + not_categorized + 192 + 4 + + + + Pattern 8 + + not_categorized + 192 + 4 + + + + Pattern 9 + + not_categorized + 192 + 4 + + + + Pattern 10 + + not_categorized + 192 + 4 + + + + + + + Pattern 1 + + + Pattern 1 + + + Pattern 1 + + + Pattern 1 + + + Pattern 1 + + + Pattern 1 + + + + + no plugin + - + false + 0 + + + no plugin + - + false + 0 + + + no plugin + - + false + 0 + + + no plugin + - + false + 0 + + + + + 0 + 120 + + + 1 + 30 + + + 2 + 300 + + + 3 + 100 + + + 4 + 200 + + + 5 + 65 + + + + + + + diff --git a/src/tests/data/song/AE_sampleConsistency.h2song b/src/tests/data/song/AE_sampleConsistency.h2song new file mode 100644 index 0000000000..052cc0b3cf --- /dev/null +++ b/src/tests/data/song/AE_sampleConsistency.h2song @@ -0,0 +1,2387 @@ + + + 1.1.1-'bc740855' + 120 + 1 + 0.5 + Untitled Song + hydrogen + ... + undefined license + false + true + + false + 0 + 0 + false + false + song + RATIO_STRAIGHT_POLYGONAL + 1.33333 + 0 + 0 + 0 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + + + 0 + Main + 1 + + + + + 18 + Sample + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 1 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 999 + -1 + -1 + 54 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + /home/phil/git/hydrogen/src/tests/data/song/res/playbackTrack.flac + 0 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 0 + Kick + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 1 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 999 + -1 + -1 + 36 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Kick-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Soft.wav + 0.202899 + 0.369565 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Med.wav + 0.369565 + 0.731884 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Hard.wav + 0.731884 + 0.865942 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Hardest.wav + 0.865942 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 1 + Stick + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.99569 + false + false + 1 + 0.7 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 37 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + SideStick-Softest.wav + 0 + 0.188406 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Soft.wav + 0.188406 + 0.402174 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Med.wav + 0.402174 + 0.597826 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Hard.wav + 0.597826 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 2 + Snare + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1.02155 + false + false + 1 + 0.7 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 38 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Snare-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Soft.wav + 0.202899 + 0.376812 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Med.wav + 0.376812 + 0.568841 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Hard.wav + 0.568841 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 3 + Hand Clap + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1.02155 + false + false + 0.7 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 39 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HandClap.wav + 0 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 4 + Snare Rimshot + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 1 + 0.7 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 40 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + SnareRimshot-Softest.wav + 0 + 0.188406 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Soft.wav + 0.188406 + 0.380435 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Med.wav + 0.380435 + 0.594203 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Hard.wav + 0.594203 + 0.728261 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Hardest.wav + 0.728261 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 5 + Floor Tom + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1.04741 + false + false + 0.44 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 41 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + TomFloor-Softest.wav + 0 + 0.199275 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Soft.wav + 0.199275 + 0.394928 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Med.wav + 0.394928 + 0.608696 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Hard.wav + 0.608696 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 6 + Hat Closed + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.685345 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 42 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatClosed-Softest.wav + 0 + 0.173913 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Soft.wav + 0.173913 + 0.373188 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Med.wav + 0.373188 + 0.576087 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Hard.wav + 0.576087 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 7 + Tom 2 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 0.76 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 43 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Tom2-Softest.wav + 0 + 0.177536 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Soft.wav + 0.177536 + 0.373188 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Med.wav + 0.373188 + 0.576087 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Hard.wav + 0.576087 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 8 + Hat Pedal + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.685345 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 44 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatPedal-Softest.wav + 0 + 0.210145 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Soft.wav + 0.210145 + 0.384058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Med.wav + 0.384058 + 0.594203 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Hard.wav + 0.594203 + 0.797101 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Hardest.wav + 0.797101 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 9 + Tom 1 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 1 + 0.8 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 45 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Tom1-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Soft.wav + 0.202899 + 0.394928 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Med.wav + 0.394928 + 0.605072 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Hard.wav + 0.605072 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 10 + Hat Open + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.698276 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 46 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatOpen-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Soft.wav + 0.202899 + 0.398551 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Med.wav + 0.398551 + 0.605072 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Hard.wav + 0.605072 + 0.797101 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Hardest.wav + 0.797101 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 11 + Cowbell + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.568965 + false + false + 1 + 0.32 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 56 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Cowbell-Softest.wav + 0 + 0.184783 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Soft.wav + 0.184783 + 0.384058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Med.wav + 0.384058 + 0.576087 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Hard.wav + 0.576087 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 12 + Ride + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.633621 + false + false + 0.56 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 51 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Ride-Softest.wav + 0 + 0.195652 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Soft.wav + 0.195652 + 0.376812 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Med.wav + 0.376812 + 0.572464 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Hard.wav + 0.572464 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 13 + Crash + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.517241 + false + false + 1 + 0.74 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 49 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Crash-Softest.wav + 0 + 0.199275 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Soft.wav + 0.199275 + 0.384058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Med.wav + 0.384058 + 0.59058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Hard.wav + 0.59058 + 0.789855 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Hardest.wav + 0.789855 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 14 + Ride 2 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 0.5 + 1 + 0 + 0 + 0.6 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 59 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + 24Ride-5.wav + 0.8 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-4.wav + 0.6 + 0.8 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-3.wav + 0.4 + 0.6 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-2.wav + 0.2 + 0.4 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-1.wav + 0 + 0.2 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 15 + Splash + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.543103 + false + false + 0.98 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 55 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Splash-Softest.wav + 0 + 0.195652 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Soft.wav + 0.195652 + 0.362319 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Med.wav + 0.362319 + 0.568841 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Hard.wav + 0.568841 + 0.75 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Hardest.wav + 0.75 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 16 + Hat Semi-Open + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.69 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 82 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatSemiOpen-Softest.wav + 0 + 0.188406 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Soft.wav + 0.188406 + 0.380435 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Med.wav + 0.380435 + 0.57971 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Hard.wav + 0.57971 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 17 + Bell + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.534828 + false + false + 0.56 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 999 + -1 + -1 + 53 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Bell-Softest.wav + 0 + 0.210145 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Soft.wav + 0.210145 + 0.405797 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Med.wav + 0.405797 + 0.605072 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Hard.wav + 0.605072 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + + + Sample + + not_categorized + 48 + 4 + + + 0 + 0 + 0.8 + 0 + 0 + C0 + -1 + 18 + false + 1 + + + + + Pattern 2 + + not_categorized + 48 + 4 + + + + Pattern 3 + + not_categorized + 192 + 4 + + + + Pattern 4 + + not_categorized + 192 + 4 + + + + Pattern 5 + + not_categorized + 192 + 4 + + + + Pattern 6 + + not_categorized + 192 + 4 + + + + Pattern 7 + + not_categorized + 192 + 4 + + + + Pattern 8 + + not_categorized + 192 + 4 + + + + Pattern 9 + + not_categorized + 192 + 4 + + + + Pattern 10 + + not_categorized + 192 + 4 + + + + + + + Sample + + + Pattern 2 + + + Pattern 2 + + + Pattern 2 + + + Pattern 2 + + + Pattern 2 + + + Pattern 2 + + + + + no plugin + - + false + 0 + + + no plugin + - + false + 0 + + + no plugin + - + false + 0 + + + no plugin + - + false + 0 + + + + + + + + diff --git a/src/tests/data/song/AE_songSizeChanged.h2song b/src/tests/data/song/AE_songSizeChanged.h2song index 522ca95e4f..992adc32ed 100644 --- a/src/tests/data/song/AE_songSizeChanged.h2song +++ b/src/tests/data/song/AE_songSizeChanged.h2song @@ -1,6 +1,6 @@ - 1.1.1-'dc98157c' + 1.1.1-'be336a1d' 100 0.73 0.5 @@ -15,13 +15,15 @@ 0 0 false - true + false song RATIO_STRAIGHT_POLYGONAL 1.33333 - 0.07 - 0.1 + 1 + 1 0 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit 0 @@ -33,40 +35,45 @@ 0 Kick + /usr/local/share/hydrogen/data/drumkits/GMRockKit GMRockKit - 2 1 false false - 0 + 1 + 1 + 0 + 0 1 true false 1 0 - 0 - 0 - 0 - 0 0 0 1 999 - 0 - 0 -1 - false - VELOCITY -1 36 + false + VELOCITY -1 0 127 + 0 + 0 + 0 + 0 0 1 Kick-Softest.wav + 0 + 0.202899 + 1 + 0 false forward 0 @@ -77,13 +84,13 @@ 1 4 1 - 0 - 0.202899 - 1 - 0 Kick-Soft.wav + 0.202899 + 0.369565 + 1 + 0 false forward 0 @@ -94,13 +101,13 @@ 1 4 1 - 0.202899 - 0.369565 - 1 - 0 Kick-Med.wav + 0.369565 + 0.731884 + 1 + 0 false forward 0 @@ -111,13 +118,13 @@ 1 4 1 - 0.369565 - 0.731884 - 1 - 0 Kick-Hard.wav + 0.731884 + 0.865942 + 1 + 0 false forward 0 @@ -128,13 +135,13 @@ 1 4 1 - 0.731884 - 0.865942 - 1 - 0 Kick-Hardest.wav + 0.865942 + 1 + 1 + 0 false forward 0 @@ -145,50 +152,51 @@ 1 4 1 - 0.865942 - 1 - 1 - 0 1 Stick + /usr/local/share/hydrogen/data/drumkits/GMRockKit GMRockKit - 2 0.99569 false false - -0.3 + 1 + 0.7 + 0 + 0 1 true false 1 0 - 0 - 0 - 0 - 0 0 0 1 - 1000 - 0 - 0 + 999 -1 - false - VELOCITY -1 37 + false + VELOCITY -1 0 127 + 0 + 0 + 0 + 0 0 1 SideStick-Softest.wav + 0 + 0.188406 + 1 + 0 false forward 0 @@ -199,13 +207,13 @@ 1 4 1 - 0 - 0.188406 - 1 - 0 SideStick-Soft.wav + 0.188406 + 0.402174 + 1 + 0 false forward 0 @@ -216,13 +224,13 @@ 1 4 1 - 0.188406 - 0.402174 - 1 - 0 SideStick-Med.wav + 0.402174 + 0.597826 + 1 + 0 false forward 0 @@ -233,13 +241,13 @@ 1 4 1 - 0.402174 - 0.597826 - 1 - 0 SideStick-Hard.wav + 0.597826 + 0.782609 + 1 + 0 false forward 0 @@ -250,13 +258,13 @@ 1 4 1 - 0.597826 - 0.782609 - 1 - 0 SideStick-Hardest.wav + 0.782609 + 1 + 1 + 0 false forward 0 @@ -267,50 +275,51 @@ 1 4 1 - 0.782609 - 1 - 1 - 0 2 Snare + /usr/local/share/hydrogen/data/drumkits/GMRockKit GMRockKit - 2 1.02155 false false - -0.3 + 1 + 0.7 + 0 + 0 1 true false 1 0 - 0 - 0 - 0 - 0 0 0 1 - 1000 - 0 - 0 + 999 -1 - false - VELOCITY -1 38 + false + VELOCITY -1 0 127 + 0 + 0 + 0 + 0 0 1 Snare-Softest.wav + 0 + 0.202899 + 1 + 0 false forward 0 @@ -321,13 +330,13 @@ 1 4 1 - 0 - 0.202899 - 1 - 0 Snare-Soft.wav + 0.202899 + 0.376812 + 1 + 0 false forward 0 @@ -338,13 +347,13 @@ 1 4 1 - 0.202899 - 0.376812 - 1 - 0 Snare-Med.wav + 0.376812 + 0.568841 + 1 + 0 false forward 0 @@ -355,13 +364,13 @@ 1 4 1 - 0.376812 - 0.568841 - 1 - 0 Snare-Hard.wav + 0.568841 + 0.782609 + 1 + 0 false forward 0 @@ -372,13 +381,13 @@ 1 4 1 - 0.568841 - 0.782609 - 1 - 0 Snare-Hardest.wav + 0.782609 + 1 + 1 + 0 false forward 0 @@ -389,50 +398,51 @@ 1 4 1 - 0.782609 - 1 - 1 - 0 3 Hand Clap + /usr/local/share/hydrogen/data/drumkits/GMRockKit GMRockKit - 2 1.02155 false false - 0.3 + 0.7 + 1 + 0 + 0 1 true false 1 0 - 0 - 0 - 0 - 0 0 0 1 1000 - 0 - 0 -1 - false - VELOCITY -1 39 + false + VELOCITY -1 0 127 + 0 + 0 + 0 + 0 0 1 HandClap.wav + 0 + 1 + 1 + 0 false forward 0 @@ -443,50 +453,51 @@ 1 4 1 - 0 - 1 - 1 - 0 4 Snare Rimshot + /usr/local/share/hydrogen/data/drumkits/GMRockKit GMRockKit - 2 1 false false - -0.3 + 1 + 0.7 + 0 + 0 1 true false 1 0 - 0 - 0 - 0 - 0 0 0 1 1000 - 0 - 0 -1 - false - VELOCITY -1 40 + false + VELOCITY -1 0 127 + 0 + 0 + 0 + 0 0 1 SnareRimshot-Softest.wav + 0 + 0.188406 + 1 + 0 false forward 0 @@ -497,13 +508,13 @@ 1 4 1 - 0 - 0.188406 - 1 - 0 SnareRimshot-Soft.wav + 0.188406 + 0.380435 + 1 + 0 false forward 0 @@ -514,13 +525,13 @@ 1 4 1 - 0.188406 - 0.380435 - 1 - 0 SnareRimshot-Med.wav + 0.380435 + 0.594203 + 1 + 0 false forward 0 @@ -531,13 +542,13 @@ 1 4 1 - 0.380435 - 0.594203 - 1 - 0 SnareRimshot-Hard.wav + 0.594203 + 0.728261 + 1 + 0 false forward 0 @@ -548,13 +559,13 @@ 1 4 1 - 0.594203 - 0.728261 - 1 - 0 SnareRimshot-Hardest.wav + 0.728261 + 1 + 1 + 0 false forward 0 @@ -565,50 +576,51 @@ 1 4 1 - 0.728261 - 1 - 1 - 0 5 Floor Tom + /usr/local/share/hydrogen/data/drumkits/GMRockKit GMRockKit - 2 1.04741 false false - 0.56 + 0.44 + 1 + 0 + 0 1 true false 1 0 - 0 - 0 - 0 - 0 0 0 1 1000 - 0 - 0 -1 - false - VELOCITY -1 41 + false + VELOCITY -1 0 127 + 0 + 0 + 0 + 0 0 1 TomFloor-Softest.wav + 0 + 0.199275 + 1 + 0 false forward 0 @@ -619,13 +631,13 @@ 1 4 1 - 0 - 0.199275 - 1 - 0 TomFloor-Soft.wav + 0.199275 + 0.394928 + 1 + 0 false forward 0 @@ -636,13 +648,13 @@ 1 4 1 - 0.199275 - 0.394928 - 1 - 0 TomFloor-Med.wav + 0.394928 + 0.608696 + 1 + 0 false forward 0 @@ -653,13 +665,13 @@ 1 4 1 - 0.394928 - 0.608696 - 1 - 0 TomFloor-Hard.wav + 0.608696 + 0.786232 + 1 + 0 false forward 0 @@ -670,13 +682,13 @@ 1 4 1 - 0.608696 - 0.786232 - 1 - 0 TomFloor-Hardest.wav + 0.786232 + 1 + 1 + 0 false forward 0 @@ -687,50 +699,51 @@ 1 4 1 - 0.786232 - 1 - 1 - 0 6 Hat Closed + /usr/local/share/hydrogen/data/drumkits/GMRockKit GMRockKit - 2 0.685345 false false - -0.52 + 1 + 0.48 + 0 + 0 1 true false 1 0 - 0 - 0 - 0 - 0 0 0 1 1000 - 0 - 0 -1 - false - VELOCITY -1 42 + false + VELOCITY -1 0 127 + 0 + 0 + 0 + 0 0 1 HatClosed-Softest.wav + 0 + 0.173913 + 1 + 0 false forward 0 @@ -741,13 +754,13 @@ 1 4 1 - 0 - 0.173913 - 1 - 0 HatClosed-Soft.wav + 0.173913 + 0.373188 + 1 + 0 false forward 0 @@ -758,13 +771,13 @@ 1 4 1 - 0.173913 - 0.373188 - 1 - 0 HatClosed-Med.wav + 0.373188 + 0.576087 + 1 + 0 false forward 0 @@ -775,13 +788,13 @@ 1 4 1 - 0.373188 - 0.576087 - 1 - 0 HatClosed-Hard.wav + 0.576087 + 0.786232 + 1 + 0 false forward 0 @@ -792,13 +805,13 @@ 1 4 1 - 0.576087 - 0.786232 - 1 - 0 HatClosed-Hardest.wav + 0.786232 + 1 + 1 + 0 false forward 0 @@ -809,50 +822,51 @@ 1 4 1 - 0.786232 - 1 - 1 - 0 7 Tom 2 + /usr/local/share/hydrogen/data/drumkits/GMRockKit GMRockKit - 2 1 false false - 0.24 + 0.76 + 1 + 0 + 0 1 true false 1 0 - 0 - 0 - 0 - 0 0 0 1 1000 - 0 - 0 -1 - false - VELOCITY -1 43 + false + VELOCITY -1 0 127 + 0 + 0 + 0 + 0 0 1 Tom2-Softest.wav + 0 + 0.177536 + 1 + 0 false forward 0 @@ -863,13 +877,13 @@ 1 4 1 - 0 - 0.177536 - 1 - 0 Tom2-Soft.wav + 0.177536 + 0.373188 + 1 + 0 false forward 0 @@ -880,13 +894,13 @@ 1 4 1 - 0.177536 - 0.373188 - 1 - 0 Tom2-Med.wav + 0.373188 + 0.576087 + 1 + 0 false forward 0 @@ -897,13 +911,13 @@ 1 4 1 - 0.373188 - 0.576087 - 1 - 0 Tom2-Hard.wav + 0.576087 + 0.782609 + 1 + 0 false forward 0 @@ -914,13 +928,13 @@ 1 4 1 - 0.576087 - 0.782609 - 1 - 0 Tom2-Hardest.wav + 0.782609 + 1 + 1 + 0 false forward 0 @@ -931,50 +945,51 @@ 1 4 1 - 0.782609 - 1 - 1 - 0 8 Hat Pedal + /usr/local/share/hydrogen/data/drumkits/GMRockKit GMRockKit - 2 0.685345 false false - -0.52 + 1 + 0.48 + 0 + 0 1 true false 1 0 - 0 - 0 - 0 - 0 0 0 1 1000 - 0 - 0 -1 - false - VELOCITY -1 44 + false + VELOCITY -1 0 127 + 0 + 0 + 0 + 0 0 1 HatPedal-Softest.wav + 0 + 0.210145 + 1 + 0 false forward 0 @@ -985,13 +1000,13 @@ 1 4 1 - 0 - 0.210145 - 1 - 0 HatPedal-Soft.wav + 0.210145 + 0.384058 + 1 + 0 false forward 0 @@ -1002,13 +1017,13 @@ 1 4 1 - 0.210145 - 0.384058 - 1 - 0 HatPedal-Med.wav + 0.384058 + 0.594203 + 1 + 0 false forward 0 @@ -1019,13 +1034,13 @@ 1 4 1 - 0.384058 - 0.594203 - 1 - 0 HatPedal-Hard.wav + 0.594203 + 0.797101 + 1 + 0 false forward 0 @@ -1036,13 +1051,13 @@ 1 4 1 - 0.594203 - 0.797101 - 1 - 0 HatPedal-Hardest.wav + 0.797101 + 1 + 1 + 0 false forward 0 @@ -1053,50 +1068,51 @@ 1 4 1 - 0.797101 - 1 - 1 - 0 9 Tom 1 + /usr/local/share/hydrogen/data/drumkits/GMRockKit GMRockKit - 2 1 false false - -0.2 + 1 + 0.8 + 0 + 0 1 true false 1 0 - 0 - 0 - 0 - 0 0 0 1 1000 - 0 - 0 -1 - false - VELOCITY -1 45 + false + VELOCITY -1 0 127 + 0 + 0 + 0 + 0 0 1 Tom1-Softest.wav + 0 + 0.202899 + 1 + 0 false forward 0 @@ -1107,13 +1123,13 @@ 1 4 1 - 0 - 0.202899 - 1 - 0 Tom1-Soft.wav + 0.202899 + 0.394928 + 1 + 0 false forward 0 @@ -1124,13 +1140,13 @@ 1 4 1 - 0.202899 - 0.394928 - 1 - 0 Tom1-Med.wav + 0.394928 + 0.605072 + 1 + 0 false forward 0 @@ -1141,13 +1157,13 @@ 1 4 1 - 0.394928 - 0.605072 - 1 - 0 Tom1-Hard.wav + 0.605072 + 0.786232 + 1 + 0 false forward 0 @@ -1158,13 +1174,13 @@ 1 4 1 - 0.605072 - 0.786232 - 1 - 0 Tom1-Hardest.wav + 0.786232 + 1 + 1 + 0 false forward 0 @@ -1175,50 +1191,51 @@ 1 4 1 - 0.786232 - 1 - 1 - 0 10 Hat Open + /usr/local/share/hydrogen/data/drumkits/GMRockKit GMRockKit - 2 0.698276 false false - -0.52 + 1 + 0.48 + 0 + 0 1 true false 1 0 - 0 - 0 - 0 - 0 0 0 1 1000 - 0 - 0 -1 - false - VELOCITY -1 46 + false + VELOCITY -1 0 127 + 0 + 0 + 0 + 0 0 1 HatOpen-Softest.wav + 0 + 0.202899 + 1 + 0 false forward 0 @@ -1229,13 +1246,13 @@ 1 4 1 - 0 - 0.202899 - 1 - 0 HatOpen-Soft.wav + 0.202899 + 0.398551 + 1 + 0 false forward 0 @@ -1246,13 +1263,13 @@ 1 4 1 - 0.202899 - 0.398551 - 1 - 0 HatOpen-Med.wav + 0.398551 + 0.605072 + 1 + 0 false forward 0 @@ -1263,13 +1280,13 @@ 1 4 1 - 0.398551 - 0.605072 - 1 - 0 HatOpen-Hard.wav + 0.605072 + 0.797101 + 1 + 0 false forward 0 @@ -1280,13 +1297,13 @@ 1 4 1 - 0.605072 - 0.797101 - 1 - 0 HatOpen-Hardest.wav + 0.797101 + 1 + 1 + 0 false forward 0 @@ -1297,50 +1314,51 @@ 1 4 1 - 0.797101 - 1 - 1 - 0 11 Cowbell + /usr/local/share/hydrogen/data/drumkits/GMRockKit GMRockKit - 2 0.568965 false false - -0.68 + 1 + 0.32 + 0 + 0 1 true false 1 0 - 0 - 0 - 0 - 0 0 0 1 1000 - 0 - 0 -1 - false - VELOCITY -1 56 + false + VELOCITY -1 0 127 + 0 + 0 + 0 + 0 0 1 Cowbell-Softest.wav + 0 + 0.184783 + 1 + 0 false forward 0 @@ -1351,13 +1369,13 @@ 1 4 1 - 0 - 0.184783 - 1 - 0 Cowbell-Soft.wav + 0.184783 + 0.384058 + 1 + 0 false forward 0 @@ -1368,13 +1386,13 @@ 1 4 1 - 0.184783 - 0.384058 - 1 - 0 Cowbell-Med.wav + 0.384058 + 0.576087 + 1 + 0 false forward 0 @@ -1385,13 +1403,13 @@ 1 4 1 - 0.384058 - 0.576087 - 1 - 0 Cowbell-Hard.wav + 0.576087 + 0.782609 + 1 + 0 false forward 0 @@ -1402,13 +1420,13 @@ 1 4 1 - 0.576087 - 0.782609 - 1 - 0 Cowbell-Hardest.wav + 0.782609 + 1 + 1 + 0 false forward 0 @@ -1419,50 +1437,51 @@ 1 4 1 - 0.782609 - 1 - 1 - 0 12 Ride + /usr/local/share/hydrogen/data/drumkits/GMRockKit GMRockKit - 2 0.633621 false false - 0.44 + 0.56 + 1 + 0 + 0 1 true false 1 0 - 0 - 0 - 0 - 0 0 0 1 1000 - 0 - 0 -1 - false - VELOCITY -1 51 + false + VELOCITY -1 0 127 + 0 + 0 + 0 + 0 0 1 Ride-Softest.wav + 0 + 0.195652 + 1 + 0 false forward 0 @@ -1473,13 +1492,13 @@ 1 4 1 - 0 - 0.195652 - 1 - 0 Ride-Soft.wav + 0.195652 + 0.376812 + 1 + 0 false forward 0 @@ -1490,13 +1509,13 @@ 1 4 1 - 0.195652 - 0.376812 - 1 - 0 Ride-Med.wav + 0.376812 + 0.572464 + 1 + 0 false forward 0 @@ -1507,13 +1526,13 @@ 1 4 1 - 0.376812 - 0.572464 - 1 - 0 Ride-Hard.wav + 0.572464 + 0.782609 + 1 + 0 false forward 0 @@ -1524,13 +1543,13 @@ 1 4 1 - 0.572464 - 0.782609 - 1 - 0 Ride-Hardest.wav + 0.782609 + 1 + 1 + 0 false forward 0 @@ -1541,50 +1560,51 @@ 1 4 1 - 0.782609 - 1 - 1 - 0 13 Crash + /usr/local/share/hydrogen/data/drumkits/GMRockKit GMRockKit - 2 0.517241 false false - -0.26 + 1 + 0.74 + 0 + 0 1 true false 1 0 - 0 - 0 - 0 - 0 0 0 1 1000 - 0 - 0 -1 - false - VELOCITY -1 49 + false + VELOCITY -1 0 127 + 0 + 0 + 0 + 0 0 1 Crash-Softest.wav + 0 + 0.199275 + 1 + 0 false forward 0 @@ -1595,13 +1615,13 @@ 1 4 1 - 0 - 0.199275 - 1 - 0 Crash-Soft.wav + 0.199275 + 0.384058 + 1 + 0 false forward 0 @@ -1612,13 +1632,13 @@ 1 4 1 - 0.199275 - 0.384058 - 1 - 0 Crash-Med.wav + 0.384058 + 0.59058 + 1 + 0 false forward 0 @@ -1629,13 +1649,13 @@ 1 4 1 - 0.384058 - 0.59058 - 1 - 0 Crash-Hard.wav + 0.59058 + 0.789855 + 1 + 0 false forward 0 @@ -1646,13 +1666,13 @@ 1 4 1 - 0.59058 - 0.789855 - 1 - 0 Crash-Hardest.wav + 0.789855 + 1 + 1 + 0 false forward 0 @@ -1663,50 +1683,51 @@ 1 4 1 - 0.789855 - 1 - 1 - 0 14 Ride 2 + /usr/local/share/hydrogen/data/drumkits/GMRockKit GMRockKit - 2 1 false false - 0.5 + 0.5 + 1 + 0 + 0 0.6 true false 1 0 - 0 - 0 - 0 - 0 0 0 1 1000 - 0 - 0 -1 - false - VELOCITY -1 59 + false + VELOCITY -1 0 127 + 0 + 0 + 0 + 0 0 1 24Ride-5.wav + 0.8 + 1 + 1 + 0 false forward 0 @@ -1717,13 +1738,13 @@ 1 4 1 - 0.8 - 1 - 1 - 0 24Ride-4.wav + 0.6 + 0.8 + 1 + 0 false forward 0 @@ -1734,13 +1755,13 @@ 1 4 1 - 0.6 - 0.8 - 1 - 0 24Ride-3.wav + 0.4 + 0.6 + 1 + 0 false forward 0 @@ -1751,13 +1772,13 @@ 1 4 1 - 0.4 - 0.6 - 1 - 0 24Ride-2.wav + 0.2 + 0.4 + 1 + 0 false forward 0 @@ -1768,13 +1789,13 @@ 1 4 1 - 0.2 - 0.4 - 1 - 0 24Ride-1.wav + 0 + 0.2 + 1 + 0 false forward 0 @@ -1785,50 +1806,51 @@ 1 4 1 - 0 - 0.2 - 1 - 0 15 Splash + /usr/local/share/hydrogen/data/drumkits/GMRockKit GMRockKit - 2 0.543103 false false - 0.02 + 0.98 + 1 + 0 + 0 1 true false 1 0 - 0 - 0 - 0 - 0 0 0 1 1000 - 0 - 0 -1 - false - VELOCITY -1 57 + false + VELOCITY -1 0 127 + 0 + 0 + 0 + 0 0 1 Splash-Softest.wav + 0 + 0.195652 + 1 + 0 false forward 0 @@ -1839,13 +1861,13 @@ 1 4 1 - 0 - 0.195652 - 1 - 0 Splash-Soft.wav + 0.195652 + 0.362319 + 1 + 0 false forward 0 @@ -1856,13 +1878,13 @@ 1 4 1 - 0.195652 - 0.362319 - 1 - 0 Splash-Med.wav + 0.362319 + 0.568841 + 1 + 0 false forward 0 @@ -1873,13 +1895,13 @@ 1 4 1 - 0.362319 - 0.568841 - 1 - 0 Splash-Hard.wav + 0.568841 + 0.75 + 1 + 0 false forward 0 @@ -1890,13 +1912,13 @@ 1 4 1 - 0.568841 - 0.75 - 1 - 0 Splash-Hardest.wav + 0.75 + 1 + 1 + 0 false forward 0 @@ -1907,67 +1929,51 @@ 1 4 1 - 0.75 - 1 - 1 - 0 16 Hat Semi-Open + /usr/local/share/hydrogen/data/drumkits/GMRockKit GMRockKit - 2 0.69 false false - -0.52 + 1 + 0.48 + 0 + 0 1 true false 1 0 - 0 - 0 - 0 - 0 0 0 1 1000 - 0 - 0 -1 - false - VELOCITY -1 82 + false + VELOCITY -1 0 127 + 0 + 0 + 0 + 0 0 1 HatSemiOpen-Softest.wav - false - forward - 0 - 0 - 0 - 0 - 0 - 1 - 4 - 1 0 0.188406 1 - 0 - - - HatSemiOpen-Soft.wav + 0 false forward 0 @@ -1978,13 +1984,13 @@ 1 4 1 + + + HatSemiOpen-Soft.wav 0.188406 0.380435 1 0 - - - HatSemiOpen-Med.wav false forward 0 @@ -1995,13 +2001,13 @@ 1 4 1 + + + HatSemiOpen-Med.wav 0.380435 0.57971 1 0 - - - HatSemiOpen-Hard.wav false forward 0 @@ -2012,13 +2018,13 @@ 1 4 1 + + + HatSemiOpen-Hard.wav 0.57971 0.782609 1 0 - - - HatSemiOpen-Hardest.wav false forward 0 @@ -2029,50 +2035,68 @@ 1 4 1 + + + HatSemiOpen-Hardest.wav 0.782609 1 1 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 17 Bell + /usr/local/share/hydrogen/data/drumkits/GMRockKit GMRockKit - 2 0.534828 false false - 0.44 + 0.56 + 1 + 0 + 0 1 true false 1 0 - 0 - 0 - 0 - 0 0 0 1 - 1000 - 0 - 0 + 999 -1 - false - VELOCITY -1 81 + false + VELOCITY -1 0 127 + 0 + 0 + 0 + 0 0 1 Bell-Softest.wav + 0 + 0.210145 + 1 + 0 false forward 0 @@ -2083,13 +2107,13 @@ 1 4 1 - 0 - 0.210145 - 1 - 0 Bell-Soft.wav + 0.210145 + 0.405797 + 1 + 0 false forward 0 @@ -2100,13 +2124,13 @@ 1 4 1 - 0.210145 - 0.405797 - 1 - 0 Bell-Med.wav + 0.405797 + 0.605072 + 1 + 0 false forward 0 @@ -2117,13 +2141,13 @@ 1 4 1 - 0.405797 - 0.605072 - 1 - 0 Bell-Hard.wav + 0.605072 + 0.786232 + 1 + 0 false forward 0 @@ -2134,13 +2158,13 @@ 1 4 1 - 0.605072 - 0.786232 - 1 - 0 Bell-Hardest.wav + 0.786232 + 1 + 1 + 0 false forward 0 @@ -2151,10 +2175,6 @@ 1 4 1 - 0.786232 - 1 - 1 - 0 @@ -2162,10 +2182,10 @@ Crash + not_categorized 48 4 - 0 @@ -2173,20 +2193,20 @@ 0.8 0 0 - 1 C0 -1 13 false + 1 Ride + not_categorized 432 4 - 0 @@ -2194,11 +2214,11 @@ 0.8 0 0 - 1 C0 -1 14 false + 1 96 @@ -2206,20 +2226,20 @@ 0.8 0 0 - 1 C0 -1 12 false + 1 Toms 1 + not_categorized 192 4 - 0 @@ -2227,11 +2247,11 @@ 1 0.6 0 - 1 C2 -1 9 false + 1 48 @@ -2239,11 +2259,11 @@ 0.8 0.28 0 - 1 C1 -1 9 false + 1 96 @@ -2251,11 +2271,11 @@ 0.57 -0.24 0 - 1 C0 -1 9 false + 1 144 @@ -2263,20 +2283,20 @@ 0.35 -0.52 0 - 1 C-1 -1 9 false + 1 Toms 2 + not_categorized 192 4 - 0 @@ -2284,47 +2304,47 @@ 0.45 0.2 0 - 1 C2 -1 7 false + 1 0 0 - 0.8 + 0.89 0 0 - 1 C0 -1 0 false + 1 6 0 - 0.8 + 0.87 0 0 - 1 C0 -1 0 false + 1 12 0 - 0.8 + 0.83 0 0 - 1 C0 -1 0 false + 1 18 @@ -2332,59 +2352,59 @@ 0.8 0 0 - 1 C0 -1 0 false + 1 24 0 - 0.8 + 0.78 0 0 - 1 C0 -1 0 false + 1 30 0 - 0.8 + 0.75 0 0 - 1 C0 -1 0 false + 1 36 0 - 0.8 + 0.73 0 0 - 1 C0 -1 0 false + 1 42 0 - 0.8 + 0.7 0 0 - 1 C0 -1 0 false + 1 48 @@ -2392,107 +2412,107 @@ 0.6 0.48 0 - 1 C1 -1 7 false + 1 48 0 - 0.8 + 0.65 0 0 - 1 C0 -1 0 false + 1 54 0 - 0.8 + 0.62 0 0 - 1 C0 -1 0 false + 1 60 0 - 0.8 + 0.59 0 0 - 1 C0 -1 0 false + 1 66 0 - 0.8 + 0.56 0 0 - 1 C0 -1 0 false + 1 72 0 - 0.8 + 0.53 0 0 - 1 C0 -1 0 false + 1 78 0 - 0.8 + 0.51 0 0 - 1 C0 -1 0 false + 1 84 0 - 0.8 + 0.47 0 0 - 1 C0 -1 0 false + 1 90 0 - 0.8 + 0.44 0 0 - 1 C0 -1 0 false + 1 96 @@ -2500,107 +2520,107 @@ 0.77 0.66 0 - 1 C0 -1 7 false + 1 96 0 - 0.8 + 0.41 0 0 - 1 C0 -1 0 false + 1 102 0 - 0.8 + 0.37 0 0 - 1 C0 -1 0 false + 1 108 0 - 0.8 + 0.36 0 0 - 1 C0 -1 0 false + 1 114 0 - 0.8 + 0.32 0 0 - 1 C0 -1 0 false + 1 120 0 - 0.8 + 0.3 0 0 - 1 C0 -1 0 false + 1 126 0 - 0.8 + 0.27 0 0 - 1 C0 -1 0 false + 1 132 0 - 0.8 + 0.26 0 0 - 1 C0 -1 0 false + 1 138 0 - 0.8 + 0.22 0 0 - 1 C0 -1 0 false + 1 144 @@ -2608,128 +2628,128 @@ 0.95 0.88 0 - 1 C-1 -1 7 false + 1 144 0 - 0.8 + 0.21 0 0 - 1 C0 -1 0 false + 1 150 0 - 0.8 + 0.17 0 0 - 1 C0 -1 0 false + 1 156 0 - 0.8 + 0.13 0 0 - 1 C0 -1 0 false + 1 162 0 - 0.8 + 0.11 0 0 - 1 C0 -1 0 false + 1 168 0 - 0.8 + 0.08 0 0 - 1 C0 -1 0 false + 1 174 0 - 0.8 + 0.06 0 0 - 1 C0 -1 0 false + 1 180 0 - 0.8 + 0.03 0 0 - 1 C0 -1 0 false + 1 186 0 - 0.8 + 0.01 0 0 - 1 C0 -1 0 false + 1 Kick & Snare + not_categorized 96 4 - 0 0 - 0.78 + 1 0 0 - 1 C0 -1 0 false + 1 6 @@ -2737,11 +2757,11 @@ 0.8 0 0 - 1 C0 -1 3 false + 1 12 @@ -2749,11 +2769,11 @@ 0.8 0 0 - 1 C0 -1 4 false + 1 18 @@ -2761,11 +2781,11 @@ 0.8 0 0 - 1 C0 -1 17 false + 1 24 @@ -2773,11 +2793,11 @@ 0.8 0 0 - 1 C0 -1 15 false + 1 30 @@ -2785,11 +2805,11 @@ 0.8 0 0 - 1 C0 -1 11 false + 1 36 @@ -2797,11 +2817,11 @@ 0.8 0 0 - 1 C0 -1 8 false + 1 48 @@ -2809,11 +2829,11 @@ 0.8 0 0 - 1 C0 -1 2 false + 1 60 @@ -2821,20 +2841,20 @@ 0.8 0 0 - 1 C0 -1 1 false + 1 Hat + not_categorized 192 4 - 0 @@ -2842,11 +2862,11 @@ 0.8 0 0 - 1 C0 -1 10 false + 1 48 @@ -2854,11 +2874,11 @@ 0.8 0 0 - 1 C0 -1 8 false + 1 96 @@ -2866,11 +2886,11 @@ 0.8 0 0 - 1 C0 -1 6 false + 1 144 @@ -2878,44 +2898,44 @@ 0.8 0 0 - 1 C0 -1 16 false + 1 Pattern 7 + not_categorized 192 4 - Pattern 8 + not_categorized 192 4 - Pattern 9 + not_categorized 192 4 - Pattern 10 + not_categorized 192 4 - @@ -2945,41 +2965,28 @@ no plugin - false - 0.0 + 0 no plugin - false - 0.0 + 0 no plugin - false - 0.0 + 0 no plugin - false - 0.0 + 0 - - - 0 - 100 - - - 2 - 180 - - - 4 - 70 - - + diff --git a/src/tests/data/song/AE_transportProcessingTimeline.h2song b/src/tests/data/song/AE_transportProcessingTimeline.h2song new file mode 100644 index 0000000000..fd53dde3be --- /dev/null +++ b/src/tests/data/song/AE_transportProcessingTimeline.h2song @@ -0,0 +1,2787 @@ + + + 1.1.1-'bc740855' + 300 + 0.5 + 0.5 + Untitled Song + hydrogen + ... + undefined license + true + true + + false + 0 + 0 + false + true + song + RATIO_STRAIGHT_POLYGONAL + 1.33333 + 0 + 0 + 0 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + + + 0 + Main + 1 + + + + + 0 + Kick + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 1 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 999 + -1 + -1 + 36 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Kick-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Soft.wav + 0.202899 + 0.369565 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Med.wav + 0.369565 + 0.731884 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Hard.wav + 0.731884 + 0.865942 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Kick-Hardest.wav + 0.865942 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 1 + Stick + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.99569 + false + false + 1 + 0.7 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 37 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + SideStick-Softest.wav + 0 + 0.188406 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Soft.wav + 0.188406 + 0.402174 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Med.wav + 0.402174 + 0.597826 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Hard.wav + 0.597826 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SideStick-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 2 + Snare + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1.02155 + false + false + 1 + 0.7 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 38 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Snare-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Soft.wav + 0.202899 + 0.376812 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Med.wav + 0.376812 + 0.568841 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Hard.wav + 0.568841 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Snare-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 3 + Hand Clap + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1.02155 + false + false + 0.7 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 39 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HandClap.wav + 0 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 4 + Snare Rimshot + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 1 + 0.7 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 40 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + SnareRimshot-Softest.wav + 0 + 0.188406 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Soft.wav + 0.188406 + 0.380435 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Med.wav + 0.380435 + 0.594203 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Hard.wav + 0.594203 + 0.728261 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + SnareRimshot-Hardest.wav + 0.728261 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 5 + Floor Tom + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1.04741 + false + false + 0.44 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 41 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + TomFloor-Softest.wav + 0 + 0.199275 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Soft.wav + 0.199275 + 0.394928 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Med.wav + 0.394928 + 0.608696 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Hard.wav + 0.608696 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + TomFloor-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 6 + Hat Closed + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.685345 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 42 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatClosed-Softest.wav + 0 + 0.173913 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Soft.wav + 0.173913 + 0.373188 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Med.wav + 0.373188 + 0.576087 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Hard.wav + 0.576087 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatClosed-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 7 + Tom 2 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 0.76 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 43 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Tom2-Softest.wav + 0 + 0.177536 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Soft.wav + 0.177536 + 0.373188 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Med.wav + 0.373188 + 0.576087 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Hard.wav + 0.576087 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom2-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 8 + Hat Pedal + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.685345 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 44 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatPedal-Softest.wav + 0 + 0.210145 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Soft.wav + 0.210145 + 0.384058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Med.wav + 0.384058 + 0.594203 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Hard.wav + 0.594203 + 0.797101 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatPedal-Hardest.wav + 0.797101 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 9 + Tom 1 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 1 + 0.8 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 45 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Tom1-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Soft.wav + 0.202899 + 0.394928 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Med.wav + 0.394928 + 0.605072 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Hard.wav + 0.605072 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Tom1-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 10 + Hat Open + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.698276 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 46 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatOpen-Softest.wav + 0 + 0.202899 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Soft.wav + 0.202899 + 0.398551 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Med.wav + 0.398551 + 0.605072 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Hard.wav + 0.605072 + 0.797101 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatOpen-Hardest.wav + 0.797101 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 11 + Cowbell + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.568965 + false + false + 1 + 0.32 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 56 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Cowbell-Softest.wav + 0 + 0.184783 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Soft.wav + 0.184783 + 0.384058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Med.wav + 0.384058 + 0.576087 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Hard.wav + 0.576087 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Cowbell-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 12 + Ride + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.633621 + false + false + 0.56 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 51 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Ride-Softest.wav + 0 + 0.195652 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Soft.wav + 0.195652 + 0.376812 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Med.wav + 0.376812 + 0.572464 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Hard.wav + 0.572464 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Ride-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 13 + Crash + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.517241 + false + false + 1 + 0.74 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 49 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Crash-Softest.wav + 0 + 0.199275 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Soft.wav + 0.199275 + 0.384058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Med.wav + 0.384058 + 0.59058 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Hard.wav + 0.59058 + 0.789855 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Crash-Hardest.wav + 0.789855 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 14 + Ride 2 + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 1 + false + false + 0.5 + 1 + 0 + 0 + 0.6 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 59 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + 24Ride-5.wav + 0.8 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-4.wav + 0.6 + 0.8 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-3.wav + 0.4 + 0.6 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-2.wav + 0.2 + 0.4 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + 24Ride-1.wav + 0 + 0.2 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 15 + Splash + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.543103 + false + false + 0.98 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 55 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Splash-Softest.wav + 0 + 0.195652 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Soft.wav + 0.195652 + 0.362319 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Med.wav + 0.362319 + 0.568841 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Hard.wav + 0.568841 + 0.75 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Splash-Hardest.wav + 0.75 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 16 + Hat Semi-Open + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.69 + false + false + 1 + 0.48 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 82 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + HatSemiOpen-Softest.wav + 0 + 0.188406 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Soft.wav + 0.188406 + 0.380435 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Med.wav + 0.380435 + 0.57971 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Hard.wav + 0.57971 + 0.782609 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + HatSemiOpen-Hardest.wav + 0.782609 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + 17 + Bell + /usr/local/share/hydrogen/data/drumkits/GMRockKit + GMRockKit + 0.534828 + false + false + 0.56 + 1 + 0 + 0 + 1 + true + false + 1 + 0 + 0 + 0 + 1 + 1000 + -1 + -1 + 53 + false + VELOCITY + -1 + 0 + 127 + 0 + 0 + 0 + 0 + + 0 + 1 + + Bell-Softest.wav + 0 + 0.210145 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Soft.wav + 0.210145 + 0.405797 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Med.wav + 0.405797 + 0.605072 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Hard.wav + 0.605072 + 0.786232 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + Bell-Hardest.wav + 0.786232 + 1 + 1 + 0 + false + forward + 0 + 0 + 0 + 0 + 0 + 1 + 4 + 1 + + + + + + + Pattern 1 + + not_categorized + 192 + 4 + + + 0 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 6 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 12 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 18 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 24 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 30 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 36 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 42 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 48 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 54 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 60 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 66 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 72 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 78 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 84 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 90 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 96 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 102 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 108 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 114 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 120 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 126 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 132 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 138 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 144 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 150 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 156 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 162 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 168 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 174 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 180 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + 186 + 0 + 0.8 + 0 + 0 + C0 + -1 + 0 + false + 1 + + + + + Pattern 2 + + not_categorized + 192 + 4 + + + + Pattern 3 + + not_categorized + 192 + 4 + + + + Pattern 4 + + not_categorized + 192 + 4 + + + + Pattern 5 + + not_categorized + 192 + 4 + + + + Pattern 6 + + not_categorized + 192 + 4 + + + + Pattern 7 + + not_categorized + 192 + 4 + + + + Pattern 8 + + not_categorized + 192 + 4 + + + + Pattern 9 + + not_categorized + 192 + 4 + + + + Pattern 10 + + not_categorized + 192 + 4 + + + + + + + Pattern 1 + + + Pattern 1 + + + Pattern 1 + + + Pattern 1 + + + Pattern 1 + + + Pattern 1 + + + Pattern 1 + + + Pattern 1 + + + Pattern 1 + + + Pattern 1 + + + Pattern 1 + + + Pattern 1 + + + Pattern 1 + + + Pattern 1 + + + Pattern 1 + + + Pattern 1 + + + Pattern 1 + + + + + no plugin + - + false + 0 + + + no plugin + - + false + 0 + + + no plugin + - + false + 0 + + + no plugin + - + false + 0 + + + + + 0 + 120 + + + 1 + 300 + + + 2 + 60 + + + 3 + 365 + + + 4 + 123 + + + 6 + 140 + + + 7 + 120 + + + 8 + 100 + + + 9 + 60 + + + 10 + 40 + + + 11 + 70 + + + 12 + 100 + + + 13 + 160 + + + + + + + diff --git a/src/tests/data/song/res/playbackTrack.flac b/src/tests/data/song/res/playbackTrack.flac new file mode 100644 index 0000000000..a25839e8e0 Binary files /dev/null and b/src/tests/data/song/res/playbackTrack.flac differ diff --git a/src/tests/main.cpp b/src/tests/main.cpp index ed73ca0e20..d2a1b22fd4 100644 --- a/src/tests/main.cpp +++ b/src/tests/main.cpp @@ -44,18 +44,23 @@ #include #endif - -void setupEnvironment(unsigned log_level) +void setupEnvironment(unsigned log_level, const QString& sLogFilePath ) { /* Logger */ - H2Core::Logger* logger = H2Core::Logger::bootstrap( log_level ); + H2Core::Logger* pLogger = nullptr; + if ( ! sLogFilePath.isEmpty() ) { + pLogger = H2Core::Logger::bootstrap( log_level, sLogFilePath, false ); + } + else { + pLogger = H2Core::Logger::bootstrap( log_level ); + } /* Test helper */ TestHelper::createInstance(); TestHelper* test_helper = TestHelper::get_instance(); /* Base */ - H2Core::Base::bootstrap( logger, true ); + H2Core::Base::bootstrap( pLogger, true ); /* Filesystem */ - H2Core::Filesystem::bootstrap( logger, test_helper->getDataDir() ); + H2Core::Filesystem::bootstrap( pLogger, test_helper->getDataDir() ); H2Core::Filesystem::info(); /* Use fake audio driver */ @@ -92,25 +97,42 @@ int main( int argc, char **argv) QCommandLineParser parser; QCommandLineOption verboseOption( QStringList() << "V" << "verbose", "Level, if present, may be None, Error, Warning, Info, Debug or 0xHHHH","Level"); - QCommandLineOption appveyorOption( QStringList() << "appveyor", "Report test progress to AppVeyor build worker" ); + QCommandLineOption appveyorOption( QStringList() << "appveyor", "Report test progress to AppVeyor build" ); QCommandLineOption benchmarkOption( QStringList() << "b" << "benchmark", "Run audio system benchmark" ); + QCommandLineOption outputFileOption( QStringList() << "o" << "output-file", "If specified the output of the logger will not be directed to stdout but instead stored in a file (either plain file name or with relative of absolute path)", + "Output File", ""); parser.addHelpOption(); parser.addOption( verboseOption ); parser.addOption( appveyorOption ); parser.addOption( benchmarkOption ); + parser.addOption( outputFileOption ); parser.process(app); QString sVerbosityString = parser.value( verboseOption ); + + const QString sLogFile = parser.value( outputFileOption ); + QString sLogFilePath = ""; + if ( parser.isSet( outputFileOption ) ) { + if ( ! sLogFile.contains( QDir::separator() ) ) { + // A plain filename was provided. It will be placed in the + // current working directory. + sLogFilePath = QDir::currentPath() + QDir::separator() + sLogFile; + } else { + QFileInfo fi( sLogFile ); + sLogFilePath = fi.absoluteFilePath(); + } + } + unsigned logLevelOpt = H2Core::Logger::None; - if( parser.isSet(verboseOption) ){ + if( parser.isSet(verboseOption) || parser.isSet( outputFileOption ) ){ if( !sVerbosityString.isEmpty() ) { logLevelOpt = H2Core::Logger::parse_log_level( sVerbosityString.toLocal8Bit() ); } else { - logLevelOpt = H2Core::Logger::Error|H2Core::Logger::Warning; + logLevelOpt = H2Core::Logger::Error|H2Core::Logger::Warning|H2Core::Logger::Info; } } - setupEnvironment(logLevelOpt); + setupEnvironment( logLevelOpt, sLogFilePath ); #ifdef HAVE_EXECINFO_H signal(SIGSEGV, fatal_signal); @@ -132,14 +154,18 @@ int main( int argc, char **argv) std::unique_ptr appveyorApiClient; std::unique_ptr avtl; if( parser.isSet( appveyorOption )) { - qDebug() << "Enabled AppVeyor reporting"; appveyorApiClient.reset( new AppVeyor::BuildWorkerApiClient() ); avtl.reset( new AppVeyorTestListener( *appveyorApiClient )); runner.eventManager().addListener( avtl.get() ); } bool wasSuccessful = runner.run( "", false ); - auto stop = std::chrono::high_resolution_clock::now(); + + // Ensure the log is written properly + auto pLogger = H2Core::Logger::get_instance(); + pLogger->flush(); + delete pLogger; + auto durationSeconds = std::chrono::duration_cast( stop - start ); auto durationMilliSeconds = std::chrono::duration_cast( stop - start ) -