More aggressive caching. In general, this makes Stepmania avoid looking in the song directory at all if the song is in the cache, in order to load songs faster. All the RadarValue calculations were also rolled into one to reduce the time needed to build the cache.

This commit is contained in:
Kyzentun
2015-03-23 11:35:30 -06:00
parent 05f455cc17
commit a2798e16f4
10 changed files with 502 additions and 457 deletions
+2
View File
@@ -41,6 +41,8 @@ enum RadarCategory
RadarCategory_Rolls, /**< How many rolls are in the song? */
RadarCategory_Lifts, /**< How many lifts are in the song? */
RadarCategory_Fakes, /**< How many fakes are in the song? */
// If you add another radar category, make sure you update
// NoteDataUtil::CalculateRadarValues to calculate it. -Kyz
NUM_RadarCategory, /**< The number of radar categories. */
RadarCategory_Invalid
};
+176 -178
View File
@@ -951,194 +951,192 @@ void NoteDataUtil::AutogenKickbox(const NoteData& in, NoteData& out, const Timin
out.RevalidateATIs(vector<int>(), false);
}
RadarStats CalculateRadarStatsFast( const NoteData &in, RadarStats &out )
struct recent_note_t
{
out.taps = 0;
out.jumps = 0;
out.hands = 0;
out.quads = 0;
map<int, int> simultaneousMap;
map<int, int> simultaneousMapNoHold;
map<int, int> simultaneousMapTapHoldHead;
map<int, int>::iterator itr;
for( int t=0; t<in.GetNumTracks(); t++ )
{
FOREACH_NONEMPTY_ROW_IN_TRACK_RANGE( in, t, r, 0, MAX_NOTE_ROW )
{
/* This function deals strictly with taps, jumps, hands, and quads.
* As such, all rows in here have to be judgable. */
if (!GAMESTATE->GetProcessedTimingData()->IsJudgableAtRow(r))
continue;
const TapNote &tn = in.GetTapNote(t, r);
switch( tn.type )
{
case TapNoteType_Mine:
case TapNoteType_Empty:
case TapNoteType_Fake:
case TapNoteType_AutoKeysound:
continue; // skip these types - they don't count
default: break;
}
if( (itr = simultaneousMap.find(r)) == simultaneousMap.end() )
simultaneousMap[r] = 1;
else
itr->second++;
if( (itr = simultaneousMapNoHold.find(r)) == simultaneousMapNoHold.end() )
simultaneousMapNoHold[r] = 1;
else
itr->second++;
if( tn.type == TapNoteType_Tap || tn.type == TapNoteType_Lift || tn.type == TapNoteType_HoldHead )
{
simultaneousMapTapHoldHead[r] = 1;
}
if( tn.type == TapNoteType_HoldHead )
{
int searchStartRow = r + 1;
int searchEndRow = r + tn.iDuration;
FOREACH_NONEMPTY_ROW_ALL_TRACKS_RANGE( in, rr, searchStartRow, searchEndRow )
{
switch( in.GetTapNote(t, rr).type )
{
case TapNoteType_Mine:
case TapNoteType_Empty:
case TapNoteType_Fake:
continue; // skip these types - they don't count
default: break;
}
if( (itr = simultaneousMap.find(rr)) == simultaneousMap.end() )
simultaneousMap[rr] = 1;
else
itr->second++;
}
}
}
}
for( itr = simultaneousMap.begin(); itr != simultaneousMap.end(); itr ++ )
{
if( itr->second >= 3 )
{
out.hands ++;
if( itr->second >= 4 )
{
out.quads ++;
}
}
}
for( itr = simultaneousMapNoHold.begin(); itr != simultaneousMapNoHold.end(); itr ++ )
{
if( itr->second >= 2 )
{
out.jumps ++;
}
}
out.taps = simultaneousMapTapHoldHead.size();
return out;
}
int row;
int track;
recent_note_t()
:row(0), track(0) {}
recent_note_t(int r, int t)
:row(r), track(t) {}
};
void NoteDataUtil::CalculateRadarValues( const NoteData &in, float fSongSeconds, RadarValues& out )
{
RadarStats stats;
CalculateRadarStatsFast( in, stats );
// The for loop and the assert are used to ensure that all fields of
// RadarValue get set in here.
FOREACH_ENUM( RadarCategory, rc )
int curr_row= -1;
bool judgable= false;
// recent_notes is used to calculate the voltage. Each element is the row
// and track number of a tap note. When the pair at the beginning is too
// old, it's deleted. This provides a way to have a rolling window
// that scans for the peak step density. -Kyz
vector<recent_note_t> recent_notes;
NoteData::all_tracks_const_iterator curr_note=
in.GetTapNoteRangeAllTracks(0, MAX_NOTE_ROW);
int num_notes_on_curr_row= 0;
TimingData* timing= GAMESTATE->GetProcessedTimingData();
// total_taps exists because the stream calculation needs GetNumTapNotes,
// but TapsAndHolds + Jumps + Hands would be inaccurate. -Kyz
float total_taps= 0;
// hold_ends tracks where currently active holds will end, which is used
// to count the number of hands. -Kyz
vector<int> hold_ends;
// num_holds_on_curr_row saves us the work of tracking where holds started
// just to keep a jump of two holds from counting as a hand.
int num_holds_on_curr_row= 0;
const float voltage_window_beats= 8.0f;
const int voltage_window= BeatToNoteRow(voltage_window_beats);
size_t max_notes_in_voltage_window= 0;
int num_chaos_rows= 0;
// Steps zeros the RadarValues before calling CalculateRadarValues. -Kyz
while(!curr_note.IsAtEnd())
{
switch( rc )
if(curr_note.Row() != curr_row)
{
case RadarCategory_Stream: out[rc] = GetStreamRadarValue( in, fSongSeconds ); break;
case RadarCategory_Voltage: out[rc] = GetVoltageRadarValue( in, fSongSeconds ); break;
case RadarCategory_Air: out[rc] = GetAirRadarValue( in, fSongSeconds ); break;
case RadarCategory_Freeze: out[rc] = GetFreezeRadarValue( in, fSongSeconds ); break;
case RadarCategory_Chaos: out[rc] = GetChaosRadarValue( in, fSongSeconds ); break;
case RadarCategory_TapsAndHolds: out[rc] = (float) stats.taps; break;
case RadarCategory_Jumps: out[rc] = (float) stats.jumps; break;
case RadarCategory_Holds: out[rc] = (float) in.GetNumHoldNotes(); break;
case RadarCategory_Mines: out[rc] = (float) in.GetNumMines(); break;
case RadarCategory_Hands: out[rc] = (float) in.GetNumHands(); break;
case RadarCategory_Rolls: out[rc] = (float) in.GetNumRolls(); break;
case RadarCategory_Lifts: out[rc] = (float) in.GetNumLifts(); break;
case RadarCategory_Fakes: out[rc] = (float) in.GetNumFakes(); break;
default: FAIL_M("Non-existant radar category attempted to be set!");
curr_row= curr_note.Row();
num_notes_on_curr_row= 0;
num_holds_on_curr_row= 0;
judgable= timing->IsJudgableAtRow(curr_row);
for(size_t n= 0; n < hold_ends.size(); ++n)
{
if(hold_ends[n] < curr_row)
{
hold_ends.erase(hold_ends.begin() + n);
--n;
}
}
for(size_t n= 0; n < recent_notes.size(); ++n)
{
if(recent_notes[n].row < curr_row - voltage_window)
{
recent_notes.erase(recent_notes.begin() + n);
--n;
}
else
{
// recent_notes is kept sorted, so reaching the first note that
// isn't old enough to remove means we're finished. -Kyz
break;
}
}
// GetChaosRadarValue did not care about whether a row is judgable.
// So chaos is checked here. -Kyz
if(GetNoteType(curr_row) >= NOTE_TYPE_12TH)
{
++num_chaos_rows;
}
}
if(judgable)
{
switch(curr_note->type)
{
case TapNoteType_Tap:
case TapNoteType_HoldHead:
// HoldTails and Attacks are counted by IsTap. -Kyz
case TapNoteType_HoldTail:
case TapNoteType_Attack:
++num_notes_on_curr_row;
// Rolls are not counted by GetNumHoldNotes, which is what
// GetStreamRadarValue and GetVoltageRadarValue used to use. -Kyz
switch(curr_note->type)
{
case TapNoteType_HoldHead:
case TapNoteType_HoldTail:
if(curr_note->subType != TapNoteSubType_Hold)
{
break;
}
// This is really dumb: GetNumTapNotes counts hold heads, and
// GetNumHoldNotes also counts hold heads. So stream and
// voltage count hold heads twice. -Kyz
++total_taps;
recent_notes.push_back(
recent_note_t(curr_row, curr_note.Track()));
case TapNoteType_Tap:
case TapNoteType_Attack:
++total_taps;
recent_notes.push_back(
recent_note_t(curr_row, curr_note.Track()));
max_notes_in_voltage_window= max(recent_notes.size(),
max_notes_in_voltage_window);
break;
}
// If there is one hold active, and one tap on this row, it does
// not count as a jump. Hands do need to count the number of
// holds active though. -Kyz
switch(num_notes_on_curr_row)
{
case 1:
++out[RadarCategory_TapsAndHolds];
break;
case 2:
++out[RadarCategory_Jumps];
break;
default:
break;
}
if(num_notes_on_curr_row + (hold_ends.size() -
num_holds_on_curr_row) >= 3)
{
++out[RadarCategory_Hands];
}
if(curr_note->type == TapNoteType_HoldHead)
{
hold_ends.push_back(curr_row + curr_note->iDuration);
++num_holds_on_curr_row;
switch(curr_note->subType)
{
case TapNoteSubType_Hold:
++out[RadarCategory_Holds];
break;
case TapNoteSubType_Roll:
++out[RadarCategory_Rolls];
break;
default:
break;
}
}
break;
case TapNoteType_Mine:
++out[RadarCategory_Mines];
break;
case TapNoteType_Lift:
++out[RadarCategory_Lifts];
break;
case TapNoteType_Fake:
++out[RadarCategory_Fakes];
break;
}
}
else
{
++out[RadarCategory_Fakes];
}
++curr_note;
}
}
float NoteDataUtil::GetStreamRadarValue( const NoteData &in, float fSongSeconds )
{
if( !fSongSeconds )
return 0.0f;
// density of steps
int iNumNotes = in.GetNumTapNotes() + in.GetNumHoldNotes();
float fNotesPerSecond = iNumNotes/fSongSeconds;
float fReturn = fNotesPerSecond / 7;
return min( fReturn, 1.0f );
}
float NoteDataUtil::GetVoltageRadarValue( const NoteData &in, float fSongSeconds )
{
if( !fSongSeconds )
return 0.0f;
const float fLastBeat = in.GetLastBeat();
const float fAvgBPS = fLastBeat / fSongSeconds;
// peak density of steps
float fMaxDensitySoFar = 0;
const float BEAT_WINDOW = 8;
const int BEAT_WINDOW_ROWS = BeatToNoteRow( BEAT_WINDOW );
for( int i=0; i<=BeatToNoteRow(fLastBeat); i+=BEAT_WINDOW_ROWS )
// Walking the notes complete, now assign any values that remain. -Kyz
#define CAP_RADAR(n) min(1.0f, (n))
if(fSongSeconds > 0.0f)
{
int iNumNotesThisWindow = in.GetNumTapNotes( i, i+BEAT_WINDOW_ROWS ) + in.GetNumHoldNotes( i, i+BEAT_WINDOW_ROWS );
float fDensityThisWindow = iNumNotesThisWindow / BEAT_WINDOW;
fMaxDensitySoFar = max( fMaxDensitySoFar, fDensityThisWindow );
out[RadarCategory_Stream]= CAP_RADAR((total_taps / fSongSeconds) / 7.0f);
// As seen in GetVoltageRadarValue: Don't use the timing data, just
// pretend the beats are evenly spaced. -Kyz
int avg_bps= in.GetLastBeat() / fSongSeconds;
out[RadarCategory_Voltage]= CAP_RADAR(
((max_notes_in_voltage_window / voltage_window_beats) * avg_bps) /
10.0f);
out[RadarCategory_Air]= CAP_RADAR(
out[RadarCategory_Jumps] / fSongSeconds);
out[RadarCategory_Freeze]= CAP_RADAR(
out[RadarCategory_Holds] / fSongSeconds);
out[RadarCategory_Chaos]= CAP_RADAR(
num_chaos_rows / fSongSeconds * .5f);
}
float fReturn = fMaxDensitySoFar*fAvgBPS/10;
return min( fReturn, 1.0f );
}
float NoteDataUtil::GetAirRadarValue( const NoteData &in, float fSongSeconds )
{
if( !fSongSeconds )
return 0.0f;
// number of doubles
int iNumDoubles = in.GetNumJumps();
float fReturn = iNumDoubles / fSongSeconds;
return min( fReturn, 1.0f );
}
float NoteDataUtil::GetFreezeRadarValue( const NoteData &in, float fSongSeconds )
{
if( !fSongSeconds )
return 0.0f;
// number of hold steps
float fReturn = in.GetNumHoldNotes() / fSongSeconds;
return min( fReturn, 1.0f );
}
float NoteDataUtil::GetChaosRadarValue( const NoteData &in, float fSongSeconds )
{
if( !fSongSeconds )
return 0.0f;
// count number of notes smaller than 8ths
int iNumChaosNotes = 0;
FOREACH_NONEMPTY_ROW_ALL_TRACKS( in, r )
{
if( GetNoteType(r) >= NOTE_TYPE_12TH )
iNumChaosNotes++;
}
float fReturn = iNumChaosNotes / fSongSeconds * 0.5f;
return min( fReturn, 1.0f );
#undef CAP_RADAR
// Sorry, there's not an assert here anymore for making sure all fields
// are set. There's a comment in the RadarCategory enum to direct
// attention here when adding new categories. -Kyz
}
void NoteDataUtil::RemoveHoldNotes( NoteData &in, int iStartIndex, int iEndIndex )
-21
View File
@@ -11,23 +11,9 @@ class Song;
struct AttackArray;
class TimingData;
/** @brief A limited selection of the RadarValues. */
struct RadarStats
{
/** @brief The number of tap notes in the song. */
int taps;
/** @brief The number of jumps in the song. */
int jumps;
/** @brief The number of 3 panel hits in the song. */
int hands;
/** @brief The number of 4 panel hits in the song. */
int quads;
};
void PlaceAutoKeysound( NoteData &out, int row, TapNote akTap );
int FindLongestOverlappingHoldNoteForAnyTrack( const NoteData &in, int iRow );
void LightTransformHelper( const NoteData &in, NoteData &out, const vector<int> &aiTracks );
RadarStats CalculateRadarStatsFast( const NoteData &in, RadarStats &out );
/**
* @brief Utility functions that deal with NoteData.
@@ -68,13 +54,6 @@ namespace NoteDataUtil
// later. -Kyz
void AutogenKickbox(const NoteData& in, NoteData& out, const TimingData& timing, StepsType out_type, int nonrandom_seed);
// radar values - return between 0.0 and 1.2
float GetStreamRadarValue( const NoteData &in, float fSongSeconds );
float GetVoltageRadarValue( const NoteData &in, float fSongSeconds );
float GetAirRadarValue( const NoteData &in, float fSongSeconds );
float GetFreezeRadarValue( const NoteData &in, float fSongSeconds );
float GetChaosRadarValue( const NoteData &in, float fSongSeconds );
void CalculateRadarValues( const NoteData &in, float fSongSeconds, RadarValues& out );
/**
+5
View File
@@ -58,6 +58,11 @@ float RageTimer::GetTimeSinceStart( bool bAccurate )
return uint32_t(usecs>>32) * 4294.967296f + uint32_t(usecs)/1000000.f;
}
uint64_t RageTimer::GetUsecsSinceStart()
{
return GetTime(true) - g_iStartTime;
}
void RageTimer::Touch()
{
uint64_t usecs = GetTime( true );
+4 -2
View File
@@ -23,6 +23,7 @@ public:
/* deprecated: */
static float GetTimeSinceStart( bool bAccurate = true ); // seconds since the program was started
static float GetTimeSinceStartFast() { return GetTimeSinceStart(false); }
static uint64_t GetUsecsSinceStart();
/* Get a timer representing half of the time ago as this one. */
RageTimer Half() const;
@@ -52,8 +53,9 @@ private:
extern const RageTimer RageZeroTimer;
// For profiling how long some chunk of code takes. -Kyz
#define START_TIME(name) float name##_start_time= RageTimer::GetTimeSinceStartFast();
#define END_TIME(name) float name##_end_time= RageTimer::GetTimeSinceStartFast(); LOG->Warn(#name " time: %f to %f = %f", name##_start_time, name##_end_time, name##_end_time - name##_start_time);
#define START_TIME(name) uint64_t name##_start_time= RageTimer::GetUsecsSinceStart();
#define END_TIME(name) uint64_t name##_end_time= RageTimer::GetUsecsSinceStart(); LOG->Warn(#name " time: %zu to %zu = %zu", name##_start_time, name##_end_time, name##_end_time - name##_start_time);
#define END_TIME_ADD_TO(name) uint64_t name##_end_time= RageTimer::GetUsecsSinceStart(); name##_total += name##_end_time - name##_start_time;
#endif
+41
View File
@@ -970,6 +970,47 @@ void MakeValidFilename( RString &sName )
sName = WStringToRString( wsName );
}
bool FindFirstFilenameContaining(const vector<RString>& filenames,
RString& out, const vector<RString>& starts_with,
const vector<RString>& contains, const vector<RString>& ends_with)
{
for(size_t i= 0; i < filenames.size(); ++i)
{
RString lower= GetFileNameWithoutExtension(filenames[i]);
lower.MakeLower();
for(size_t s= 0; s < starts_with.size(); ++s)
{
if(!lower.compare(0, starts_with[s].size(), starts_with[s]))
{
out= filenames[i];
return true;
}
}
size_t lower_size= lower.size();
for(size_t s= 0; s < ends_with.size(); ++s)
{
if(lower_size >= ends_with[s].size())
{
size_t end_pos= lower_size - ends_with[s].size();
if(!lower.compare(end_pos, string::npos, ends_with[s]))
{
out= filenames[i];
return true;
}
}
}
for(size_t s= 0; s < contains.size(); ++s)
{
if(lower.find(contains[s]) != string::npos)
{
out= filenames[i];
return true;
}
}
}
return false;
}
int g_argc = 0;
char **g_argv = NULL;
+5
View File
@@ -382,6 +382,11 @@ RString GetExtension( const RString &sPath );
RString GetFileNameWithoutExtension( const RString &sPath );
void MakeValidFilename( RString &sName );
bool FindFirstFilenameContaining(
const vector<RString>& filenames, RString& out,
const vector<RString>& starts_with,
const vector<RString>& contains, const vector<RString>& ends_with);
extern const wchar_t INVALID_CHAR;
int utf8_get_char_len( char p );
+8 -5
View File
@@ -4775,11 +4775,14 @@ void ScreenEdit::HandleMainMenuChoice( MainMenuChoice c, const vector<int> &iAns
g_StepsData.rows[lifts].SetOneUnthemedChoice( ssprintf("%d", m_NoteDataEdit.GetNumLifts()) );
g_StepsData.rows[fakes].SetOneUnthemedChoice( ssprintf("%d", m_NoteDataEdit.GetNumFakes()) );
}
g_StepsData.rows[stream].SetOneUnthemedChoice( ssprintf("%.2f", NoteDataUtil::GetStreamRadarValue(m_NoteDataEdit,fMusicSeconds)) );
g_StepsData.rows[voltage].SetOneUnthemedChoice( ssprintf("%.2f", NoteDataUtil::GetVoltageRadarValue(m_NoteDataEdit,fMusicSeconds)) );
g_StepsData.rows[air].SetOneUnthemedChoice( ssprintf("%.2f", NoteDataUtil::GetAirRadarValue(m_NoteDataEdit,fMusicSeconds)) );
g_StepsData.rows[freeze].SetOneUnthemedChoice( ssprintf("%.2f", NoteDataUtil::GetFreezeRadarValue(m_NoteDataEdit,fMusicSeconds)) );
g_StepsData.rows[chaos].SetOneUnthemedChoice( ssprintf("%.2f", NoteDataUtil::GetChaosRadarValue(m_NoteDataEdit,fMusicSeconds)) );
RadarValues radar;
radar.Zero();
NoteDataUtil::CalculateRadarValues(m_NoteDataEdit, fMusicSeconds, radar);
g_StepsData.rows[stream].SetOneUnthemedChoice(ssprintf("%.2f", radar[RadarCategory_Stream]));
g_StepsData.rows[voltage].SetOneUnthemedChoice(ssprintf("%.2f", radar[RadarCategory_Voltage]));
g_StepsData.rows[air].SetOneUnthemedChoice(ssprintf("%.2f", radar[RadarCategory_Air]));
g_StepsData.rows[freeze].SetOneUnthemedChoice(ssprintf("%.2f", radar[RadarCategory_Freeze]));
g_StepsData.rows[chaos].SetOneUnthemedChoice(ssprintf("%.2f", radar[RadarCategory_Chaos]));
EditMiniMenu( &g_StepsData, SM_BackFromStepsData, SM_None );
break;
}
+260 -251
View File
@@ -43,7 +43,7 @@
* @brief The internal version of the cache for StepMania.
*
* Increment this value to invalidate the current cache. */
const int FILE_CACHE_VERSION = 223;
const int FILE_CACHE_VERSION = 224;
/** @brief How long does a song sample last by default? */
const float DEFAULT_MUSIC_SAMPLE_LENGTH = 12.f;
@@ -292,9 +292,9 @@ bool Song::LoadFromSongDir( RString sDir, bool load_autosave )
RString sCacheFilePath = GetCacheFilePath();
if( !DoesFileExist(sCacheFilePath) )
bUseCache = false;
if( !PREFSMAN->m_bFastLoad && GetHashForDirectory(m_sSongDir) != uCacheHash )
bUseCache = false; // this cache is out of date
{ bUseCache = false; }
if(!PREFSMAN->m_bFastLoad && GetHashForDirectory(m_sSongDir) != uCacheHash)
{ bUseCache = false; } // this cache is out of date
if(load_autosave)
{ bUseCache= false; }
@@ -344,6 +344,10 @@ bool Song::LoadFromSongDir( RString sDir, bool load_autosave )
}
TidyUpData(false, true);
// If edits are not cached, looking for them causes a substantial hit to
// loading time. -Kyz
LoadEditsFromSongDir(sDir);
// Don't save a cache file if the autosave is being loaded, because the
// cache file would contain the autosave filename. -Kyz
if(!load_autosave)
@@ -369,24 +373,6 @@ bool Song::LoadFromSongDir( RString sDir, bool load_autosave )
if( PREFSMAN->m_BackgroundCache == BGCACHE_LOW_RES_PRELOAD && m_bHasBackground )
BACKGROUNDCACHE->LoadBackground( GetBackgroundPath() );
*/
// Load any .edit files in the song folder.
// Doing this BEFORE setting up AutoGen just in case.
vector<RString> vs;
GetDirListing( sDir + "*.edit", vs, false, false);
// XXX: I'm sure there's a StepMania way of doing this, but familiar with this codebase I am not.
for(unsigned int i = 0; i < vs.size(); ++i) {
// Try SSCLoader
SSCLoader ldSSC;
if( ldSSC.LoadEditFromFile(sDir + vs[i], ProfileSlot_Invalid, true, this) != true )
{
// No dice? Try SMLoader then. If SMLoader fails too, well whatever.
// We don't have to do anything to fail gracefully.
SMLoader ldSM;
ldSM.LoadEditFromFile(sDir + vs[i], ProfileSlot_Invalid, true, this);
}
}
// Note: If vs.empty() then this loop is skipped entirely (vs.size() == 0)
// Add AutoGen pointers. (These aren't cached.)
AddAutoGenNotes();
@@ -396,7 +382,6 @@ bool Song::LoadFromSongDir( RString sDir, bool load_autosave )
LOG->UserLog( "Song", sDir, "has no music; ignored." );
return false; // don't load this song
}
return true; // do load this song
}
@@ -469,6 +454,28 @@ bool Song::ReloadFromSongDir( RString sDir )
return true;
}
void Song::LoadEditsFromSongDir(RString dir)
{
// Load any .edit files in the song folder.
// Doing this BEFORE setting up AutoGen just in case.
vector<RString> vs;
GetDirListing(dir + "*.edit", vs, false, false);
// XXX: I'm sure there's a StepMania way of doing this, but familiar with this codebase I am not.
for(unsigned int i = 0; i < vs.size(); ++i)
{
// Try SSCLoader
SSCLoader ldSSC;
if(ldSSC.LoadEditFromFile(dir + vs[i], ProfileSlot_Invalid, true, this) != true)
{
// No dice? Try SMLoader then. If SMLoader fails too, well whatever.
// We don't have to do anything to fail gracefully.
SMLoader ldSM;
ldSM.LoadEditFromFile(dir + vs[i], ProfileSlot_Invalid, true, this);
}
}
// Note: If vs.empty() then this loop is skipped entirely (vs.size() == 0)
}
bool Song::HasAutosaveFile()
{
if(m_sSongFileName.empty())
@@ -530,7 +537,7 @@ void FixupPath( RString &path, const RString &sSongPath )
}
// Songs in BlacklistImages will never be autodetected as song images.
void Song::TidyUpData( bool fromCache, bool /* duringCache */ )
void Song::TidyUpData( bool from_cache, bool /* duringCache */ )
{
// We need to do this before calling any of HasMusic, HasHasCDTitle, etc.
ASSERT_M( m_sSongDir.Left(3) != "../", m_sSongDir ); // meaningless
@@ -559,262 +566,265 @@ void Song::TidyUpData( bool fromCache, bool /* duringCache */ )
Trim(this->m_sMainTitle);
}
if( !HasMusic() )
{
vector<RString> arrayPossibleMusic;
FILEMAN->GetDirListingWithMultipleExtensions(m_sSongDir,
ActorUtil::GetTypeExtensionList(FT_Sound), arrayPossibleMusic);
if( !arrayPossibleMusic.empty() )
{
int idx = 0;
/* If the first song is "intro", and we have more than one available,
* don't use it--it's probably a KSF intro music file, which we don't
* (yet) support. */
if( arrayPossibleMusic.size() > 1 &&
!arrayPossibleMusic[0].Left(5).CompareNoCase("intro") )
++idx;
// we found a match
m_sMusicFile = arrayPossibleMusic[idx];
}
}
// This must be done before radar calculation.
if(m_fMusicLengthSeconds > 0.0f || lastSecond > -1.0f)
{
if(m_fMusicLengthSeconds == 0.0f)
{
m_fMusicLengthSeconds= lastSecond;
}
}
else if(HasMusic())
{
RString error;
RageSoundReader *Sample = RageSoundReader_FileReader::OpenFile( GetMusicPath(), error );
/* XXX: Checking if the music file exists eliminates a warning
* originating from BMS files (which have no music file, per se)
* but it's something of a hack. */
if( Sample == NULL && m_sMusicFile != "" )
{
LOG->UserLog( "Sound file", GetMusicPath(), "couldn't be opened: %s", error.c_str() );
// Don't use this file.
m_sMusicFile = "";
}
else if ( Sample != NULL )
{
m_fMusicLengthSeconds = Sample->GetLength() / 1000.0f;
delete Sample;
if( m_fMusicLengthSeconds < 0 )
{
// It failed; bad file or something. It's already logged a warning.
m_fMusicLengthSeconds = 100; // guess
}
else if( m_fMusicLengthSeconds == 0 )
{
LOG->UserLog( "Sound file", GetMusicPath(), "is empty." );
}
}
}
else // ! HasMusic()
{
m_fMusicLengthSeconds = 100; // guess
LOG->UserLog("Song",
GetSongDir(),
"has no music file; guessing at %f seconds",
m_fMusicLengthSeconds);
}
if( m_fMusicLengthSeconds < 0 )
{
LOG->UserLog("Sound file",
GetMusicPath(),
"has a negative length %f.",
m_fMusicLengthSeconds);
m_fMusicLengthSeconds = 0;
}
m_SongTiming.TidyUpData( false );
FOREACH( Steps *, m_vpSteps, s )
{
(*s)->m_Timing.TidyUpData( true );
}
/* Generate these before we autogen notes, so the new notes can inherit
* their source's values. */
ReCalculateRadarValuesAndLastSecond( fromCache, true );
Trim( m_sMainTitle );
Trim( m_sSubTitle );
Trim( m_sArtist );
// Fall back on the song directory name.
if( m_sMainTitle == "" )
NotesLoader::GetMainAndSubTitlesFromFullTitle(Basename(this->GetSongDir()),
m_sMainTitle, m_sSubTitle );
if(m_sMainTitle == "")
NotesLoader::GetMainAndSubTitlesFromFullTitle(
Basename(this->GetSongDir()), m_sMainTitle, m_sSubTitle);
if( m_sArtist == "" )
if(m_sArtist == "")
m_sArtist = "Unknown artist";
TranslateTitles();
if( m_fMusicSampleStartSeconds == -1 ||
m_fMusicSampleLengthSeconds == 0 ||
m_fMusicSampleStartSeconds+m_fMusicSampleLengthSeconds > this->m_fMusicLengthSeconds )
{
const TimingData &timing = this->m_SongTiming;
m_fMusicSampleStartSeconds = timing.GetElapsedTimeFromBeat( 100 );
if( m_fMusicSampleStartSeconds+m_fMusicSampleLengthSeconds > this->m_fMusicLengthSeconds )
{
// Attempt to get a reasonable default.
int iBeat = lrintf(this->m_SongTiming.GetBeatFromElapsedTime(this->GetLastSecond())/2);
iBeat -= iBeat%4;
m_fMusicSampleStartSeconds = timing.GetElapsedTimeFromBeat( (float)iBeat );
}
}
// The old logic meant that you couldn't have sample lengths that go forever,
// e.g. those in Donkey Konga. I never liked that. -freem
if( m_fMusicSampleLengthSeconds <= 0.00f )
m_fMusicSampleLengthSeconds = DEFAULT_MUSIC_SAMPLE_LENGTH;
// Here's the problem: We have a directory full of images. We want to
// determine which image is the banner, which is the background, and which
// is the CDTitle.
CHECKPOINT_M( "Looking for images..." );
CHECKPOINT_M("Looking for images...");
if( !fromCache )
m_SongTiming.TidyUpData(false);
FOREACH(Steps *, m_vpSteps, s)
{
(*s)->m_Timing.TidyUpData(true);
}
if(!from_cache)
{
// Set the has flags before tidying so that tidying can check them instead
// of using the has functions that hit the disk. -Kyz
// These will be written to cache, for Song::LoadFromSongDir to use later.
m_bHasMusic = HasMusic();
m_bHasBanner = HasBanner();
m_bHasBackground = HasBackground();
if(m_bHasBanner)
{ BANNERCACHE->CacheBanner(GetBannerPath()); }
/*
if(m_bHasBackground)
{ BANNERCACHE->CacheBackground(GetBackgroundPath()); }
*/
if(!m_bHasMusic)
{
vector<RString> arrayPossibleMusic;
FILEMAN->GetDirListingWithMultipleExtensions(m_sSongDir,
ActorUtil::GetTypeExtensionList(FT_Sound), arrayPossibleMusic);
if( !arrayPossibleMusic.empty() )
{
int idx = 0;
/* If the first song is "intro", and we have more than one available,
* don't use it--it's probably a KSF intro music file, which we don't
* (yet) support. */
if( arrayPossibleMusic.size() > 1 &&
!arrayPossibleMusic[0].Left(5).CompareNoCase("intro") )
++idx;
// we found a match
m_sMusicFile = arrayPossibleMusic[idx];
m_bHasMusic= true;
}
}
// This must be done before radar calculation.
if(m_bHasMusic)
{
RString error;
RageSoundReader *Sample = RageSoundReader_FileReader::OpenFile(GetMusicPath(), error);
/* XXX: Checking if the music file exists eliminates a warning
* originating from BMS files (which have no music file, per se)
* but it's something of a hack. */
if(Sample == NULL && m_sMusicFile != "")
{
LOG->UserLog("Sound file", GetMusicPath(), "couldn't be opened: %s", error.c_str());
// Don't use this file.
m_sMusicFile = "";
}
else if(Sample != NULL)
{
m_fMusicLengthSeconds = Sample->GetLength() / 1000.0f;
delete Sample;
if(m_fMusicLengthSeconds < 0)
{
// It failed; bad file or something. It's already logged a warning.
m_fMusicLengthSeconds = 100; // guess
}
else if(m_fMusicLengthSeconds == 0)
{
LOG->UserLog("Sound file", GetMusicPath(), "is empty.");
}
}
}
else // ! HasMusic()
{
m_fMusicLengthSeconds = 100; // guess
LOG->UserLog("Song",
GetSongDir(),
"has no music file; guessing at %f seconds",
m_fMusicLengthSeconds);
}
if(m_fMusicLengthSeconds < 0)
{
LOG->UserLog("Sound file",
GetMusicPath(),
"has a negative length %f.",
m_fMusicLengthSeconds);
m_fMusicLengthSeconds = 0;
}
if(m_fMusicSampleStartSeconds == -1 ||
m_fMusicSampleLengthSeconds == 0 ||
m_fMusicSampleStartSeconds+m_fMusicSampleLengthSeconds > this->m_fMusicLengthSeconds)
{
const TimingData &timing = this->m_SongTiming;
m_fMusicSampleStartSeconds = timing.GetElapsedTimeFromBeat(100);
if(m_fMusicSampleStartSeconds+m_fMusicSampleLengthSeconds > this->m_fMusicLengthSeconds)
{
// Attempt to get a reasonable default.
int iBeat = lrintf(this->m_SongTiming.GetBeatFromElapsedTime(this->GetLastSecond())/2);
iBeat -= iBeat%4;
m_fMusicSampleStartSeconds = timing.GetElapsedTimeFromBeat((float)iBeat);
}
}
// The old logic meant that you couldn't have sample lengths that go forever,
// e.g. those in Donkey Konga. I never liked that. -freem
if(m_fMusicSampleLengthSeconds <= 0.00f)
{ m_fMusicSampleLengthSeconds = DEFAULT_MUSIC_SAMPLE_LENGTH; }
// Fetch the list of images in the song dir ONCE, not TWELVE TIMES! -Kyz
vector<RString> image_dir_list;
GetImageDirListing(m_sSongDir + "*", image_dir_list);
// For blank args to FindFirstFilenameContaining. -Kyz
vector<RString> empty_list;
bool has_jacket= HasJacket();
bool has_cdimage= HasCDImage();
bool has_disc= HasDisc();
bool has_cdtitle= HasCDTitle();
// First, check the file name for hints.
if( !HasBanner() )
if(!m_bHasBanner)
{
/* If a nonexistant banner file is specified, and we can't find a
* replacement, don't wipe out the old value. */
//m_sBannerFile = "";
// find an image with "banner" in the file name
vector<RString> arrayPossibleBanners;
GetImageDirListing( m_sSongDir + "*banner*", arrayPossibleBanners );
vector<RString> contains(1, "banner");
/* Some people do things differently for the sake of being different.
* Don't match eg. abnormal, numbness. */
GetImageDirListing( m_sSongDir + "* BN", arrayPossibleBanners );
if( !arrayPossibleBanners.empty() )
m_sBannerFile = arrayPossibleBanners[0];
vector<RString> ends_with(1, " bn");
m_bHasBanner= FindFirstFilenameContaining(image_dir_list,
m_sBannerFile, empty_list, contains, ends_with);
}
if( !HasBackground() )
if(!m_bHasBackground)
{
//m_sBackgroundFile = "";
// find an image with "bg" or "background" in the file name
vector<RString> arrayPossibleBGs;
GetImageDirListing( m_sSongDir + "*background*", arrayPossibleBGs );
// don't match e.g. "subgroup", "hobgoblin", etc.
GetImageDirListing( m_sSongDir + "*bg", arrayPossibleBGs );
if( !arrayPossibleBGs.empty() )
m_sBackgroundFile = arrayPossibleBGs[0];
vector<RString> contains(1, "background");
vector<RString> ends_with(1, "bg");
m_bHasBackground= FindFirstFilenameContaining(image_dir_list,
m_sBackgroundFile, empty_list, contains, ends_with);
}
if( !HasJacket() )
if(!has_jacket)
{
// find an image with "jacket" or "albumart" in the filename.
vector<RString> arrayPossibleJackets;
GetImageDirListing( m_sSongDir + "jk_*", arrayPossibleJackets );
GetImageDirListing( m_sSongDir + "*jacket*", arrayPossibleJackets );
GetImageDirListing( m_sSongDir + "*albumart*", arrayPossibleJackets );
if( !arrayPossibleJackets.empty() )
m_sJacketFile = arrayPossibleJackets[0];
vector<RString> starts_with(1, "jk_");
vector<RString> contains;
contains.reserve(2);
contains.push_back("jacket");
contains.push_back("albumart");
has_jacket= FindFirstFilenameContaining(image_dir_list,
m_sJacketFile, starts_with, contains, empty_list);
}
if( !HasCDImage() )
if(!has_cdimage)
{
// CD image, a la ddr 1st-3rd (not to be confused with CDTitles)
// find an image with "-cd" at the end of the filename.
vector<RString> arrayPossibleCDImages;
GetImageDirListing( m_sSongDir + "*-cd", arrayPossibleCDImages );
if( !arrayPossibleCDImages.empty() )
m_sCDFile = arrayPossibleCDImages[0];
vector<RString> ends_with(1, "-cd");
has_cdimage= FindFirstFilenameContaining(image_dir_list,
m_sCDFile, empty_list, empty_list, ends_with);
}
if( !HasDisc() )
if(!has_disc)
{
// a rectangular graphic, not to be confused with CDImage above.
vector<RString> arrayPossibleDiscImages;
GetImageDirListing( m_sSongDir + "* disc", arrayPossibleDiscImages );
GetImageDirListing( m_sSongDir + "* title", arrayPossibleDiscImages );
if( !arrayPossibleDiscImages.empty() )
m_sDiscFile = arrayPossibleDiscImages[0];
vector<RString> ends_with;
ends_with.reserve(2);
ends_with.push_back(" disc");
ends_with.push_back(" title");
has_disc= FindFirstFilenameContaining(image_dir_list,
m_sDiscFile, empty_list, empty_list, ends_with);
}
if( !HasCDTitle() )
if(!has_cdtitle)
{
// find an image with "cdtitle" in the file name
vector<RString> arrayPossibleCDTitles;
GetImageDirListing( m_sSongDir + "*cdtitle*", arrayPossibleCDTitles );
if( !arrayPossibleCDTitles.empty() )
m_sCDTitleFile = arrayPossibleCDTitles[0];
vector<RString> contains(1, "cdtitle");
has_cdtitle= FindFirstFilenameContaining(image_dir_list,
m_sCDTitleFile, empty_list, contains, empty_list);
}
if( !HasLyrics() )
if(!HasLyrics())
{
// Check if there is a lyric file in here
vector<RString> arrayLyricFiles;
GetDirListing(m_sSongDir + RString("*.lrc"), arrayLyricFiles );
if( !arrayLyricFiles.empty() )
m_sLyricsFile = arrayLyricFiles[0];
vector<RString> lyric_files;
GetDirListing(m_sSongDir + RString("*.lrc"), lyric_files);
if(!lyric_files.empty())
{ m_sLyricsFile = lyric_files[0]; }
}
/* Now, For the images we still haven't found,
* look at the image dimensions of the remaining unclassified images. */
vector<RString> arrayImages;
GetImageDirListing( m_sSongDir + "*", arrayImages );
for( unsigned i=0; i<arrayImages.size(); i++ ) // foreach image
for(unsigned int i= 0; i < image_dir_list.size(); ++i) // foreach image
{
if( HasBanner() && HasCDTitle() && HasBackground() )
if(m_bHasBanner && m_bHasBackground && has_cdtitle)
break; // done
// ignore DWI "-char" graphics
RString sLower = arrayImages[i];
sLower.MakeLower();
if( BlacklistedImages.find(sLower) != BlacklistedImages.end() )
RString lower = image_dir_list[i];
lower.MakeLower();
if(BlacklistedImages.find(lower) != BlacklistedImages.end())
continue; // skip
// Skip any image that we've already classified
if( HasBanner() && m_sBannerFile.EqualsNoCase(arrayImages[i]) )
if(m_bHasBanner && m_sBannerFile.EqualsNoCase(image_dir_list[i]))
continue; // skip
if( HasBackground() && m_sBackgroundFile.EqualsNoCase(arrayImages[i]) )
if(m_bHasBackground && m_sBackgroundFile.EqualsNoCase(image_dir_list[i]))
continue; // skip
if( HasCDTitle() && m_sCDTitleFile.EqualsNoCase(arrayImages[i]) )
if(has_cdtitle && m_sCDTitleFile.EqualsNoCase(image_dir_list[i]))
continue; // skip
if( HasJacket() && m_sJacketFile.EqualsNoCase(arrayImages[i]) )
if(has_jacket && m_sJacketFile.EqualsNoCase(image_dir_list[i]))
continue; // skip
if( HasDisc() && m_sDiscFile.EqualsNoCase(arrayImages[i]) )
if(has_disc && m_sDiscFile.EqualsNoCase(image_dir_list[i]))
continue; // skip
if( HasCDImage() && m_sCDFile.EqualsNoCase(arrayImages[i]) )
if(has_cdimage && m_sCDFile.EqualsNoCase(image_dir_list[i]))
continue; // skip
RString sPath = m_sSongDir + arrayImages[i];
RString sPath = m_sSongDir + image_dir_list[i];
// We only care about the dimensions.
RString error;
RageSurface *img = RageSurfaceUtils::LoadFile( sPath, error, true );
if( !img )
RageSurface *img = RageSurfaceUtils::LoadFile(sPath, error, true);
if(!img)
{
LOG->UserLog( "Graphic file", sPath, "couldn't be loaded: %s", error.c_str() );
LOG->UserLog("Graphic file", sPath, "couldn't be loaded: %s", error.c_str());
continue;
}
@@ -822,23 +832,27 @@ void Song::TidyUpData( bool fromCache, bool /* duringCache */ )
const int height = img->h;
delete img;
if( !HasBackground() && width >= 320 && height >= 240 )
if(!m_bHasBackground && width >= 320 && height >= 240)
{
m_sBackgroundFile = arrayImages[i];
m_sBackgroundFile = image_dir_list[i];
m_bHasBackground= true;
continue;
}
if( !HasBanner() && 100<=width && width<=320 && 50<=height && height<=240 )
if(!m_bHasBanner && 100 <= width && width <= 320 &&
50 <= height && height <= 240)
{
m_sBannerFile = arrayImages[i];
m_sBannerFile = image_dir_list[i];
m_bHasBanner= true;
continue;
}
/* Some songs have overlarge banners. Check if the ratio is reasonable
* (over 2:1; usually over 3:1), and large (not a cdtitle). */
if( !HasBanner() && width > 200 && float(width) / height > 2.0f )
if(!m_bHasBanner && width > 200 && float(width) / height > 2.0f)
{
m_sBannerFile = arrayImages[i];
m_sBannerFile = image_dir_list[i];
m_bHasBanner= true;
continue;
}
@@ -857,75 +871,70 @@ void Song::TidyUpData( bool fromCache, bool /* duringCache */ )
* various information about the song in question). As it stands,
* I'm keeping this code until I figure out wtf to do -aj
*/
if( !HasCDTitle() && width<=100 && height<=48 )
if(!has_cdtitle && width <= 100 && height <= 48)
{
m_sCDTitleFile = arrayImages[i];
m_sCDTitleFile = image_dir_list[i];
has_cdtitle= true;
continue;
}
// Jacket files typically have the same width and height.
if( !HasJacket() && width == height )
if(!has_jacket && width == height)
{
m_sJacketFile = arrayImages[i];
m_sJacketFile = image_dir_list[i];
has_jacket= true;
continue;
}
// Disc images are typically rectangular; make sure we have a banner already.
if( !HasDisc() && (width > height) && HasBanner() )
if(!has_disc && (width > height) && m_bHasBanner)
{
if( arrayImages[i] != m_sBannerFile )
m_sDiscFile = arrayImages[i];
if(image_dir_list[i] != m_sBannerFile)
{
m_sDiscFile = image_dir_list[i];
has_disc= true;
}
continue;
}
// CD images are the same as Jackets, typically the same width and height
if( !HasCDImage() && width == height )
if(!has_cdimage && width == height)
{
m_sCDFile = arrayImages[i];
m_sCDFile = image_dir_list[i];
has_cdimage= true;
continue;
}
}
// If no BGChanges are specified and there are movies in the song
// directory, then assume they are DWI style where the movie begins at
// beat 0.
if(!HasBGChanges())
{
vector<RString> arrayPossibleMovies;
FILEMAN->GetDirListingWithMultipleExtensions(m_sSongDir,
ActorUtil::GetTypeExtensionList(FT_Movie), arrayPossibleMovies);
}
// These will be written to cache, for Song::LoadFromSongDir to use later.
m_bHasMusic = HasMusic();
m_bHasBanner = HasBanner();
m_bHasBackground = HasBackground();
if( HasBanner() )
BANNERCACHE->CacheBanner( GetBannerPath() );
/*
if( HasBackground() )
BANNERCACHE->CacheBackground( GetBackgroundPath() );
*/
// If no BGChanges are specified and there are movies in the song directory, then assume
// they are DWI style where the movie begins at beat 0.
if( (!HasBGChanges() && !fromCache) )
{
vector<RString> arrayPossibleMovies;
FILEMAN->GetDirListingWithMultipleExtensions(m_sSongDir,
ActorUtil::GetTypeExtensionList(FT_Movie), arrayPossibleMovies);
/* Use this->GetBeatFromElapsedTime(0) instead of 0 to start when the
* music starts. */
if( arrayPossibleMovies.size() == 1 )
/* Use this->GetBeatFromElapsedTime(0) instead of 0 to start when the
* music starts. */
if(arrayPossibleMovies.size() == 1)
this->AddBackgroundChange(BACKGROUND_LAYER_1,
BackgroundChange(0,
arrayPossibleMovies[0],
"",
1.f,
SBE_StretchNoLoop));
BackgroundChange(0, arrayPossibleMovies[0], "", 1.f,
SBE_StretchNoLoop));
}
// Don't allow multiple Steps of the same StepsType and Difficulty
// (except for edits). We should be able to use difficulty names as
// unique identifiers for steps. */
SongUtil::AdjustDuplicateSteps(this);
}
/* Don't allow multiple Steps of the same StepsType and Difficulty (except
* for edits). We should be able to use difficulty names as unique
* identifiers for steps. */
if( !fromCache )
/* Generate these before we autogen notes, so the new notes can inherit
* their source's values. */
ReCalculateRadarValuesAndLastSecond(from_cache, true);
// If the music length is suspiciously shorter than the last second, adjust
// the length. This prevents the ogg patch from setting a false length. -Kyz
if(m_fMusicLengthSeconds < lastSecond - 10.0f)
{
SongUtil::AdjustDuplicateSteps( this );
m_fMusicLengthSeconds= lastSecond;
}
}
+1
View File
@@ -90,6 +90,7 @@ public:
bool LoadFromSongDir( RString sDir, bool load_autosave= false );
// This one takes the effort to reuse Steps pointers as best as it can
bool ReloadFromSongDir( RString sDir );
void LoadEditsFromSongDir(RString dir);
bool HasAutosaveFile();
bool LoadAutosaveFile();