Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Variable resolution support #1173

Draft
wants to merge 3 commits into
base: development
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion src/core/Basics/Pattern.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,34 @@
#include <core/Basics/Note.h>
#include <core/Basics/PatternList.h>
#include <core/AudioEngine.h>
#include <core/Basics/Song.h>
#include <core/Hydrogen.h>

#include <core/Helpers/Xml.h>
#include <core/Helpers/Filesystem.h>
#include <core/Helpers/Legacy.h>

#include <numeric>

namespace H2Core
{

const char* Pattern::__class_name = "Pattern";

Pattern::Pattern( const QString& name, const QString& info, const QString& category, int length, int denominator )
: Object( __class_name )
, __length( length )
, __denominator( denominator)
, __name( name )
, __info( info )
, __category( category )
{
m_nResolution = H2Core::Song::nDefaultResolutionTPQN;
// Default to a pattern length of a whole note.
if ( length <= 0 ) {
__length = 4 * m_nResolution;
} else {
__length = length;
}
}

Pattern::Pattern( Pattern* other )
Expand Down Expand Up @@ -101,6 +111,7 @@ Pattern* Pattern::load_from( XMLNode* node, InstrumentList* instruments )
if ( pattern->get_name().isEmpty() ) {
pattern->set_name( node->read_string( "pattern_name", "unknown", false, false ) );
}
pattern->set_resolution( node->read_int( "resolution", Song::nDefaultResolutionTPQN ) );
XMLNode note_list_node = node->firstChildElement( "noteList" );
if ( !note_list_node.isNull() ) {
XMLNode note_node = note_list_node.firstChildElement( "note" );
Expand Down Expand Up @@ -139,6 +150,7 @@ void Pattern::save_to( XMLNode* node, const Instrument* instrumentOnly ) const
pattern_node.write_string( "category", __category );
pattern_node.write_int( "size", __length );
pattern_node.write_int( "denominator", __denominator );
pattern_node.write_int( "resolution", m_nResolution );
XMLNode note_list_node = pattern_node.createNode( "noteList" );
int id = ( instrumentOnly == nullptr ? -1 : instrumentOnly->get_id() );
for( auto it=__notes.cbegin(); it!=__notes.cend(); ++it ) {
Expand Down Expand Up @@ -284,6 +296,37 @@ void Pattern::extand_with_flattened_virtual_patterns( PatternList* patterns )
}
}

/// Calculate the minimum resolution that can be used to accurately represent the pattern.
int Pattern::get_minimum_resolution() const
{
int nDenominator = 1;
for ( auto it : __notes ) {
int nPos = it.first;
nDenominator = std::gcd( nDenominator, nPos );
}
return m_nResolution / nDenominator;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain this please?
If m_nResolution = 3 (3 ticks per quarter notes or whole notes?), and the pattern has a note at position = 7,
the ratio m_nResolution / nDenominator equals (int) 3/7 = 0, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, there's a bug here. nDenominator should be initialised to 1, not 0.

Copy link
Contributor

@oddtime oddtime Feb 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why 1? then at the end of the loop nDenominator will be always 1

}

/// Retime a pattern to a given resolution. This adjusts the positions of all notes to fit.
int Pattern::retime_to_resolution( int nResolution )
{
std::list< Note *> notes;
for ( auto it : __notes ) {
notes.push_back( it.second );
}

__notes.clear();

for ( auto pNote : notes ) {
int nPos = pNote->get_position() * nResolution / m_nResolution;
pNote->set_position( nPos );
__notes.insert( std::make_pair( nPos, pNote ) );
}
__length = __length * nResolution / m_nResolution;
m_nResolution = nResolution;
}


};

/* vim: set softtabstop=4 noexpandtab: */
32 changes: 30 additions & 2 deletions src/core/Basics/Pattern.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ class Pattern : public H2Core::Object
* \param length the length of the pattern
* \param denominator the denominator for meter representation (eg 4/4)
*/
Pattern( const QString& name="Pattern", const QString& info="", const QString& category="not_categorized", int length=MAX_NOTES, int denominator=4 );
Pattern( const QString& name="Pattern", const QString& info="", const QString& category="not_categorized",
int length=-1, int denominator=4 );
/** copy constructor */
Pattern( Pattern* other );
/** destructor */
Expand Down Expand Up @@ -194,9 +195,22 @@ class Pattern : public H2Core::Object
*/
void save_to( XMLNode* node, const Instrument* instrumentOnly = nullptr ) const;

/// Get resolution of the pattern, in ticks per quarter-note
int get_resolution() const;

/// Set resolution, in ticks per quarter-note.
/// This does not alter the length of the pattern or the position of notes
void set_resolution( int nResolution );

/// Calculate the minimum resolution that can be used to accurately represent the pattern.
int get_minimum_resolution() const;

/// Retime a pattern to a given resolution. This adjusts the positions of all notes to fit.
int retime_to_resolution( int nResolution );

private:
int __length; ///< the length of the pattern
int __denominator; ///< the meter denominator of the pattern used in meter (eg 4/4)
int __denominator; ///< the meter denominator of the pattern used in meter (eg 4/4)
QString __name; ///< the name of thepattern
QString __category; ///< the category of the pattern
QString __info; ///< a description of the pattern
Expand All @@ -210,6 +224,9 @@ class Pattern : public H2Core::Object
* \return a new Pattern instance
*/
static Pattern* load_from( XMLNode* node, InstrumentList* instruments );

int m_nResolution; ///< Resolution in ticks per quarter-note

};

#define FOREACH_NOTE_CST_IT_BEGIN_END(_notes,_it) \
Expand Down Expand Up @@ -322,8 +339,19 @@ inline void Pattern::flattened_virtual_patterns_clear()
__flattened_virtual_patterns.clear();
}

inline int Pattern::get_resolution() const
{
return m_nResolution;
}

inline void Pattern::set_resolution( int nResolution )
{
m_nResolution = nResolution;
}

};


#endif // H2C_PATTERN_H

/* vim: set softtabstop=4 noexpandtab: */
4 changes: 3 additions & 1 deletion src/core/Basics/Song.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const char* Song::__class_name = "Song";
Song::Song( const QString& sName, const QString& sAuthor, float fBpm, float fVolume )
: Object( __class_name )
, m_bIsMuted( false )
, m_resolution( 48 )
, m_resolution( nDefaultResolutionTPQN )
, m_fBpm( fBpm )
, m_sName( sName )
, m_sAuthor( sAuthor )
Expand Down Expand Up @@ -689,6 +689,7 @@ Song* SongReader::readSong( const QString& sFileName )

float fBpm = LocalFileMng::readXmlFloat( songNode, "bpm", 120 );
Hydrogen::get_instance()->setNewBpmJTM( fBpm );
int nResolution = LocalFileMng::readXmlFloat( songNode, "resolution", Song::nDefaultResolutionTPQN );
float fVolume = LocalFileMng::readXmlFloat( songNode, "volume", 0.5 );
float fMetronomeVolume = LocalFileMng::readXmlFloat( songNode, "metronomeVolume", 0.5 );
QString sName( LocalFileMng::readXmlString( songNode, "name", "Untitled Song" ) );
Expand Down Expand Up @@ -724,6 +725,7 @@ Song* SongReader::readSong( const QString& sFileName )
float fSwingFactor = LocalFileMng::readXmlFloat( songNode, "swing_factor", 0.0 );

pSong = new Song( sName, sAuthor, fBpm, fVolume );
pSong->setResolution( nResolution );
pSong->setMetronomeVolume( fMetronomeVolume );
pSong->setNotes( sNotes );
pSong->setLicense( sLicense );
Expand Down
9 changes: 9 additions & 0 deletions src/core/Basics/Song.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,16 @@ class Song : public H2Core::Object
SONG_MODE
};

static constexpr int nDefaultResolutionTPQN = 48;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my recent experience on H2 code, it seems better to have this expressed in Ticks per whole notes, because the grid resolutions, i.e. the inverses of quantum note values (1/4, 1/8, 1/16...) refer to that unit...
Otherwise should all the formulas like getColumn() have an additional 4 factor?
What is the advantage of having this in TPQN?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only because that's what MIDI uses, and that's what the core already uses (because it looks like it was heavily influenced by MIDI). Multiplying up by 4 when needed (seems to be a fairly small number of uses) is a fairly small price to pay for the consistency (if not, a macro can be defined).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.
a macro would be good enough


Song( const QString& sName, const QString& sAuthor, float fBpm, float fVolume );
~Song();

static Song* getEmptySong();
static Song* getDefaultSong();

int getDefaultPatternSize() const;

bool getIsMuted() const;
void setIsMuted( bool bIsMuted );

Expand Down Expand Up @@ -285,6 +289,11 @@ class Song : public H2Core::Object

};

inline int Song::getDefaultPatternSize() const
{
return getResolution() * 4;
}

inline bool Song::getIsMuted() const
{
return m_bIsMuted;
Expand Down
24 changes: 12 additions & 12 deletions src/core/Hydrogen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1738,7 +1738,7 @@ inline int audioEngine_updateNoteQueue( unsigned nFrames )
// PATTERN MODE
else if ( pSong->getMode() == Song::PATTERN_MODE ) {

int nPatternSize = MAX_NOTES;
int nPatternSize = pSong->getDefaultPatternSize();

// If the user chose to playback the pattern she focuses,
// use it to overwrite `m_pPlayingPatterns`.
Expand Down Expand Up @@ -1802,7 +1802,7 @@ inline int audioEngine_updateNoteQueue( unsigned nFrames )
//////////////////////////////////////////////////////////////
// Metronome
// Only trigger the metronome at a predefined rate.
if ( m_nPatternTickPosition % 48 == 0 ) {
if ( m_nPatternTickPosition % pSong->getResolution() == 0 ) {
float fPitch;
float fVelocity;

Expand Down Expand Up @@ -1934,8 +1934,8 @@ inline int findPatternInTick( int nTick, bool bLoopMode, int* pPatternStartTick
std::vector<PatternList*> *pPatternColumns = pSong->getPatternGroupVector();
int nColumns = pPatternColumns->size();

// Sum the lengths of all pattern columns and use the macro
// MAX_NOTES in case some of them are of size zero. If the
// Sum the lengths of all pattern columns and use the default length
// in case some of them are of size zero. If the
// supplied value nTick is bigger than this and doesn't belong to
// the next pattern column, we just found the pattern list we were
// searching for.
Expand All @@ -1945,7 +1945,7 @@ inline int findPatternInTick( int nTick, bool bLoopMode, int* pPatternStartTick
if ( pColumn->size() != 0 ) {
nPatternSize = pColumn->longest_pattern_length();
} else {
nPatternSize = MAX_NOTES;
nPatternSize = pSong->getDefaultPatternSize();
}

if ( ( nTick >= nTotalTick ) && ( nTick < nTotalTick + nPatternSize ) ) {
Expand All @@ -1971,7 +1971,7 @@ inline int findPatternInTick( int nTick, bool bLoopMode, int* pPatternStartTick
if ( pColumn->size() != 0 ) {
nPatternSize = pColumn->longest_pattern_length();
} else {
nPatternSize = MAX_NOTES;
nPatternSize = pSong->getDefaultPatternSize();
}

if ( ( nLoopTick >= nTotalTick )
Expand Down Expand Up @@ -2564,16 +2564,16 @@ void Hydrogen::addRealtimeNote( int instrument,
UNUSED( pitch );

Preferences *pPreferences = Preferences::get_instance();
Song *pSong = getSong();
unsigned int nRealColumn = 0;
unsigned res = pPreferences->getPatternEditorGridResolution();
int nBase = pPreferences->isPatternEditorUsingTriplets() ? 3 : 4;
int scalar = ( 4 * MAX_NOTES ) / ( res * nBase );
int scalar = ( 4 * pSong->getResolution() * 4 ) / ( res * nBase );
bool hearnote = forcePlay;
int currentPatternNumber;

AudioEngine::get_instance()->lock( RIGHT_HERE );

Song *pSong = getSong();
if ( !pPreferences->__playselectedinstrument ) {
if ( instrument >= ( int ) pSong->getInstrumentList()->size() ) {
// unused instrument
Expand Down Expand Up @@ -3338,7 +3338,7 @@ long Hydrogen::getTickForPosition( int pos )
{
nPatternSize = pColumn->longest_pattern_length();
} else {
nPatternSize = MAX_NOTES;
nPatternSize = pSong->getDefaultPatternSize();
}
totalTick += nPatternSize;
}
Expand Down Expand Up @@ -3755,19 +3755,19 @@ long Hydrogen::getPatternLength( int nPattern )
if ( pSong->getIsLoopEnabled() ) {
nPattern = nPattern % nPatternGroups;
} else {
return MAX_NOTES;
return pSong->getDefaultPatternSize();
}
}

if ( nPattern < 1 ){
return MAX_NOTES;
return pSong->getDefaultPatternSize();
}

PatternList* pPatternList = pColumns->at( nPattern - 1 );
if ( pPatternList->size() > 0 ) {
return pPatternList->longest_pattern_length();
} else {
return MAX_NOTES;
return pSong->getDefaultPatternSize();
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/core/Hydrogen.h
Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,8 @@ class Hydrogen : public H2Core::Object
*
* The function will loop over all and sums up their
* Pattern::__length. If one of the Pattern is NULL or no
* Pattern is present one of the PatternList, #MAX_NOTES will
* be added instead.
* Pattern is present one of the PatternList, the default
* pattern length will be added instead.
*
* The driver should be LOCKED when calling this!
*
Expand Down
2 changes: 1 addition & 1 deletion src/core/IO/DiskWriterDriver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ void* diskWriterDriver_thread( void* param )
if ( pColumn->size() != 0 ) {
nPatternSize = pColumn->longest_pattern_length();
} else {
nPatternSize = MAX_NOTES;
nPatternSize = 4 * pSong->getResolution();
}

// check pattern bpm if timeline bpm is in use
Expand Down
4 changes: 3 additions & 1 deletion src/core/LocalFileMgr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,8 @@ int SongWriter::writeSong( Song * pSong, const QString& filename )
QDomNode songNode = doc.createElement( "song" );

LocalFileMng::writeXmlString( songNode, "version", QString( get_version().c_str() ) );
LocalFileMng::writeXmlString( songNode, "resolution", QString("%1").arg( pSong->getResolution() ) );

LocalFileMng::writeXmlString( songNode, "bpm", QString("%1").arg( pSong->getBpm() ) );
LocalFileMng::writeXmlString( songNode, "volume", QString("%1").arg( pSong->getVolume() ) );
LocalFileMng::writeXmlString( songNode, "metronomeVolume", QString("%1").arg( pSong->getMetronomeVolume() ) );
Expand All @@ -366,7 +368,7 @@ int SongWriter::writeSong( Song * pSong, const QString& filename )
LocalFileMng::writeXmlString( songNode, "license", pSong->getLicense() );
LocalFileMng::writeXmlBool( songNode, "loopEnabled", pSong->getIsLoopEnabled() );
LocalFileMng::writeXmlBool( songNode, "patternModeMode", Preferences::get_instance()->patternModePlaysSelected());

LocalFileMng::writeXmlString( songNode, "playbackTrackFilename", QString("%1").arg( pSong->getPlaybackTrackFilename() ) );
LocalFileMng::writeXmlBool( songNode, "playbackTrackEnabled", pSong->getPlaybackTrackEnabled() );
LocalFileMng::writeXmlString( songNode, "playbackTrackVolume", QString("%1").arg( pSong->getPlaybackTrackVolume() ) );
Expand Down
2 changes: 1 addition & 1 deletion src/core/Sampler/Sampler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1510,7 +1510,7 @@ void Sampler::preview_instrument(Instrument* pInstr )
m_pPreviewInstrument = pInstr;
pInstr->set_is_preview_instrument(true);

Note *pPreviewNote = new Note( m_pPreviewInstrument, 0, 1.0, 0.5, 0.5, MAX_NOTES, 0 );
Note *pPreviewNote = new Note( m_pPreviewInstrument, 0, 1.0, 0.5, 0.5, -1, 0 );

noteOn( pPreviewNote ); // exclusive note
AudioEngine::get_instance()->unlock();
Expand Down
6 changes: 3 additions & 3 deletions src/core/Smf/Smf.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,6 @@ std::vector<char> SMF::getBuffer()

// :::::::::::::::::::...

constexpr unsigned int TPQN = 192;
constexpr unsigned int DRUM_CHANNEL = 9;
constexpr unsigned int NOTE_LENGTH = 12;

Expand Down Expand Up @@ -379,7 +378,8 @@ SMF1Writer::~SMF1Writer()


SMF* SMF1Writer::createSMF( Song* pSong ){
SMF* pSmf = new SMF( 1, TPQN );
// TODO: MIDI export uses a TPQN 4 times as high as it should be. We should fix that.
SMF* pSmf = new SMF( 1, pSong->getResolution() * 4 );
// Standard MIDI format 1 files should have the first track being the tempo map
// which is a track that contains global meta events only.

Expand Down Expand Up @@ -534,7 +534,7 @@ SMF0Writer::~SMF0Writer()

SMF* SMF0Writer::createSMF( Song* pSong ){
// MIDI files format 0 have all their events in one track
SMF* pSmf = new SMF( 0, TPQN );
SMF* pSmf = new SMF( 0, pSong->getResolution() * 4 );
m_pTrack = createTrack0( pSong );
pSmf->addTrack( m_pTrack );
return pSmf;
Expand Down
Loading