zynaddsubfx

ZynAddSubFX open source synthesizer
Log | Files | Refs | Submodules | LICENSE

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 }