diff --git a/aegisub/audio_spectrum.cpp b/aegisub/audio_spectrum.cpp index baf1037d4..5e8a624d7 100644 --- a/aegisub/audio_spectrum.cpp +++ b/aegisub/audio_spectrum.cpp @@ -35,6 +35,10 @@ // #include +#include +#include +#include +#include #include "audio_spectrum.h" #include "fft.h" #include "colorspace.h" @@ -44,15 +48,55 @@ // Audio spectrum FFT data cache +// Spectrum cache basically caches the raw result of FFT +class AudioSpectrumCache { +public: + // Type of a single FFT result line + typedef std::vector CacheLine; + + // Types for cache aging + typedef unsigned int CacheAccessTime; + struct CacheAgeData { + CacheAccessTime access_time; + unsigned long first_line; + unsigned long num_lines; // includes overlap-lines + bool operator< (const CacheAgeData& second) { return access_time < second.access_time; } + CacheAgeData(CacheAccessTime t, unsigned long first, unsigned long num) : access_time(t), first_line(first), num_lines(num) { } + }; + typedef std::vector CacheAgeList; + + // Get the overlap'th overlapping FFT in FFT group i, generating it if needed + virtual CacheLine& GetLine(unsigned long i, unsigned int overlap, bool &created, CacheAccessTime access_time) = 0; + + // Get the total number of cache lines currently stored in this cache node's sub tree + virtual size_t GetManagedLineCount() = 0; + + // Append to a list of last access times to the cache + virtual void GetLineAccessTimes(CacheAgeList &ages) = 0; + + // Delete the cache storage starting with the given line id + // Return true if the object called on is empty and can safely be deleted too + virtual bool KillLine(unsigned long line_id) = 0; + + // Set the FFT size used + static void SetLineLength(unsigned long new_length) + { + line_length = new_length; + null_line.resize(new_length, 0); + } + + virtual ~AudioSpectrumCache() {}; + +protected: + // A cache line containing only zero-values + static CacheLine null_line; + // The FFT size + static unsigned long line_length; +}; + AudioSpectrumCache::CacheLine AudioSpectrumCache::null_line; unsigned long AudioSpectrumCache::line_length; -void AudioSpectrumCache::SetLineLength(unsigned long new_length) -{ - line_length = new_length; - null_line.resize(new_length, 0); -} - // Bottom level FFT cache, holds actual power data itself @@ -62,9 +106,13 @@ private: unsigned long start, length; // start and end of range unsigned int overlaps; + CacheAccessTime last_access; + public: - CacheLine& GetLine(unsigned long i, unsigned int overlap, bool &created) + CacheLine& GetLine(unsigned long i, unsigned int overlap, bool &created, CacheAccessTime access_time) { + last_access = access_time; + // This check ought to be redundant if (i >= start && i-start < length) return data[i - start + overlap*length]; @@ -72,6 +120,21 @@ public: return null_line; } + size_t GetManagedLineCount() + { + return data.size(); + } + + void GetLineAccessTimes(CacheAgeList &ages) + { + ages.push_back(CacheAgeData(last_access, start, data.size())); + } + + bool KillLine(unsigned long line_id) + { + return start == line_id; + } + FinalSpectrumCache(AudioProvider *provider, unsigned long _start, unsigned long _length, unsigned int _overlaps) { start = _start; @@ -149,7 +212,7 @@ private: AudioProvider *provider; public: - CacheLine &GetLine(unsigned long i, unsigned int overlap, bool &created) + CacheLine &GetLine(unsigned long i, unsigned int overlap, bool &created, CacheAccessTime access_time) { if (i >= start && i-start <= length) { // Determine which sub-cache this line resides in @@ -165,12 +228,46 @@ public: } } - return sub_caches[subcache]->GetLine(i, overlap, created); + return sub_caches[subcache]->GetLine(i, overlap, created, access_time); } else { return null_line; } } + size_t GetManagedLineCount() + { + size_t res = 0; + for (size_t i = 0; i < sub_caches.size(); ++i) { + if (sub_caches[i]) + res += sub_caches[i]->GetManagedLineCount(); + } + return res; + } + + void GetLineAccessTimes(CacheAgeList &ages) + { + for (size_t i = 0; i < sub_caches.size(); ++i) { + if (sub_caches[i]) + sub_caches[i]->GetLineAccessTimes(ages); + } + } + + bool KillLine(unsigned long line_id) + { + int sub_caches_left = 0; + for (size_t i = 0; i < sub_caches.size(); ++i) { + if (sub_caches[i]) { + if (sub_caches[i]->KillLine(line_id)) { + delete sub_caches[i]; + sub_caches[i] = 0; + } else { + sub_caches_left++; + } + } + } + return sub_caches_left == 0; + } + IntermediateSpectrumCache(AudioProvider *_provider, unsigned long _start, unsigned long _length, unsigned int _overlaps, int _depth) { provider = _provider; @@ -203,6 +300,91 @@ public: }; + +class AudioSpectrumCacheManager { +private: + IntermediateSpectrumCache *cache_root; + unsigned long cache_hits, cache_misses; + AudioSpectrumCache::CacheAccessTime cur_time; + + unsigned long max_lines_cached; + +public: + AudioSpectrumCache::CacheLine &GetLine(unsigned long i, unsigned int overlap) + { + bool created = false; + AudioSpectrumCache::CacheLine &res = cache_root->GetLine(i, overlap, created, cur_time++); + if (created) + cache_misses++; + else + cache_hits++; + return res; + } + + void Age() + { + wxLogDebug(_T("AudioSpectrumCacheManager stats: hits=%u, misses=%u, misses%%=%f, managed lines=%u (max=%u)"), cache_hits, cache_misses, cache_misses/float(cache_hits+cache_misses)*100, cache_root->GetManagedLineCount(), max_lines_cached); + + // 0 means no limit + if (max_lines_cached == 0) + return; + // No reason to proceed with complicated stuff if the count is too small + // (FIXME: does this really pay off?) + if (cache_root->GetManagedLineCount() < max_lines_cached) + return; + + // Get and sort ages + AudioSpectrumCache::CacheAgeList ages; + cache_root->GetLineAccessTimes(ages); + std::sort(ages.begin(), ages.end()); + + // Number of lines we have found used so far + // When this exceeds max_lines_caches go into kill-mode + unsigned long cumulative_lines = 0; + // Run backwards through the line age list (the most recently accessed items are at end) + AudioSpectrumCache::CacheAgeList::reverse_iterator it = ages.rbegin(); + + // Find the point where we have too many lines cached + while (cumulative_lines < max_lines_cached) { + if (it == ages.rend()) { + wxLogDebug(_T("AudioSpectrumCacheManager done aging did not exceed max_lines_cached")); + return; + } + cumulative_lines += it->num_lines; + ++it; + } + + // By here, we have exceeded max_lines_cached so backtrack one + --it; + + // And now start cleaning up + for (; it != ages.rend(); ++it) { + cache_root->KillLine(it->first_line); + } + + wxLogDebug(_T("AudioSpectrumCacheManager done aging, managed lines now=%u (max=%u)"), cache_root->GetManagedLineCount(), max_lines_cached); + assert(cache_root->GetManagedLineCount() < max_lines_cached); + } + + AudioSpectrumCacheManager(AudioProvider *provider, unsigned long line_length, unsigned long num_lines, unsigned int num_overlaps) + { + cache_hits = cache_misses = 0; + cur_time = 0; + cache_root = new IntermediateSpectrumCache(provider, 0, num_lines, num_overlaps, 0); + + // option is stored in megabytes, but we want number of bytes + unsigned long max_cache_size = Options.AsInt(_T("Audio Spectrum Memory Max")) * 1024 * 1024; + unsigned long line_size = sizeof(AudioSpectrumCache::CacheLine::value_type) * line_length; + max_lines_cached = max_cache_size / line_size; + } + + ~AudioSpectrumCacheManager() + { + delete cache_root; + } +}; + + // AudioSpectrum AudioSpectrum::AudioSpectrum(AudioProvider *_provider) @@ -228,7 +410,7 @@ AudioSpectrum::AudioSpectrum(AudioProvider *_provider) num_lines = (unsigned long)_num_lines; AudioSpectrumCache::SetLineLength(line_length); - cache = new IntermediateSpectrumCache(provider, 0, num_lines, fft_overlaps, 0); + cache = new AudioSpectrumCacheManager(provider, line_length, num_lines, fft_overlaps); power_scale = 1; minband = Options.AsInt(_T("Audio Spectrum Cutoff")); @@ -261,9 +443,6 @@ void AudioSpectrum::RenderRange(__int64 range_start, __int64 range_end, bool sel unsigned long first_line = (unsigned long)(fft_overlaps * range_start / line_length / 2); unsigned long last_line = (unsigned long)(fft_overlaps * range_end / line_length / 2); - unsigned int cache_hits=0, cache_misses=0; - bool was_cache_miss; - float *power = new float[line_length]; int last_imgcol_rendered = -1; @@ -290,15 +469,12 @@ void AudioSpectrum::RenderRange(__int64 range_start, __int64 range_end, bool sel if (imgcol <= last_imgcol_rendered) continue; - was_cache_miss = false; - AudioSpectrumCache::CacheLine &line = cache->GetLine(baseline, overlap, was_cache_miss); + AudioSpectrumCache::CacheLine &line = cache->GetLine(baseline, overlap); ++overlap; if (overlap >= fft_overlaps) { overlap = 0; ++baseline; } - if (was_cache_miss) cache_misses++; - else cache_hits++; // Apply a "compressed" scaling to the signal power for (unsigned int j = 0; j < line_length; j++) { @@ -365,7 +541,7 @@ void AudioSpectrum::RenderRange(__int64 range_start, __int64 range_end, bool sel delete[] power; - wxLogDebug(_T("Rendered spectrum: %u cache hits, %u misses"), cache_hits, cache_misses); + cache->Age(); } diff --git a/aegisub/audio_spectrum.h b/aegisub/audio_spectrum.h index be13176c3..6c4197344 100644 --- a/aegisub/audio_spectrum.h +++ b/aegisub/audio_spectrum.h @@ -38,36 +38,17 @@ #define AUDIO_SPECTRUM_H #include -#include #include "audio_provider.h" -// Spectrum cache basically caches the raw result of FFT -class AudioSpectrumCache { -public: - // Type of a single FFT result line - typedef std::vector CacheLine; - - // Get the overlap'th overlapping FFT in FFT group i, generating it if needed - virtual CacheLine& GetLine(unsigned long i, unsigned int overlap, bool &created) = 0; - - // Set the FFT size used - static void SetLineLength(unsigned long new_length); - - virtual ~AudioSpectrumCache() {}; - -protected: - // A cache line containing only zero-values - static CacheLine null_line; - // The FFT size - static unsigned long line_length; -}; +// Specified and implemented in cpp file, interface is private to spectrum code +class AudioSpectrumCacheManager; class AudioSpectrum { private: // Data provider - AudioSpectrumCache *cache; + AudioSpectrumCacheManager *cache; // Colour pallettes unsigned char colours_normal[256*3]; diff --git a/aegisub/changelog.txt b/aegisub/changelog.txt index 9514c5a3b..54f04cc21 100644 --- a/aegisub/changelog.txt +++ b/aegisub/changelog.txt @@ -55,11 +55,13 @@ Please visit http://aegisub.net to download latest version o Misc. changes and bugfixes in karaoke mode. Using the syllable splitter should be easier now. (jfs) o Fixed loading of Avisynth Scripts as audio. (AMZ) - Changes to Audio Spectrum: (jfs) - o The calculated FFT data are now cached, so things should be faster - o Actual signal power is now more accurately represented - o The palette is changed - o The selection is no longer shown by ugly reverse colour but with a different palette instead - o Use vertical zoom slider to amplify/dampen displayed signal strength (useful for better visualisation of quiet sections, or easier picking out the dominating frequencies in noisy sections) + o The calculated FFT data are now cached, so things should be faster. + - The maximum size of the cache can be configured. Default is unlimited, which provides the best performance assuming enough memory is available. + o The quality of the spectrum can be easier configured, better quality requires more CPU and memory. + o Actual signal power is now more accurately represented. + o The palette is changed. + o The selection is no longer shown by ugly reverse colour but with a different palette instead. + o Use vertical zoom slider to amplify/dampen displayed signal strength. (Useful for better visualisation of quiet sections, or easier picking out the dominating frequencies in noisy sections.) - Style Manager changes: o Automatically select the style of the current line when Style Manager is opened. (jfs) o Style storages with invalid characters are now caught, and the invalid characters replaced with safe ones. Previously such storages seemed to be created correctly but were never stored to disk. (jfs) diff --git a/aegisub/dialog_options.cpp b/aegisub/dialog_options.cpp index 82e6f98f7..c5345ddab 100644 --- a/aegisub/dialog_options.cpp +++ b/aegisub/dialog_options.cpp @@ -530,6 +530,7 @@ DialogOptions::DialogOptions(wxWindow *parent) AddTextControl(audioAdvPage,audioAdvSizer1,_("Spectrum cutoff"),_T("Audio spectrum cutoff"),TEXT_TYPE_NUMBER); wxString spectrum_quality_choices[] = { _("0 - Regular quality"), _("1 - Better quality"), _("2 - High quality"), _("3 - Insane quality") }; AddComboControl(audioAdvPage,audioAdvSizer1,_("Spectrum quality"),_T("Audio spectrum quality"),wxArrayString(4,spectrum_quality_choices)); + AddTextControl(audioAdvPage,audioAdvSizer1,_("Spectrum cache memory max (MB)"),_T("Audio spectrum memory max"),TEXT_TYPE_NUMBER); audioAdvSizer1->AddGrowableCol(0,1); // Main sizer diff --git a/aegisub/options.cpp b/aegisub/options.cpp index 2f13dc697..50971f30a 100644 --- a/aegisub/options.cpp +++ b/aegisub/options.cpp @@ -193,6 +193,7 @@ void OptionsManager::LoadDefaults(bool onlyDefaults) { // Technically these can do with just the spectrum object being re-created SetInt(_T("Audio Spectrum Cutoff"),0); SetInt(_T("Audio Spectrum Quality"),0); + SetInt(_T("Audio Spectrum Memory Max"),0); // megabytes // Automation // The path changes only take effect when a script is (re)loaded but Automatic should be good enough, it certainly doesn't warrart a restart