1 // Licensed GNU LGPL v3 or later: http://www.gnu.org/licenses/lgpl.html
2 
3 #include "sminstenccache.hh"
4 #include "smbinbuffer.hh"
5 #include "sminstencoder.hh"
6 #include "smmmapin.hh"
7 #include "smmemout.hh"
8 #include "smmain.hh"
9 #include "smleakdebugger.hh"
10 #include "config.h"
11 
12 #include <mutex>
13 #include <cinttypes>
14 #include <regex>
15 
16 #include <assert.h>
17 #include <unistd.h>
18 #include <glib/gstdio.h>
19 #include <utime.h>
20 
21 using namespace SpectMorph;
22 
23 using std::string;
24 using std::vector;
25 using std::map;
26 using std::regex;
27 using std::regex_search;
28 
29 static string
cache_filename(const string & filename)30 cache_filename (const string& filename)
31 {
32   return sm_get_user_dir (USER_DIR_CACHE) + "/" + filename;
33 }
34 
35 static LeakDebugger leak_debugger ("SpectMorph::InstEncCache::CacheData");
36 
CacheData()37 InstEncCache::CacheData::CacheData()
38 {
39   leak_debugger.add (this);
40 }
41 
~CacheData()42 InstEncCache::CacheData::~CacheData()
43 {
44   leak_debugger.del (this);
45 }
46 
InstEncCache()47 InstEncCache::InstEncCache() :
48   cache_file_re ("inst_enc_[0-9a-f]{8}_[0-9a-f]{8}_[0-9]+_[0-9a-f]{40}$")
49 {
50   delete_old_files();
51 }
52 
53 InstEncCache*
the()54 InstEncCache::the()
55 {
56   return Global::inst_enc_cache();
57 }
58 
59 void
cache_save_L(const string & key)60 InstEncCache::cache_save_L (const string& key)
61 {
62   const CacheData& cache_data = cache[key];
63 
64   BinBuffer buffer;
65 
66   buffer.write_start ("SpectMorphCache");
67   buffer.write_string (cache_data.version.c_str());
68   buffer.write_int (cache_data.data.size());
69   buffer.write_string (sha1_hash (&cache_data.data[0], cache_data.data.size()).c_str());
70   buffer.write_end();
71 
72   vector<string> files;
73   Error error = read_dir (sm_get_user_dir (USER_DIR_CACHE), files);
74   for (auto filename : files)
75     {
76       if (regex_search (filename, cache_file_re)) /* avoid unlink on something that we shouldn't delete */
77         {
78           if (filename.size() > key.size() && filename.compare (0, key.size(), key) == 0)
79             {
80               unlink (cache_filename (filename).c_str());
81             }
82         }
83     }
84 
85   string out_filename = cache_filename (key) + "_" + cache_data.version;
86   FILE *outf = fopen (out_filename.c_str(), "wb");
87   if (outf)
88     {
89       for (auto ch : buffer.to_string())
90         fputc (ch, outf);
91       fputc (0, outf);
92       const unsigned char *ptr = cache_data.data.data();
93       fwrite (ptr, 1, cache_data.data.size(), outf);
94       fclose (outf);
95     }
96 }
97 
98 static bool
ends_with(const string & value,const string & ending)99 ends_with (const string& value, const string& ending)
100 {
101   if (ending.size() > value.size())
102     return false;
103 
104   return std::equal (ending.rbegin(), ending.rend(), value.rbegin());
105 }
106 
107 void
cache_try_load_L(const string & cache_key,const string & need_version)108 InstEncCache::cache_try_load_L (const string& cache_key, const string& need_version)
109 {
110   GenericIn *in_file = nullptr;
111   string     abs_filename;
112 
113   vector<string> files;
114   Error error = read_dir (sm_get_user_dir (USER_DIR_CACHE), files);
115   for (auto filename : files)
116     {
117       if (regex_search (filename, cache_file_re))
118         {
119           if (ends_with (filename, need_version))
120             {
121               abs_filename = cache_filename (filename);
122               in_file = GenericIn::open (abs_filename);
123               if (in_file)
124                 break;
125             }
126         }
127     }
128 
129   if (!in_file)  // no cache entry
130     return;
131 
132   // read header (till zero char)
133   string header_str;
134   int ch;
135   while ((ch = in_file->get_byte()) > 0)
136     header_str += char (ch);
137 
138   BinBuffer buffer;
139   buffer.from_string (header_str);
140 
141   string type       = buffer.read_start_inplace();
142   string version    = buffer.read_string_inplace();
143   int    data_size  = buffer.read_int();
144   string data_hash  = buffer.read_string_inplace();
145 
146   if (version == need_version)
147     {
148       vector<unsigned char> data (data_size);
149       if (in_file->read (&data[0], data.size()) == data_size)
150         {
151           string load_data_hash = sha1_hash (&data[0], data.size());
152           if (load_data_hash == data_hash)
153             {
154               cache[cache_key].version = version;
155               cache[cache_key].data    = std::move (data);
156 
157               /* bump mtime on successful load; this information is used during
158                * InstEncCache::delete_old_files() to remove the oldest cache files
159                */
160               g_utime (abs_filename.c_str(), nullptr);
161             }
162         }
163     }
164   delete in_file;
165 }
166 
167 static string
mk_version(const string & wav_data_hash,int midi_note,int iclipstart,int iclipend,Instrument::EncoderConfig & cfg)168 mk_version (const string& wav_data_hash, int midi_note, int iclipstart, int iclipend, Instrument::EncoderConfig& cfg)
169 {
170   /* create one single string that lists all the dependencies for the cache entry;
171    * hash it to get a compact representation of the "version"
172    */
173   string depends;
174 
175   depends += wav_data_hash + "\n";
176   depends += string_printf ("%s\n", PACKAGE_VERSION);
177   depends += string_printf ("%d\n", midi_note);
178   depends += string_printf ("%d\n", iclipstart);
179   depends += string_printf ("%d\n", iclipend);
180   if (cfg.enabled)
181     {
182       for (auto entry : cfg.entries)
183         depends += entry.param + "=" + entry.value + "\n";
184     }
185 
186   return sha1_hash (depends);
187 }
188 
189 Audio *
encode(Group * group,const WavData & wav_data,const string & wav_data_hash,int midi_note,int iclipstart,int iclipend,Instrument::EncoderConfig & cfg,const std::function<bool ()> & kill_function)190 InstEncCache::encode (Group *group, const WavData& wav_data, const string& wav_data_hash, int midi_note, int iclipstart, int iclipend, Instrument::EncoderConfig& cfg,
191                       const std::function<bool()>& kill_function)
192 {
193   // if group is not specified we create a random group just for this one request
194   std::unique_ptr<Group> random_group;
195   if (!group)
196     {
197       random_group.reset (create_group());
198       group = random_group.get();
199     }
200 
201   string cache_key = string_printf ("inst_enc_%s_%d", group->id.c_str(), midi_note);
202   string version   = mk_version (wav_data_hash, midi_note, iclipstart, iclipend, cfg);
203 
204   // search disk cache and memory cache
205   Audio *audio = cache_lookup (cache_key, version);
206   if (audio)
207     return audio;
208 
209   /* clip sample */
210   vector<float> clipped_samples = wav_data.samples();
211 
212   /* sanity checks for clipping boundaries */
213   iclipend   = sm_bound<int> (0, iclipend,  clipped_samples.size());
214   iclipstart = sm_bound<int> (0, iclipstart, iclipend);
215 
216   /* do the clipping */
217   clipped_samples.erase (clipped_samples.begin() + iclipend, clipped_samples.end());
218   clipped_samples.erase (clipped_samples.begin(), clipped_samples.begin() + iclipstart);
219 
220   WavData wav_data_clipped (clipped_samples, 1, wav_data.mix_freq(), wav_data.bit_depth());
221 
222   InstEncoder enc;
223   audio = enc.encode (wav_data_clipped, midi_note, cfg, kill_function);
224   if (!audio)
225     return nullptr;
226 
227   cache_add (cache_key, version, audio);
228 
229   return audio;
230 }
231 
232 Audio *
cache_lookup(const string & cache_key,const string & version)233 InstEncCache::cache_lookup (const string& cache_key, const string& version)
234 {
235   std::lock_guard<std::mutex> lg (cache_mutex);
236   if (cache[cache_key].version != version)
237     {
238       cache_try_load_L (cache_key, version);
239     }
240   if (cache[cache_key].version == version) // cache hit (in memory)
241     {
242       vector<unsigned char>& data = cache[cache_key].data;
243       cache[cache_key].read_stamp = cache_read_stamp++;
244 
245       GenericIn *in = MMapIn::open_mem (&data[0], &data[data.size()]);
246       Audio     *audio = new Audio;
247       Error      error = audio->load (in);
248 
249       delete in;
250 
251       if (!error)
252         return audio;
253 
254       delete audio;
255     }
256   return nullptr;
257 }
258 
259 void
cache_add(const string & cache_key,const string & version,const Audio * audio)260 InstEncCache::cache_add (const string& cache_key, const string& version, const Audio *audio)
261 {
262   vector<unsigned char> data;
263   MemOut                audio_mem_out (&data);
264 
265   audio->save (&audio_mem_out);
266 
267   // LOCK cache: store entry
268   std::lock_guard<std::mutex> lg (cache_mutex);
269 
270   cache[cache_key].version    = version;
271   cache[cache_key].data       = data;
272   cache[cache_key].read_stamp = cache_read_stamp++;
273 
274   cache_save_L (cache_key);
275 
276   /* enforce size limits and expire cache data from time to time */
277   if ((cache_read_stamp % 10) == 0)
278     {
279       delete_old_files();
280       delete_old_memory_L();
281     }
282 }
283 
284 void
clear()285 InstEncCache::clear()
286 {
287   std::lock_guard<std::mutex> lg (cache_mutex);
288 
289   cache.clear();
290 }
291 
292 InstEncCache::Group *
create_group()293 InstEncCache::create_group()
294 {
295   Group *g = new Group();
296 
297   g->id = string_printf ("%08x_%08x", g_random_int(), g_random_int());
298   return g;
299 }
300 
301 void
delete_old_files()302 InstEncCache::delete_old_files()
303 {
304   struct Status
305   {
306     string abs_filename;
307     uint64 mtime = 0;
308     size_t size = 0;
309   };
310   vector<string> files;
311   vector<Status> file_status;
312 
313   Error error = read_dir (sm_get_user_dir (USER_DIR_CACHE), files);
314   if (error)
315     return;
316 
317   for (auto filename : files)
318     {
319       Status status;
320       status.abs_filename = cache_filename (filename);
321       GStatBuf stbuf;
322       if (g_stat (status.abs_filename.c_str(), &stbuf) == 0)
323         {
324           status.mtime = stbuf.st_mtime;
325           status.size  = stbuf.st_size;;
326           file_status.push_back (status);
327         }
328     }
329   std::sort (file_status.begin(), file_status.end(),
330     [](const Status& st1, const Status& st2)
331       {
332         /* sort: start with newest entries */
333         return st1.mtime > st2.mtime;
334       });
335 
336   const size_t max_total_size = 100 * 1000 * 1000; // 100 MB total cache size
337   size_t total_size = 0;
338   for (auto status : file_status)
339     {
340       /* using a regexp here avoids deleting unrelated files; even if something is
341        * misconfigured this should make calling unlink() relatively safe */
342       if (regex_search (status.abs_filename, cache_file_re))
343         {
344           total_size += status.size;
345           if (total_size > max_total_size)
346             unlink (status.abs_filename.c_str());
347           // printf ("%s %" PRIu64 " %zd %zd\n", status.abs_filename.c_str(), status.mtime, status.size, total_size);
348         }
349     }
350 }
351 
352 void
delete_old_memory_L()353 InstEncCache::delete_old_memory_L()
354 {
355   struct Status
356   {
357     string key;
358     uint64 read_stamp;
359     size_t size;
360   };
361   vector<Status> mem_status;
362 
363   for (auto& entry : cache)
364     {
365       const string&     key = entry.first;
366       const CacheData&  cache_data = entry.second;
367 
368       Status status;
369       status.key        = key;
370       status.size       = cache_data.data.size();
371       status.read_stamp = cache_data.read_stamp;
372 
373       mem_status.push_back (status);
374     }
375   std::sort (mem_status.begin(), mem_status.end(),
376     [](const Status& st1, const Status& st2)
377       {
378         /* sort: start with newest entries */
379         return st1.read_stamp > st2.read_stamp;
380       });
381 
382   const size_t max_total_size = 50 * 1000 * 1000; // 50 MB total cache size
383   size_t total_size = 0;
384   for (const auto& status : mem_status)
385     {
386       total_size += status.size;
387       if (total_size > max_total_size)
388         {
389           /* since there is also disk cache, deletion from memory cache
390            * will not affect performance much, as long as it can be reloaded
391            * from the files we have stored
392            */
393           cache.erase (status.key);
394         }
395       // printf ("%s %" PRIu64 " %zd %zd\n", status.key.c_str(), status.read_stamp, status.size, total_size);
396     }
397 }
398