SaveOSC.cpp (20223B)
1 #include <cassert> 2 #include <dirent.h> 3 #include <thread> 4 #include <mutex> 5 #include <iostream> 6 #include <ctime> 7 #include <unistd.h> 8 #include <rtosc/thread-link.h> 9 #include <rtosc/rtosc-time.h> 10 11 #ifndef ZYN_GIT_WORKTREE 12 #include <git2.h> 13 #endif 14 15 #include "../Misc/Master.h" 16 #include "../Misc/MiddleWare.h" 17 #include "../UI/NSM.H" 18 19 // for linking purposes only: 20 NSM_Client *nsm = 0; 21 zyn::MiddleWare *middleware = 0; 22 23 char *instance_name=(char*)""; 24 25 // TODO: Check if rNoWalk is really needed 26 27 class SaveOSCTest 28 { 29 30 void _masterChangedCallback(zyn::Master* m) 31 { 32 /* 33 Note: This message will appear 4 times: 34 * Once at startup (changing from nil) 35 * Once after the loading 36 * Twice for the temporary exchange during saving 37 */ 38 #ifdef SAVE_OSC_DEBUG 39 printf("Changing master from %p (%p) to %p...\n", master, &master, m); 40 #endif 41 master = m; 42 master->setMasterChangedCallback(__masterChangedCallback, this); 43 master->setUnknownAddressCallback(__unknownAddressCallback, this); 44 } 45 46 // TODO: eliminate static callbacks 47 static void __masterChangedCallback(void* ptr, zyn::Master* m) 48 { 49 ((SaveOSCTest*)ptr)->_masterChangedCallback(m); 50 } 51 52 std::atomic<unsigned> unknown_addresses_count = { 0 }; 53 54 void _unknownAddressCallback(bool, rtosc::msg_t) 55 { 56 ++unknown_addresses_count; 57 } 58 59 static void __unknownAddressCallback(void* ptr, bool offline, rtosc::msg_t msg) 60 { 61 ((SaveOSCTest*)ptr)->_unknownAddressCallback(offline, msg); 62 } 63 64 void setUp() { 65 // this might be set to true in the future 66 // when saving will work better 67 config.cfg.SaveFullXml = false; 68 69 synth = new zyn::SYNTH_T; 70 synth->buffersize = 256; 71 synth->samplerate = 48000; 72 synth->alias(); 73 74 mw = new zyn::MiddleWare(std::move(*synth), &config); 75 mw->setUiCallback(0, _uiCallback, this); 76 _masterChangedCallback(mw->spawnMaster()); 77 realtime = nullptr; 78 } 79 80 void tearDown() { 81 #ifdef SAVE_OSC_DEBUG 82 printf("Master at the end: %p\n", master); 83 #endif 84 delete mw; 85 delete synth; 86 } 87 88 struct { 89 std::string operation; 90 std::string file; 91 uint64_t stamp; 92 bool status; 93 std::string savefile_content; 94 int msgnext = 0, msgmax = -1; 95 } recent; 96 std::mutex cb_mutex; 97 using mutex_guard = std::lock_guard<std::mutex>; 98 99 bool timeOutOperation(const char* osc_path, const char* arg1, int tries, int prependArg = -1) 100 { 101 clock_t begin = clock(); // just for statistics 102 103 bool ok = false; 104 rtosc_arg_val_t start_time; 105 rtosc_arg_val_current_time(&start_time); 106 107 if(prependArg == -1) 108 { 109 mw->transmitMsgGui(0, osc_path, "stT", arg1, start_time.val.t); 110 } else { 111 mw->transmitMsgGui(0, osc_path, "istT", prependArg, arg1, start_time.val.t); 112 } 113 114 int attempt; 115 for(attempt = 0; attempt < tries; ++attempt) 116 { 117 mutex_guard guard(cb_mutex); 118 if(recent.stamp == start_time.val.t && 119 recent.operation == osc_path && 120 recent.file == arg1) 121 { 122 ok = recent.status; 123 break; 124 } 125 usleep(1000); 126 } 127 128 // statistics: 129 clock_t end = clock(); 130 double elapsed_secs = double(end - begin) / CLOCKS_PER_SEC; 131 132 fprintf(stderr, "Action %s finished after %lf ms,\n" 133 " with a timeout of <%d ms (%s)\n", 134 osc_path, elapsed_secs, 135 attempt+1, 136 attempt == tries ? "timeout" 137 : ok ? "success" : "failure"); 138 139 return ok && (attempt != tries); 140 } 141 142 void uiCallback(const char* msg) 143 { 144 if(!strcmp(msg, "/save_osc") || !strcmp(msg, "/load_xmz") || !strcmp(msg, "/load_xiz")) 145 { 146 mutex_guard guard(cb_mutex); 147 #ifdef SAVE_OSC_DEBUG 148 fprintf(stderr, "Received message \"%s\".\n", msg); 149 #endif 150 int args = rtosc_narguments(msg); 151 152 if(args == 3) 153 { 154 assert( !strcmp(rtosc_argument_string(msg), "stT") 155 || !strcmp(rtosc_argument_string(msg), "stF")); 156 recent.operation = msg; 157 recent.file = rtosc_argument(msg, 0).s; 158 recent.stamp = rtosc_argument(msg, 1).t; 159 recent.status = rtosc_argument(msg, 2).T; 160 recent.savefile_content.clear(); 161 recent.msgnext = 0; 162 recent.msgmax = -1; 163 } 164 else 165 { 166 assert(!strcmp(rtosc_argument_string(msg), "stiis")); 167 assert(rtosc_argument(msg, 0).s == recent.file); 168 assert(rtosc_argument(msg, 1).t == recent.stamp); 169 if(recent.msgmax == -1) 170 recent.msgmax = rtosc_argument(msg, 3).i; 171 else 172 assert(recent.msgmax == rtosc_argument(msg, 3).i); 173 assert(recent.msgnext == rtosc_argument(msg, 2).i); 174 recent.savefile_content += rtosc_argument(msg, 4).s; 175 ++recent.msgnext; 176 } 177 } 178 else if(!strcmp(msg, "/damage")) 179 { 180 // (ignore) 181 } 182 else 183 { 184 // parameter update triggers message to UI 185 // (ignore) 186 } 187 } 188 189 void wait_for_message() 190 { 191 int attempt; 192 for(attempt = 0; attempt < 1000; ++attempt) 193 { 194 mutex_guard guard(cb_mutex); 195 if((recent.msgmax != -1) && 196 recent.msgnext > recent.msgmax) 197 { 198 break; 199 } 200 usleep(1000); 201 } 202 assert(attempt < 1000); 203 } 204 205 void dump_savefile(int res) 206 { 207 const std::string& savefile = recent.savefile_content; 208 std::cout << "Saving " 209 << (res == EXIT_SUCCESS ? "successful" : "failed") 210 << "." << std::endl; 211 std::cout << "The savefile content follows" << std::endl; 212 std::cout << "----8<----" << std::endl; 213 std::cout << savefile << std::endl; 214 std::cout << "---->8----" << std::endl; 215 } 216 217 public: 218 SaveOSCTest() { setUp(); } 219 ~SaveOSCTest() { tearDown(); } 220 221 static void _uiCallback(void* ptr, const char* msg) 222 { 223 ((SaveOSCTest*)ptr)->uiCallback(msg); 224 } 225 226 int test_files(const std::vector<std::string>& filenames) 227 { 228 int rval_total = EXIT_SUCCESS; 229 for(std::size_t idx = 0; idx < filenames.size() && rval_total == EXIT_SUCCESS; ++idx) 230 { 231 const std::string& filename = filenames[idx]; 232 assert(mw); 233 int rval; 234 235 bool load_ok; 236 if (strstr(filename.c_str(), ".xiz") == 237 filename.c_str() + filename.length() - 4) 238 { 239 mw->transmitMsgGui(0, "/reset_master", ""); 240 fprintf(stderr, "Loading XIZ file %s...\n", filename.c_str()); 241 load_ok = timeOutOperation("/load_xiz", filename.c_str(), 1000, 0); 242 } 243 else { 244 fprintf(stderr, "Loading XMZ file %s...\n", filename.c_str()); 245 load_ok = timeOutOperation("/load_xmz", filename.c_str(), 1000); 246 } 247 mw->discardAllbToUButHandleFree(); 248 249 if(load_ok) 250 { 251 fputs("Saving OSC file now...\n", stderr); 252 // There is actually no need to wait for /save_osc, since 253 // we're in the "UI" thread which does the saving itself, 254 // but this gives an example how it works with remote front-ends 255 // The filename '""' will write the savefile to stdout 256 rval = timeOutOperation("/save_osc", "", 1000) 257 ? EXIT_SUCCESS 258 : EXIT_FAILURE; 259 wait_for_message(); 260 mw->discardAllbToUButHandleFree(); 261 dump_savefile(rval); 262 } 263 else 264 { 265 std::cerr << "ERROR: Could not load XML file " 266 << filename << "." << std::endl; 267 rval = EXIT_FAILURE; 268 } 269 270 if(rval == EXIT_FAILURE) 271 rval_total = EXIT_FAILURE; 272 } 273 274 return rval_total; 275 } 276 277 278 // enable everything, cycle through all presets 279 // => only preset ports and enable ports are saved 280 // => all other ports are defaults and thus not saved 281 int test_presets() 282 { 283 // enable almost everything, in order to test all ports 284 mw->transmitMsgGui(0, "/part0/kit0/adpars/VoicePar0/PFilterEnabled", "T"); 285 mw->transmitMsgGui(0, "/part0/kit0/adpars/VoicePar0/PFreqEnvelopeEnabled", "T"); 286 mw->transmitMsgGui(0, "/part0/kit0/adpars/VoicePar0/PFreqLfoEnabled", "T"); 287 mw->transmitMsgGui(0, "/part0/kit0/adpars/VoicePar0/PAAEnabled", "T"); 288 mw->transmitMsgGui(0, "/part0/kit0/adpars/VoicePar0/PAmpEnvelopeEnabled", "T"); 289 mw->transmitMsgGui(0, "/part0/kit0/adpars/VoicePar0/PAmpLfoEnabled", "T"); 290 mw->transmitMsgGui(0, "/part0/kit0/adpars/VoicePar0/PFilterEnabled", "T"); 291 mw->transmitMsgGui(0, "/part0/kit0/adpars/VoicePar0/PFilterEnvelopeEnabled", "T"); 292 mw->transmitMsgGui(0, "/part0/kit0/adpars/VoicePar0/PFilterLfoEnabled", "T"); 293 mw->transmitMsgGui(0, "/part0/kit0/adpars/VoicePar0/PFMEnabled", "i", 1); 294 mw->transmitMsgGui(0, "/part0/kit0/adpars/VoicePar0/PFMFreqEnvelopeEnabled", "T"); 295 mw->transmitMsgGui(0, "/part0/kit0/adpars/VoicePar0/PFMAmpEnvelopeEnabled", "T"); 296 mw->transmitMsgGui(0, "/part0/kit0/Psubenabled", "T"); 297 mw->transmitMsgGui(0, "/part0/kit0/subpars/PBandWidthEnvelopeEnabled", "T"); 298 mw->transmitMsgGui(0, "/part0/kit0/subpars/PFreqEnvelopeEnabled", "T"); 299 mw->transmitMsgGui(0, "/part0/kit0/subpars/PGlobalFilterEnabled", "T"); 300 mw->transmitMsgGui(0, "/part0/kit0/Ppadenabled", "T"); 301 302 // use all effects as ins fx 303 mw->transmitMsgGui(0, "/insefx0/efftype", "S", "Reverb"); 304 mw->transmitMsgGui(0, "/insefx1/efftype", "S", "Phaser"); 305 mw->transmitMsgGui(0, "/insefx2/efftype", "S", "Echo"); 306 mw->transmitMsgGui(0, "/insefx3/efftype", "S", "Distortion"); 307 mw->transmitMsgGui(0, "/insefx4/efftype", "S", "Sympathetic"); 308 mw->transmitMsgGui(0, "/insefx5/efftype", "S", "DynFilter"); 309 mw->transmitMsgGui(0, "/insefx6/efftype", "S", "Alienwah"); 310 mw->transmitMsgGui(0, "/insefx7/efftype", "S", "EQ"); 311 mw->transmitMsgGui(0, "/part0/partefx0/efftype", "S", "Chorus"); 312 mw->transmitMsgGui(0, "/part0/partefx1/efftype", "S", "Reverse"); 313 // use all effects as sys fx (except Chorus, it does not differ) 314 mw->transmitMsgGui(0, "/sysefx0/efftype", "S", "Reverb"); 315 mw->transmitMsgGui(0, "/sysefx1/efftype", "S", "Phaser"); 316 mw->transmitMsgGui(0, "/sysefx2/efftype", "S", "Echo"); 317 mw->transmitMsgGui(0, "/sysefx3/efftype", "S", "Distortion"); 318 319 int res = EXIT_SUCCESS; 320 321 for (int preset = 0; preset < 18 && res == EXIT_SUCCESS; ++preset) 322 { 323 std::cout << "testing preset " << preset << std::endl; 324 325 // save all insefx with preset "preset" 326 char insefxstr[] = "/insefx0/preset"; 327 char partefxstr[] = "/part0/partefx0/preset"; 328 char sysefxstr[] = "/sysefx0/preset"; 329 int npresets_ins[] = {13, 12, 9, 6, 5, 5, 4, 2}; 330 int npresets_part[] = {12, 3}; 331 for(; insefxstr[7] < '0' + (char)(sizeof(npresets_ins) / sizeof(npresets_ins[0])); ++insefxstr[7]) 332 if(preset < npresets_ins[insefxstr[7]-'0']) 333 mw->transmitMsgGui(0, insefxstr, "i", preset); // "/insefx?/preset ?" 334 for(; partefxstr[14] < '0' + (char)(sizeof(npresets_part) / sizeof(npresets_part[0])); ++partefxstr[14]) 335 if(preset < npresets_part[partefxstr[14]-'0']) 336 mw->transmitMsgGui(0, partefxstr, "i", preset); // "/part0/partefx?/preset ?" 337 if(preset == 13) // for presets 13-17, test the up to 5 sysefx 338 { 339 mw->transmitMsgGui(0, "/sysefx0/efftype", "S", "Sympathetic"); 340 mw->transmitMsgGui(0, "/sysefx1/efftype", "S", "DynFilter"); 341 mw->transmitMsgGui(0, "/sysefx2/efftype", "S", "Alienwah"); 342 mw->transmitMsgGui(0, "/sysefx3/efftype", "S", "EQ"); 343 } 344 int type_offset = (preset>=13)?4:0; 345 for(; sysefxstr[7] < '4'; ++sysefxstr[7]) 346 if(preset%13 < npresets_ins[sysefxstr[7]-'0'+type_offset]) 347 mw->transmitMsgGui(0, sysefxstr, "i", preset%13); // "/sysefx?/preset ?" 348 349 char filename[] = "file0"; 350 filename[4] += preset; 351 res = timeOutOperation("/save_osc", filename, 1000) 352 ? res 353 : EXIT_FAILURE; 354 355 356 wait_for_message(); 357 dump_savefile(res); 358 359 const std::string& savefile = recent.savefile_content; 360 const char* next_line; 361 for(const char* line = savefile.c_str(); 362 *line && res == EXIT_SUCCESS; 363 line = next_line) 364 { 365 next_line = strchr(line, '\n'); 366 if (next_line) { ++next_line; } // first char of new line 367 else { next_line = line + strlen(line); } // \0 terminator 368 369 auto line_contains = [](const char* line, const char* next_line, const char* what) { 370 auto pos = strstr(line, what); 371 return (pos && pos < next_line); 372 }; 373 374 if(// empty line / comment 375 line[0] == '\n' || line[0] == '%' || 376 // accept [Ee]nabled, presets, and effect types, 377 // because we set them ourselves 378 line_contains(line, next_line, "nabled") || 379 line_contains(line, next_line, "/preset") || 380 line_contains(line, next_line, "/efftype") || 381 // formants have random values: 382 line_contains(line, next_line, "/Pformants") 383 ) 384 { 385 // OK 386 } else { 387 // everything else will not be OK, because this means 388 // a derivation from the default value, while we only 389 // used default values 390 // => that would mean an rDefault/rPreset does not match 391 // what the oject really uses as default value 392 std::string bad_line(line, next_line-1); 393 std::cout << "Error: invalid rDefault/rPreset: " 394 << bad_line 395 << std::endl; 396 res = EXIT_FAILURE; 397 } 398 } 399 400 if(unknown_addresses_count) 401 { 402 std::cout << "Error: master caught unknown addresses" 403 << std::endl; 404 res = EXIT_FAILURE; 405 } 406 } 407 return res; 408 } 409 410 411 void start_realtime(void) 412 { 413 do_exit = false; 414 415 realtime = new std::thread([this](){ 416 while(!do_exit) 417 { 418 if(!master->uToB->hasNext()) { 419 if(do_exit) 420 break; 421 422 usleep(500); 423 continue; 424 } 425 const char *msg = master->uToB->read(); 426 #ifdef SAVE_OSC_DEBUG 427 printf("Master %p: handling <%s>\n", master, msg); 428 #endif 429 master->applyOscEvent(msg, false); 430 }}); 431 } 432 433 void stop_realtime(void) 434 { 435 do_exit = true; 436 437 realtime->join(); 438 delete realtime; 439 realtime = NULL; 440 } 441 442 static void findfiles(std::string dirname, std::vector<std::string>& all_files) 443 { 444 if(dirname.back() != '/' && dirname.back() != '\\') 445 dirname += '/'; 446 447 DIR *dir = opendir(dirname.c_str()); 448 if(dir == NULL) 449 return; 450 451 struct dirent *fn; 452 while((fn = readdir(dir))) { 453 const char *filename = fn->d_name; 454 455 if(fn->d_type == DT_DIR) { 456 if(strcmp(filename, ".") && strcmp(filename, "..")) 457 findfiles(dirname + filename, all_files); 458 } 459 else 460 { 461 std::size_t len = strlen(filename); 462 //check for extension 463 if( (strstr(filename, ".xiz") == filename + len - 4) 464 || (strstr(filename, ".xmz") == filename + len - 4)) 465 { 466 all_files.push_back(dirname + filename); 467 } 468 } 469 } 470 471 closedir(dir); 472 } 473 474 #ifndef ZYN_GIT_WORKTREE 475 static std::string get_git_worktree() 476 { 477 git_libgit2_init(); 478 479 git_buf root_path = {0, 0, 0}; 480 git_repository_discover(&root_path, ".", 0, NULL); 481 482 git_repository *repo = NULL; 483 git_repository_open(&repo, root_path.ptr); 484 485 std::string toplevel = git_repository_workdir(repo); 486 487 git_repository_free(repo); 488 git_buf_free(&root_path); 489 git_libgit2_shutdown(); 490 491 return toplevel; 492 } 493 #endif 494 495 private: 496 zyn::Config config; 497 zyn::SYNTH_T* synth; 498 zyn::Master* master = NULL; 499 zyn::MiddleWare* mw; 500 std::thread* realtime; 501 bool do_exit; 502 }; 503 504 int main(int argc, char** argv) 505 { 506 SaveOSCTest test; 507 test.start_realtime(); 508 509 std::vector<std::string> all_files; 510 if(argc == 1) 511 { 512 // leave vector empty - test presets 513 } else if(!strcmp(argv[1], "test-all")) { 514 std::string zyn_git_worktree; 515 #ifdef ZYN_GIT_WORKTREE 516 zyn_git_worktree = ZYN_GIT_WORKTREE; 517 #else 518 zyn_git_worktree = SaveOSCTest::get_git_worktree(); 519 #endif 520 SaveOSCTest::findfiles(zyn_git_worktree, all_files); 521 } else { 522 all_files.reserve(argc-1); 523 for(int idx = 1; idx < argc; ++idx) 524 all_files.push_back(argv[idx]); 525 } 526 527 528 int res = all_files.size() == 0 ? test.test_presets() 529 : test.test_files(all_files); 530 test.stop_realtime(); 531 std::cerr << "Summary: " << ((res == EXIT_SUCCESS) ? "SUCCESS" : "FAILURE") 532 << std::endl; 533 return res; 534 }