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