#include "global.h" #include "NoteDataUtil.h" #include "RageUtil.h" #include "RageLog.h" #include "PlayerOptions.h" #include "song.h" #include "GameState.h" #include "RadarValues.h" #include "Foreach.h" NoteType NoteDataUtil::GetSmallestNoteTypeForMeasure( const NoteData &n, int iMeasureIndex ) { const int iMeasureStartIndex = iMeasureIndex * ROWS_PER_MEASURE; const int iMeasureLastIndex = (iMeasureIndex+1) * ROWS_PER_MEASURE - 1; // probe to find the smallest note type NoteType nt; for( nt=(NoteType)0; nt iNewNumTracks ) { /* Use a different algorithm for reducing tracks. */ LoadOverlapped( in, out, iNewNumTracks ); return; } NoteData Original; Original.To4s( in ); out.Config( in ); out.SetNumTracks( iNewNumTracks ); int iCurTrackOffset = 0; int iTrackOffsetMin = 0; int iTrackOffsetMax = abs( iNewNumTracks - Original.GetNumTracks() ); int bOffsetIncreasing = true; int iLastRow = Original.GetLastRow(); int iLastMeasure = 0; for( int r=0; r<=iLastRow; ) { // copy notes in this measure for( int t=0; t= NOTE_TYPE_12TH ) iNumChaosNotes++; } float fReturn = iNumChaosNotes / fSongSeconds * 0.5f; return min( fReturn, 1.0f ); } void NoteDataUtil::RemoveHoldNotes(NoteData &in, float fStartBeat, float fEndBeat) { int iStartIndex = BeatToNoteRow( fStartBeat ); int iEndIndex = BeatToNoteRow( fEndBeat ); // turn all the HoldNotes into TapNotes for( int i=in.GetNumHoldNotes()-1; i>=0; i-- ) // iterate backwards so we can delete { const HoldNote hn = in.GetHoldNote( i ); if( !hn.RangeOverlaps(iStartIndex,iEndIndex) ) continue; // skip in.RemoveHoldNote( i ); in.SetTapNote( hn.iTrack, hn.iStartRow, TAP_ORIGINAL_TAP ); } } void NoteDataUtil::RemoveSimultaneousNotes(NoteData &in, int iMaxSimultaneous, float fStartBeat, float fEndBeat) { int iStartIndex = BeatToNoteRow( fStartBeat ); int iEndIndex = BeatToNoteRow( fEndBeat ); // turn all the HoldNotes into TapNotes FOREACH_NONEMPTY_ROW_ALL_TRACKS_RANGE( in, r, iStartIndex, iEndIndex ) { set viTracksHeld; in.GetTracksHeldAtRow( r, viTracksHeld ); // remove the first tap note or the first hold note that starts on this row int iTotalTracksPressed = in.GetNumTracksWithTap(r) + viTracksHeld.size(); int iTracksToRemove = max( 0, iTotalTracksPressed - iMaxSimultaneous ); for( int t=0; iTracksToRemove>0 && tm_iRoundSeed; do { RandomGen rnd( iShuffleSeed ); random_shuffle( &iTakeFromTrack[0], &iTakeFromTrack[NumTracks], rnd ); iShuffleSeed++; } while ( !memcmp( iOrig, iTakeFromTrack, sizeof(iOrig) ) ); } break; case NoteDataUtil::stomp: switch( st ) { case STEPS_TYPE_DANCE_SINGLE: case STEPS_TYPE_DANCE_COUPLE: iTakeFromTrack[0] = 3; iTakeFromTrack[1] = 2; iTakeFromTrack[2] = 1; iTakeFromTrack[3] = 0; iTakeFromTrack[4] = 7; iTakeFromTrack[5] = 6; iTakeFromTrack[6] = 5; iTakeFromTrack[7] = 4; break; case STEPS_TYPE_DANCE_DOUBLE: iTakeFromTrack[0] = 1; iTakeFromTrack[1] = 0; iTakeFromTrack[2] = 3; iTakeFromTrack[3] = 2; iTakeFromTrack[4] = 5; iTakeFromTrack[5] = 4; iTakeFromTrack[6] = 7; iTakeFromTrack[7] = 6; break; default: break; } break; default: ASSERT(0); } } static void SuperShuffleTaps( NoteData &inout, int iStartIndex, int iEndIndex ) { /* We already did the normal shuffling code above, which did a good job * of shuffling HoldNotes without creating impossible patterns. * Now, go in and shuffle the TapNotes per-row. * * This is only called by NoteDataUtil::Turn. "in" is in 4s, and iStartIndex * and iEndIndex are in range. */ FOREACH_NONEMPTY_ROW_ALL_TRACKS_RANGE( inout, r, iStartIndex, iEndIndex ) { for( int t1=0; t1=0; i-- ) if( fmodf(inout.GetHoldNote(i).GetStartBeat(),1) != 0 ) // doesn't start on a beat inout.RemoveHoldNote( i ); } void NoteDataUtil::Wide( NoteData &inout, float fStartBeat, float fEndBeat ) { // Make all all quarter notes into jumps. inout.ConvertHoldNotesTo4s(); /* Start on an even beat. */ fStartBeat = Quantize( fStartBeat, 2.0f ); const int first_row = BeatToNoteRow( fStartBeat ); const int last_row = min( BeatToNoteRow(fEndBeat), inout.GetLastRow() ); for( int i=first_row; i 0 ) { bSpaceAroundIsEmpty = false; break; } if( !bSpaceAroundIsEmpty ) continue; // skip // add a note determinitsitcally int iBeat = (int)roundf( NoteRowToBeat(i) ); int iTrackOfNote = inout.GetFirstTrackWithTap(i); int iTrackToAdd = iTrackOfNote + (iBeat%5)-2; // won't be more than 2 tracks away from the existing note CLAMP( iTrackToAdd, 0, inout.GetNumTracks()-1 ); if( iTrackToAdd == iTrackOfNote ) iTrackToAdd++; CLAMP( iTrackToAdd, 0, inout.GetNumTracks()-1 ); if( iTrackToAdd == iTrackOfNote ) iTrackToAdd--; CLAMP( iTrackToAdd, 0, inout.GetNumTracks()-1 ); if( inout.GetTapNote(iTrackToAdd, i).type != TapNote::empty ) { iTrackToAdd = (iTrackToAdd+1) % inout.GetNumTracks(); } inout.SetTapNote(iTrackToAdd, i, TAP_ADDITION_TAP); } inout.Convert4sToHoldNotes(); } void NoteDataUtil::Big( NoteData &inout, float fStartBeat, float fEndBeat ) { InsertIntelligentTaps(inout,1.0f,0.5f,1.0f,false,fStartBeat,fEndBeat); // add 8ths between 4ths } void NoteDataUtil::Quick( NoteData &inout, float fStartBeat, float fEndBeat ) { InsertIntelligentTaps(inout,0.5f,0.25f,1.0f,false,fStartBeat,fEndBeat); // add 16ths between 8ths } // Due to popular request by people annoyed with the "new" implementation of Quick, we now have // this BMR-izer for your steps. Use with caution. void NoteDataUtil::BMRize( NoteData &inout, float fStartBeat, float fEndBeat ) { Big( inout, fStartBeat, fEndBeat ); Quick( inout, fStartBeat, fEndBeat ); } void NoteDataUtil::Skippy( NoteData &inout, float fStartBeat, float fEndBeat ) { InsertIntelligentTaps(inout,1.0f,0.75f,1.0f,true,fStartBeat,fEndBeat); // add 16ths between 4ths } void NoteDataUtil::InsertIntelligentTaps( NoteData &inout, float fWindowSizeBeats, float fInsertOffsetBeats, float fWindowStrideBeats, bool bSkippy, float fStartBeat, float fEndBeat ) { ASSERT( fInsertOffsetBeats <= fWindowSizeBeats ); ASSERT( fWindowSizeBeats <= fWindowStrideBeats ); inout.ConvertHoldNotesTo4s(); bool bRequireNoteAtBeginningOfWindow = !bSkippy; bool bRequireNoteAtEndOfWindow = true; /* Start on a multiple of fBeatInterval. */ fStartBeat = Quantize( fStartBeat, fWindowStrideBeats ); // Insert a beat in the middle of every fBeatInterval. const int first_row = BeatToNoteRow( fStartBeat ); const int last_row = min( BeatToNoteRow(fEndBeat), inout.GetLastRow() ); const int rows_per_window = BeatToNoteRow( fWindowSizeBeats ); const int rows_per_stride = BeatToNoteRow( fWindowStrideBeats ); const int insert_row_offset = BeatToNoteRow( fInsertOffsetBeats ); for( int i=first_row; i= 2 ) { iTrackOfNoteToAdd = min(iTrackOfNoteEarlier,iTrackOfNoteLater)+1; goto done_looking_for_track_to_add; } // try to choose a track just to the left if( min(iTrackOfNoteEarlier,iTrackOfNoteLater)-1 >= 0 ) { iTrackOfNoteToAdd = min(iTrackOfNoteEarlier,iTrackOfNoteLater)-1; goto done_looking_for_track_to_add; } // try to choose a track just to the right if( max(iTrackOfNoteEarlier,iTrackOfNoteLater)+1 < inout.GetNumTracks() ) { iTrackOfNoteToAdd = max(iTrackOfNoteEarlier,iTrackOfNoteLater)+1; goto done_looking_for_track_to_add; } done_looking_for_track_to_add: inout.SetTapNote(iTrackOfNoteToAdd, iRowToAdd, TAP_ADDITION_TAP); } inout.Convert4sToHoldNotes(); } void NoteDataUtil::AddMines( NoteData &inout, float fStartBeat, float fEndBeat ) { const int first_row = BeatToNoteRow( fStartBeat ); const int last_row = min( BeatToNoteRow(fEndBeat), inout.GetLastRow() ); // // Change whole rows at a time to be tap notes. Otherwise, it causes // major problems for our scoring system. -Chris // int iRowCount = 0; int iPlaceEveryRows = 6; FOREACH_NONEMPTY_ROW_ALL_TRACKS_RANGE( inout, r, first_row, last_row ) { iRowCount++; // place every 6 or 7 rows // XXX: What is "6 or 7" derived from? Can we calculate that in a way // that won't break if ROWS_PER_MEASURE changes? if( iRowCount>=iPlaceEveryRows ) { for( int t=0; t last_row ) continue; // Only place a mines if there's not another step nearby int iMineRangeBegin = BeatToNoteRow( fMineBeat-0.5f ) + 1; int iMineRangeEnd = BeatToNoteRow( fMineBeat+0.5f ) - 1; if( !inout.IsRangeEmpty(hn.iTrack, iMineRangeBegin, iMineRangeEnd) ) continue; // Add a mine right after the hold end. inout.SetTapNote( hn.iTrack, iMineRow, TAP_ADDITION_MINE ); // Convert all notes inout this row to mines. for( int t=0; t viTracks; inout.GetTracksHeldAtRow( iRowEcho, viTracks ); // don't lay if holding 2 already if( viTracks.size() >= 2 ) continue; // don't lay // don't lay echos on top of a HoldNote if( find(viTracks.begin(),viTracks.end(),iEchoTrack) != viTracks.end() ) continue; // don't lay } inout.SetTapNote( iEchoTrack, iRowEcho, TAP_ADDITION_TAP ); } } void NoteDataUtil::Planted( NoteData &inout, float fStartBeat, float fEndBeat ) { ConvertTapsToHolds( inout, 1, fStartBeat, fEndBeat ); } void NoteDataUtil::Floored( NoteData &inout, float fStartBeat, float fEndBeat ) { ConvertTapsToHolds( inout, 2, fStartBeat, fEndBeat ); } void NoteDataUtil::Twister( NoteData &inout, float fStartBeat, float fEndBeat ) { ConvertTapsToHolds( inout, 3, fStartBeat, fEndBeat ); } void NoteDataUtil::ConvertTapsToHolds( NoteData &inout, int iSimultaneousHolds, float fStartBeat, float fEndBeat ) { // Convert all taps to freezes. const int first_row = BeatToNoteRow( fStartBeat ); const int last_row = min( BeatToNoteRow(fEndBeat), inout.GetLastRow() ); FOREACH_NONEMPTY_ROW_ALL_TRACKS_RANGE( inout, r, first_row, last_row ) { int iTrackAddedThisRow = 0; for( int t=0; t iSimultaneousHolds ) break; if( inout.GetTapNote(t,r).type == TapNote::tap ) { // Find the ending row for this hold int iTapsLeft = iSimultaneousHolds; int r2 = r+1; FOREACH_NONEMPTY_ROW_ALL_TRACKS_RANGE( inout, next_row, r+1, last_row ) { r2 = next_row; // If there are two taps in a row on the same track, // don't convert the earlier one to a hold. if( inout.GetTapNote(t,r2).type != TapNote::empty ) goto dont_add_hold; set tracksDown; inout.GetTracksHeldAtRow( r2, tracksDown ); inout.GetTapNonEmptyTracks( r2, tracksDown ); iTapsLeft -= tracksDown.size(); if( iTapsLeft == 0 ) break; // we found the ending row for this hold else if( iTapsLeft < 0 ) goto dont_add_hold; } float fStartBeat = NoteRowToBeat(r); float fEndBeat = NoteRowToBeat(r2); // If the steps end in a tap, convert that tap // to a hold that lasts for at least one beat. if( r2==r+1 ) fEndBeat = fStartBeat+1; inout.AddHoldNote( HoldNote(t,BeatToNoteRow(fStartBeat),BeatToNoteRow(fEndBeat)) ); iTrackAddedThisRow++; } dont_add_hold: ; } } } void NoteDataUtil::Stomp( NoteData &inout, StepsType st, float fStartBeat, float fEndBeat ) { // Make all non jumps with ample space around them into jumps. const int first_row = BeatToNoteRow( fStartBeat ); const int last_row = min( BeatToNoteRow(fEndBeat), inout.GetLastRow() ); int iTrackMapping[MAX_NOTE_TRACKS]; GetTrackMapping( st, stomp, inout.GetNumTracks(), iTrackMapping ); FOREACH_NONEMPTY_ROW_ALL_TRACKS_RANGE( inout, r, first_row, last_row ) { if( inout.GetNumTracksWithTap(r) != 1 ) continue; // skip for( int t=0; t= 1 ) continue; int iOppositeTrack = iTrackMapping[t]; inout.SetTapNote( iOppositeTrack, r, TAP_ADDITION_TAP ); } } } } void NoteDataUtil::SnapToNearestNoteType( NoteData &inout, NoteType nt1, NoteType nt2, float fBeginBeat, float fEndBeat ) { // nt2 is optional and should be NOTE_TYPE_INVALID if it is not used float fSnapInterval1, fSnapInterval2; switch( nt1 ) { case NOTE_TYPE_4TH: fSnapInterval1 = 1/1.0f; break; case NOTE_TYPE_8TH: fSnapInterval1 = 1/2.0f; break; case NOTE_TYPE_12TH: fSnapInterval1 = 1/3.0f; break; case NOTE_TYPE_16TH: fSnapInterval1 = 1/4.0f; break; case NOTE_TYPE_24TH: fSnapInterval1 = 1/6.0f; break; case NOTE_TYPE_32ND: fSnapInterval1 = 1/8.0f; break; case NOTE_TYPE_48TH: fSnapInterval1 = 1/12.0f; break; case NOTE_TYPE_64TH: fSnapInterval1 = 1/16.0f; break; default: ASSERT( false ); return; } switch( nt2 ) { case NOTE_TYPE_4TH: fSnapInterval2 = 1/1.0f; break; case NOTE_TYPE_8TH: fSnapInterval2 = 1/2.0f; break; case NOTE_TYPE_12TH: fSnapInterval2 = 1/3.0f; break; case NOTE_TYPE_16TH: fSnapInterval2 = 1/4.0f; break; case NOTE_TYPE_24TH: fSnapInterval2 = 1/6.0f; break; case NOTE_TYPE_32ND: fSnapInterval2 = 1/8.0f; break; case NOTE_TYPE_48TH: fSnapInterval2 = 1/12.0f; break; case NOTE_TYPE_64TH: fSnapInterval2 = 1/16.0f; break; case NOTE_TYPE_INVALID: fSnapInterval2 = 10000; break; // nothing will ever snap to this. That's what we want! default: ASSERT( false ); return; } int rowBegin = BeatToNoteRow( fBeginBeat ); int rowEnd = BeatToNoteRow( fEndBeat ); inout.ConvertHoldNotesTo2sAnd3s(); // iterate over all TapNotes in the interval and snap them FOREACH_NONEMPTY_ROW_ALL_TRACKS_RANGE( inout, i, rowBegin, rowEnd ) { int iOldIndex = i; float fOldBeat = NoteRowToBeat( iOldIndex ); float fNewBeat1 = Quantize( fOldBeat, fSnapInterval1 ); float fNewBeat2 = Quantize( fOldBeat, fSnapInterval2 ); bool bNewBeat1IsCloser = fabsf(fNewBeat1-fOldBeat) < fabsf(fNewBeat2-fOldBeat); float fNewBeat = bNewBeat1IsCloser ? fNewBeat1 : fNewBeat2; int iNewIndex = BeatToNoteRow( fNewBeat ); for( int c=0; c0; t-- ) // inout.GetNumTracks()-1 times { int iTrackEarlier = t; int iTrackLater = (t+1) % inout.GetNumTracks(); // swap TapNote tnEarlier = inout.GetTapNote(iTrackEarlier, r); TapNote tnLater = inout.GetTapNote(iTrackLater, r); inout.SetTapNote(iTrackEarlier, r, tnLater); inout.SetTapNote(iTrackLater, r, tnEarlier); } } inout.Convert4sToHoldNotes(); } struct ValidRow { StepsType st; bool bValidMask[MAX_NOTE_TRACKS]; }; #define T true #define f false const ValidRow g_ValidRows[] = { { STEPS_TYPE_DANCE_DOUBLE, { T,T,T,T,f,f,f,f } }, { STEPS_TYPE_DANCE_DOUBLE, { f,T,T,T,T,f,f,f } }, { STEPS_TYPE_DANCE_DOUBLE, { f,f,f,T,T,T,T,f } }, { STEPS_TYPE_DANCE_DOUBLE, { f,f,f,f,T,T,T,T } }, }; void NoteDataUtil::FixImpossibleRows( NoteData &inout, StepsType st ) { vector vpValidRowsToCheck; for( unsigned i=0; isModifier ); if( po.ContainsTransformOrTurn() ) { float fStartBeat, fEndBeat; a->GetAttackBeats( pSong, NULL, fStartBeat, fEndBeat ); NoteDataUtil::TransformNoteData( nd, po, st, fStartBeat, fEndBeat ); } } } void NoteDataUtil::TransformNoteData( NoteData &nd, const PlayerOptions &po, StepsType st, float fStartBeat, float fEndBeat ) { // Apply remove transforms before others so that we don't go removing // notes we just inserted. if( po.m_bTransforms[PlayerOptions::TRANSFORM_LITTLE] ) NoteDataUtil::Little(nd, fStartBeat, fEndBeat); if( po.m_bTransforms[PlayerOptions::TRANSFORM_NOHOLDS] ) NoteDataUtil::RemoveHoldNotes(nd, fStartBeat, fEndBeat); if( po.m_bTransforms[PlayerOptions::TRANSFORM_NOMINES] ) NoteDataUtil::RemoveMines(nd, fStartBeat, fEndBeat); if( po.m_bTransforms[PlayerOptions::TRANSFORM_NOJUMPS] ) NoteDataUtil::RemoveJumps(nd, fStartBeat, fEndBeat); if( po.m_bTransforms[PlayerOptions::TRANSFORM_NOHANDS] ) NoteDataUtil::RemoveHands(nd, fStartBeat, fEndBeat); if( po.m_bTransforms[PlayerOptions::TRANSFORM_NOQUADS] ) NoteDataUtil::RemoveQuads(nd, fStartBeat, fEndBeat); // Apply inserts. if( po.m_bTransforms[PlayerOptions::TRANSFORM_BIG] ) NoteDataUtil::Big(nd, fStartBeat, fEndBeat); if( po.m_bTransforms[PlayerOptions::TRANSFORM_QUICK] ) NoteDataUtil::Quick(nd, fStartBeat, fEndBeat); if( po.m_bTransforms[PlayerOptions::TRANSFORM_BMRIZE] ) NoteDataUtil::BMRize(nd, fStartBeat, fEndBeat); // Skippy will still add taps to places that the other // AddIntelligentTaps above won't. if( po.m_bTransforms[PlayerOptions::TRANSFORM_SKIPPY] ) NoteDataUtil::Skippy(nd, fStartBeat, fEndBeat); // These aren't affects by the above inserts very much. if( po.m_bTransforms[PlayerOptions::TRANSFORM_MINES] ) NoteDataUtil::AddMines(nd, fStartBeat, fEndBeat); if( po.m_bTransforms[PlayerOptions::TRANSFORM_ECHO] ) NoteDataUtil::Echo(nd, fStartBeat, fEndBeat); // Jump-adding transforms aren't much affected by additional taps. if( po.m_bTransforms[PlayerOptions::TRANSFORM_WIDE] ) NoteDataUtil::Wide(nd, fStartBeat, fEndBeat); if( po.m_bTransforms[PlayerOptions::TRANSFORM_STOMP] ) NoteDataUtil::Stomp(nd, st, fStartBeat, fEndBeat); // Transforms that add holds go last. If they went first, most tap-adding // transforms wouldn't do anything because tap-adding transforms skip areas // where there's a hold. if( po.m_bTransforms[PlayerOptions::TRANSFORM_PLANTED] ) NoteDataUtil::Planted(nd, fStartBeat, fEndBeat); if( po.m_bTransforms[PlayerOptions::TRANSFORM_FLOORED] ) NoteDataUtil::Floored(nd, fStartBeat, fEndBeat); if( po.m_bTransforms[PlayerOptions::TRANSFORM_TWISTER] ) NoteDataUtil::Twister(nd, fStartBeat, fEndBeat); // Apply turns and shuffles last to that they affect inserts. if( po.m_bTurns[PlayerOptions::TURN_MIRROR] ) NoteDataUtil::Turn( nd, st, NoteDataUtil::mirror, fStartBeat, fEndBeat ); if( po.m_bTurns[PlayerOptions::TURN_LEFT] ) NoteDataUtil::Turn( nd, st, NoteDataUtil::left, fStartBeat, fEndBeat ); if( po.m_bTurns[PlayerOptions::TURN_RIGHT] ) NoteDataUtil::Turn( nd, st, NoteDataUtil::right, fStartBeat, fEndBeat ); if( po.m_bTurns[PlayerOptions::TURN_SHUFFLE] ) NoteDataUtil::Turn( nd, st, NoteDataUtil::shuffle, fStartBeat, fEndBeat ); if( po.m_bTurns[PlayerOptions::TURN_SUPER_SHUFFLE] ) NoteDataUtil::Turn( nd, st, NoteDataUtil::super_shuffle, fStartBeat, fEndBeat ); } void NoteDataUtil::AddTapAttacks( NoteData &nd, Song* pSong ) { // throw an attack in every 30 seconds const char* szAttacks[3] = { "2x", "drunk", "dizzy", }; for( float sec=15; secm_fMusicLengthSeconds; sec+=30 ) { float fBeat = pSong->GetBeatFromElapsedTime( sec ); int iBeat = (int)fBeat; int iTrack = iBeat % nd.GetNumTracks(); // deterministically calculates track nd.SetTapAttackNote( iTrack, BeatToNoteRow(fBeat), szAttacks[rand()%ARRAYSIZE(szAttacks)], 15 ); } } #if 0 // undo this if ScaleRegion breaks more things than it fixes void NoteDataUtil::Scale( NoteData &nd, float fScale ) { ASSERT( fScale > 0 ); NoteData temp; temp.CopyAll( &nd ); nd.ClearAll(); for( int r=0; r<=temp.GetLastRow(); r++ ) { for( int t=0; t 0 ); ASSERT( fStartBeat < fEndBeat ); ASSERT( fStartBeat >= 0 ); NoteData temp1, temp2; temp1.Config( nd ); temp2.Config( nd ); const int iFirstRowAtEndOfRegion = min( nd.GetLastRow(), BeatToNoteRowNotRounded(fEndBeat) ); const int iScaledFirstRowAfterRegion = (int)((fStartBeat + (fEndBeat - fStartBeat) * fScale) * ROWS_PER_BEAT); if( fStartBeat != 0 ) temp1.CopyRange( nd, 0, BeatToNoteRowNotRounded(fStartBeat) ); if( nd.GetLastRow() > iFirstRowAtEndOfRegion ) temp1.CopyRange( nd, iFirstRowAtEndOfRegion, nd.GetLastRow(), iScaledFirstRowAfterRegion); temp2.CopyRange( nd, BeatToNoteRowNotRounded(fStartBeat), iFirstRowAtEndOfRegion ); nd.ClearAll(); for( int r=0; r<=temp2.GetLastRow(); r++ ) { for( int t=0; t 0 ) // add blank rows iPasteAtRow += BeatToNoteRow( fBeatsToShift ); else // delete rows iTakeFromRow += BeatToNoteRow( -fBeatsToShift ); temp.CopyRange( nd, iTakeFromRow, nd.GetLastRow() ); nd.ClearRange( min(iTakeFromRow,iPasteAtRow), nd.GetLastRow() ); nd.CopyRange( temp, 0, temp.GetLastRow(), iPasteAtRow ); } void NoteDataUtil::RemoveAllTapsOfType( NoteData& ndInOut, TapNote::Type typeToRemove ) { for( int t=0; t