• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ossia / score / 31975605394

16 Aug 2026 10:10PM UTC coverage: 20.35%. First build
31975605394

Pull #2211

github

web-flow
Merge 1defa584c into bb5e533af
Pull Request #2211: project: collect, relink, trim and archive a project's files

1182 of 1996 new or added lines in 43 files covered. (59.22%)

43464 of 213584 relevant lines covered (20.35%)

5552.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

19.72
/src/plugins/score-plugin-pd/Pd/PdProcess.cpp
1
#include "PdProcess.hpp"
2

3
#include <Process/Dataflow/Port.hpp>
4
#include <Process/Dataflow/PortFactory.hpp>
5
#include <Process/Dataflow/WidgetInlets.hpp>
6
#include <Process/ExecutionTransaction.hpp>
7
#include <Process/ExternalFiles.hpp>
8
#include <Process/PresetHelpers.hpp>
9

10
#include <Pd/Commands/EditPd.hpp>
11

12
#include <Audio/Settings/Model.hpp>
13

14
#include <score/application/GUIApplicationContext.hpp>
15
#include <score/serialization/DataStreamVisitor.hpp>
16
#include <score/serialization/JSONValueVisitor.hpp>
17
#include <score/serialization/JSONVisitor.hpp>
18
#include <score/tools/DeleteAll.hpp>
19
#include <score/tools/File.hpp>
20

21
#include <ossia/detail/small_flat_map.hpp>
22
#include <ossia/network/base/parameter_data.hpp>
23
#include <ossia/network/common/complex_type.hpp>
24
#include <ossia/network/dataspace/dataspace_visitors.hpp>
25

26
#include <QDir>
27
#include <QFile>
28
#include <QProcess>
29
#include <QRegularExpression>
30
#include <QSettings>
31

32
#include <wobjectimpl.h>
33
#include <z_libpd.h>
34
W_OBJECT_IMPL(Pd::ProcessModel)
42✔
35
namespace Pd
36
{
37
Instance::Instance()
4✔
38
{
39
  instance = libpd_new_instance();
4✔
40
}
41

42
Instance::~Instance()
4✔
43
{
44
  libpd_free_instance(instance);
4✔
45
}
4✔
46

47
static const auto& initTypeMap()
×
48
{
49
  static ossia::flat_map<QString, ossia::val_type> widgetTypeMap{
×
50
      {"floatslider", ossia::val_type::FLOAT},
×
51
      {"logfloatslider", ossia::val_type::FLOAT},
×
52
      {"intslider", ossia::val_type::INT},
×
53
      {"intspinbox", ossia::val_type::INT},
×
54
      {"toggle", ossia::val_type::BOOL},
×
55
      {"lineedit", ossia::val_type::STRING},
×
56
      {"combobox", ossia::val_type::STRING},
×
57
      {"enum", ossia::val_type::STRING},
×
58
      {"button", ossia::val_type::IMPULSE},
×
59
      {"hsvslider", ossia::val_type::VEC4F},
×
60
      {"xyslider", ossia::val_type::VEC2F},
×
61
      {"multislider", ossia::val_type::LIST},
×
62
      {"colorchooser", ossia::val_type::VEC4F},
×
63
      {"text", ossia::val_type::STRING},
×
64
      {"checkbox", ossia::val_type::BOOL},
×
65
      {"bang", ossia::val_type::IMPULSE},
×
66
      {"pulse", ossia::val_type::IMPULSE},
×
67
      {"impulse", ossia::val_type::IMPULSE},
×
68
  };
69

70
  return widgetTypeMap;
×
71
}
×
72
enum pd_thing_to_parse
73
{
74
  unknown,
75
  type,
76
  range,
77
  min,
78
  max,
79
  unit,
80
  widget,
81
  defaultv
82
};
83

84
static void parseControlTypeAttributes(PatchSpec::Control& ctl, const QStringList& args)
×
85
{
86
  pd_thing_to_parse next_is{};
×
87
  // First look for the type
88
  for(int i = 1; i < args.size(); i++)
×
89
  {
90
    switch(next_is)
×
91
    {
×
92
      default:
93
      case unknown:
94
        if(args[i] == "@type")
×
95
        {
96
          next_is = type;
×
97
          break;
×
98
        }
99
        if(args[i] == "@unit")
×
100
        {
101
          next_is = unit;
×
102
          break;
×
103
        }
104
        if(args[i] == "@widget")
×
105
        {
106
          next_is = widget;
×
107
          break;
×
108
        }
109
        break;
×
110
      case type:
111
        ctl.type = args[i];
×
112
        next_is = unknown;
×
113
        break;
×
114
      case unit:
115
        ctl.unit = args[i];
×
116
        next_is = unknown;
×
117
        break;
×
118
      case widget:
119
        ctl.widget = args[i];
×
120
        next_is = unknown;
×
121
        break;
×
122
    }
123
  }
×
124

125
  static const auto& widgetFuncMap = initTypeMap();
×
126
  if(auto it = widgetFuncMap.find(ctl.widget); it != widgetFuncMap.end())
×
127
  {
128
    ctl.deduced_type = it->second;
×
129
  }
×
130
  else
131
  {
132
    if(auto param = ossia::default_parameter_for_type(ctl.unit.toStdString()))
×
133
      ctl.deduced_type = ossia::underlying_type(param->type);
×
134
    else if(auto param = ossia::default_parameter_for_type(ctl.type.toStdString()))
×
135
      ctl.deduced_type = ossia::underlying_type(param->type);
×
136
    else
137
      ctl.deduced_type = ossia::val_type::FLOAT;
×
138
  }
139
}
×
140

141
static std::optional<ossia::value>
142
parseControlValue(PatchSpec::Control& ctl, const QStringList& args, int& i)
143
{
144
  switch(*ctl.deduced_type)
145
  {
146
    case ossia::val_type::NONE:
147
      return std::nullopt;
148
    case ossia::val_type::BOOL: {
149
      auto str = args[i].toLower();
150
      return bool(str.startsWith('t') || str.startsWith('y') || str == "1");
151
    }
152
    case ossia::val_type::IMPULSE:
153
      return ossia::impulse{};
154
    case ossia::val_type::FLOAT: {
155
      bool ok{true};
156
      double v = args[i].toDouble(&ok);
157
      if(ok)
158
        return float(v);
159
      break;
160
    }
161
    case ossia::val_type::INT: {
162
      bool ok{true};
163
      int v = args[i].toInt(&ok);
164
      if(ok)
165
        return int(v);
166
      break;
167
    }
168
    case ossia::val_type::VEC2F:
169
      SCORE_TODO;
170
      break;
171
    case ossia::val_type::VEC3F:
172
      SCORE_TODO;
173
      break;
174
    case ossia::val_type::VEC4F:
175
      SCORE_TODO;
176
      break;
177
    case ossia::val_type::LIST:
178
      SCORE_TODO;
179
      break;
180
    case ossia::val_type::MAP:
181
      SCORE_TODO;
182
      break;
183
    case ossia::val_type::STRING:
184
      return args[i].toStdString();
185
  }
186
  return std::nullopt;
187
}
188

189
static void parseControlDataRange(
×
190
    PatchSpec::Control& ctl, const QStringList& args, int& i,
191
    std::optional<ossia::value>& min_domain, std::optional<ossia::value>& max_domain,
192
    std::optional<ossia::domain>& domain)
193
{
194
  switch(*ctl.deduced_type)
×
195
  {
196
    case ossia::val_type::NONE:
197
    case ossia::val_type::BOOL:
198
    case ossia::val_type::IMPULSE:
199
      break;
×
200
    case ossia::val_type::FLOAT: {
201
      if(i < args.size() - 1)
×
202
      {
203
        bool ok{true};
×
204
        min_domain = args[i].toDouble(&ok);
×
205
        if(!ok)
×
206
          min_domain = std::nullopt;
×
207

208
        i++;
×
209
        ok = true;
×
210
        max_domain = args[i].toDouble(&ok);
×
211
        if(!ok)
×
212
          max_domain = std::nullopt;
×
213
      }
×
214
      break;
×
215
    }
216
    case ossia::val_type::INT: {
217
      if(i < args.size() - 1)
×
218
      {
219
        bool ok{true};
×
220
        min_domain = args[i].toInt(&ok);
×
221
        if(!ok)
×
222
          min_domain = std::nullopt;
×
223

224
        i++;
×
225
        ok = true;
×
226
        max_domain = args[i].toInt(&ok);
×
227
        if(!ok)
×
228
          max_domain = std::nullopt;
×
229
      }
×
230
      break;
×
231
    }
232
    case ossia::val_type::VEC2F:
233
      SCORE_TODO;
×
234
      break;
×
235
    case ossia::val_type::VEC3F:
236
      SCORE_TODO;
×
237
      break;
×
238
    case ossia::val_type::VEC4F:
239
      SCORE_TODO;
×
240
      break;
×
241
    case ossia::val_type::LIST:
242
      SCORE_TODO;
×
243
      break;
×
244
    case ossia::val_type::MAP:
245
      SCORE_TODO;
×
246
      break;
×
247
    case ossia::val_type::STRING: {
248
      std::vector<std::string> vec;
×
249
      while(i < args.size() && args[i][0] != '@')
×
250
      {
251
        vec.push_back(args[i].toStdString());
×
252
        i++;
×
253
      }
254
      if(!vec.empty())
×
255
      {
256
        domain = ossia::domain_base<std::string>{std::move(vec)};
×
257
      }
×
258
      break;
259
    }
×
260
  }
261
}
×
262

263
static void parseControlDataAttributes(PatchSpec::Control& ctl, const QStringList& args)
×
264
{
265
  pd_thing_to_parse next_is{};
×
266
  std::optional<ossia::value> min_domain{}, max_domain{};
×
267
  std::optional<ossia::domain> domain;
×
268
  for(int i = 1; i < args.size(); i++)
×
269
  {
270
    switch(next_is)
×
271
    {
×
272
      default:
273
      case unknown:
274
        if(args[i] == "@range")
×
275
        {
276
          next_is = range;
×
277
          break;
×
278
        }
279
        if(args[i] == "@min")
×
280
        {
281
          next_is = min;
×
282
          break;
×
283
        }
284
        if(args[i] == "@max")
×
285
        {
286
          next_is = max;
×
287
          break;
×
288
        }
289
        if(args[i] == "@default")
×
290
        {
291
          next_is = defaultv;
×
292
          break;
×
293
        }
294
        break;
×
295
      case range:
296
        parseControlDataRange(ctl, args, i, min_domain, max_domain, domain);
×
297
        next_is = unknown;
×
298
        break;
×
299
      case min:
300
        min_domain = args[i].toDouble();
×
301
        next_is = unknown;
×
302
        break;
×
303
      case max:
304
        max_domain = args[i].toDouble();
×
305
        next_is = unknown;
×
306
        break;
×
307
      case defaultv:
308
        ctl.defaultv = args[i].toFloat();
×
309
        next_is = unknown;
×
310
        break;
×
311
    }
312
  }
×
313

314
  if(domain)
×
315
    ctl.domain = std::move(*domain);
×
316
  else if(min_domain && max_domain)
×
317
    ctl.domain = ossia::make_domain(*min_domain, *max_domain);
×
318
}
×
319

320
static PatchSpec::Control parseControlSpec(QString var)
×
321
{
322
  var = var.replace("\n", " ");
×
323
  QStringList splitted = var.split(" ");
×
324
  splitted.removeAll(QString{});
×
325

326
  PatchSpec::Control ctl;
×
327
  ctl.name = splitted.front();
×
328
  ctl.remote = var;
×
329

330
  parseControlTypeAttributes(ctl, splitted);
×
331
  parseControlDataAttributes(ctl, splitted);
×
332

333
  return ctl;
×
334
}
×
335

336
static const auto& initFuncMap()
×
337
{
338
  using InletFunc = Process::
339
      Inlet* (*)(const PatchSpec::Control&, const Id<Process::Port>&, QObject*);
340
  static ossia::hash_map<QString, InletFunc> widgetFuncMap{
×
341
      {"floatslider",
×
342
       [](const PatchSpec::Control& ctl, const Id<Process::Port>& id,
×
343
          QObject* parent) -> Process::Inlet* {
344
         const auto [dom_min, dom_max] = ossia::get_float_minmax(ctl.domain);
×
345
         float min{dom_min ? *dom_min : 0.f};
×
346
         float max{dom_max ? *dom_max : 1.f};
×
347
         float init{ossia::convert<float>(ctl.defaultv)};
×
348
         return new Process::FloatSlider{min, max, init, ctl.name, id, parent};
×
349
       }},
×
350
      {"logfloatslider",
×
351
       [](const PatchSpec::Control& ctl, const Id<Process::Port>& id,
×
352
          QObject* parent) -> Process::Inlet* {
353
         const auto [dom_min, dom_max] = ossia::get_float_minmax(ctl.domain);
×
354
         float min{dom_min ? *dom_min : 0.f};
×
355
         float max{dom_max ? *dom_max : 1.f};
×
356
         float init{ossia::convert<float>(ctl.defaultv)};
×
357
         return new Process::LogFloatSlider{min, max, init, ctl.name, id, parent};
×
358
       }},
×
359
      {"intslider",
×
360
       [](const PatchSpec::Control& ctl, const Id<Process::Port>& id,
×
361
          QObject* parent) -> Process::Inlet* {
362
         const auto [dom_min, dom_max] = ossia::get_float_minmax(ctl.domain);
×
363
         int min{dom_min ? int(*dom_min) : 0};
×
364
         int max{dom_max ? int(*dom_max) : 127};
×
365
         int init{ossia::convert<int>(ctl.defaultv)};
×
366
         return new Process::IntSlider{min, max, init, ctl.name, id, parent};
×
367
       }},
×
368
      {"intspinbox",
×
369
       [](const PatchSpec::Control& ctl, const Id<Process::Port>& id,
×
370
          QObject* parent) -> Process::Inlet* {
371
         const auto [dom_min, dom_max] = ossia::get_float_minmax(ctl.domain);
×
372
         int min{dom_min ? int(*dom_min) : 0};
×
373
         int max{dom_max ? int(*dom_max) : 127};
×
374
         int init{ossia::convert<int>(ctl.defaultv)};
×
375
         return new Process::IntSpinBox{min, max, init, ctl.name, id, parent};
×
376
       }},
×
377
      {"toggle",
×
378
       [](const PatchSpec::Control& ctl, const Id<Process::Port>& id,
×
379
          QObject* parent) -> Process::
380
                               Inlet* {
381
    return new Process::Toggle{ossia::convert<bool>(ctl.defaultv), ctl.name, id, parent};
×
382
       }},
×
383
      {"lineedit",
×
384
       [](const PatchSpec::Control& ctl, const Id<Process::Port>& id,
×
385
          QObject* parent) -> Process::Inlet* {
386
         const std::string& init = ossia::convert<std::string>(ctl.defaultv);
×
387
         return new Process::LineEdit{
×
388
             QString::fromUtf8(init.c_str(), init.size()), ctl.name, id, parent};
×
389
       }},
×
390
      {"combobox",
×
391
       [](const PatchSpec::Control& ctl, const Id<Process::Port>& id,
×
392
          QObject* parent) -> Process::Inlet* {
393
         std::vector<std::string> choices;
×
394
         if(auto dom = ctl.domain.v.target<ossia::domain_base<std::string>>())
×
395
           choices = dom->values;
×
396
         std::string defaultv;
×
397
         if(auto v = ctl.defaultv.target<std::string>())
×
398
           defaultv = *v;
×
399

400
         return new Process::Enum{choices, {}, defaultv, ctl.name, id, parent};
×
401
       }},
×
402
      {"button",
×
403
       [](const PatchSpec::Control& ctl, const Id<Process::Port>& id,
×
404
          QObject* parent) -> Process::Inlet* {
405
         return new Process::Button{ctl.name, id, parent};
×
406
       }},
×
407
      {"hsvslider",
×
408
       [](const PatchSpec::Control& ctl, const Id<Process::Port>& id,
×
409
          QObject* parent) -> Process::Inlet* {
410
         return new Process::HSVSlider{ossia::vec4f{}, ctl.name, id, parent};
×
411
       }},
×
412
      {"xyslider",
×
413
       [](const PatchSpec::Control& ctl, const Id<Process::Port>& id,
×
414
          QObject* parent) -> Process::Inlet* {
415
         return new Process::XYSlider{ossia::vec2f{}, ctl.name, id, parent};
×
416
       }},
×
417
      {"multislider",
×
418
       [](const PatchSpec::Control& ctl, const Id<Process::Port>& id,
×
419
          QObject* parent) -> Process::
420
                               Inlet* {
421
    return new Process::MultiSlider{std::vector<ossia::value>{}, ctl.name, id, parent};
×
422
       }}};
×
423
  widgetFuncMap.reserve(widgetFuncMap.size() * 4);
×
424

425
  // Note: we cast to make a copy as otherwise this may be a reference..
426
  // but the left hand side may introduce
427
  // new values in the map and invalidate them
428
  widgetFuncMap["colorchooser"] = InletFunc(widgetFuncMap["hsvslider"]);
×
429
  widgetFuncMap["enum"] = InletFunc(widgetFuncMap["combobox"]);
×
430
  widgetFuncMap["text"] = InletFunc(widgetFuncMap["lineedit"]);
×
431
  widgetFuncMap["checkbox"] = InletFunc(widgetFuncMap["toggle"]);
×
432
  widgetFuncMap["bang"] = InletFunc(widgetFuncMap["button"]);
×
433
  widgetFuncMap["pulse"] = InletFunc(widgetFuncMap["button"]);
×
434
  widgetFuncMap["impulse"] = InletFunc(widgetFuncMap["button"]);
×
435
  return widgetFuncMap;
×
436
}
×
437
Process::Inlet* makeInletFromSpec(
×
438
    const PatchSpec::Control& ctl, const Id<Process::Port>& id, QObject* parent)
439
{
440
  static const auto& widgetFuncMap = initFuncMap();
×
441
  Process::Inlet* inl{};
×
442
  if(auto it = widgetFuncMap.find(ctl.widget); it != widgetFuncMap.end())
×
443
  {
444
    inl = it->second(ctl, id, parent);
×
445
  }
×
446
  else
447
  {
448
    auto param = ossia::default_parameter_for_type(ctl.unit.toStdString());
×
449
    if(!param)
×
450
      param = ossia::default_parameter_for_type(ctl.type.toStdString());
×
451
    if(param)
×
452
    {
453
      if(param->unit)
×
454
      {
455
        auto dataspace = ossia::get_dataspace_text(param->unit);
×
456
        if(dataspace == "color")
×
457
          inl = widgetFuncMap.at("hsvslider")(ctl, id, parent);
×
458
        else if(dataspace == "position")
×
459
          inl = widgetFuncMap.at("xyslider")(ctl, id, parent);
×
460
      }
×
461
      else
462
      {
463
        switch(ossia::underlying_type(param->type))
×
464
        {
465
          case ossia::val_type::FLOAT:
466
            inl = widgetFuncMap.at("floatslider")(ctl, id, parent);
×
467
            break;
×
468
          case ossia::val_type::INT:
469
            inl = widgetFuncMap.at("intslider")(ctl, id, parent);
×
470
            break;
×
471
          case ossia::val_type::BOOL:
472
            inl = widgetFuncMap.at("toggle")(ctl, id, parent);
×
473
            break;
×
474
          case ossia::val_type::IMPULSE:
475
            inl = widgetFuncMap.at("button")(ctl, id, parent);
×
476
            break;
×
477
          case ossia::val_type::VEC2F:
478
          case ossia::val_type::VEC3F:
479
          case ossia::val_type::VEC4F:
480
          case ossia::val_type::LIST:
481
            inl = widgetFuncMap.at("multislider")(ctl, id, parent);
×
482
            break;
×
483
          case ossia::val_type::STRING:
484
            inl = widgetFuncMap.at("lineedit")(ctl, id, parent);
×
485
            break;
×
486
          case ossia::val_type::MAP:
487
          case ossia::val_type::NONE:
488
            break;
×
489
        }
490
      }
491
    }
×
492
  }
493

494
  if(!inl)
×
495
  {
496
    inl = new Process::ValueInlet{ctl.name, id, parent};
×
497
  }
×
498
  return inl;
×
499
}
×
500
static bool checkIfBinaryIsInPath(const QString& binary)
1✔
501
{
502
#if !defined(_WIN32)
503
  QProcess findProcess;
1✔
504
  findProcess.start("which", {binary});
1✔
505
  findProcess.setReadChannel(QProcess::ProcessChannel::StandardOutput);
1✔
506

507
  if(!findProcess.waitForFinished())
1✔
508
    return {};
×
509

510
  QFileInfo check_file(findProcess.readAll().trimmed());
1✔
511
  return check_file.exists() && check_file.isFile();
1✔
512
#endif
513
  return false;
514
}
1✔
515

516
#if defined(_WIN32)
517
static QString readKeyFromRegistry(const QString& path, const QString& key)
518
{
519
  QSettings settings(path, QSettings::Registry64Format);
520
  return settings.value(key).toString();
521
}
522
#endif
523
const QString& locatePurrDataBinary() noexcept
×
524
{
525
  static const QString pdbinary = []() -> QString {
×
526
#if __APPLE__
527
    {
528
      const auto& applist = QDir{"/Applications"}.entryList();
529
      if(applist.contains("Pd-l2ork.app"))
530
      {
531
        QString pd_path = "/Applications/Pd-l2ork.app/Contents/MacOS/nwjs";
532
        if(QFile::exists(pd_path))
533
          return pd_path;
534
      }
535
    }
536
#endif
537

538
#if _WIN32
539
    if(QFile::exists("c:\\Program Files\\Purr Data\\bin\\pd.exe"))
540
      return "c:\\Program Files\\Purr Data\\bin\\pd.exe";
541
    else if(QFile::exists("c:\\Program Files (x86)\\Purr Data\\bin\\pd.exe"))
542
      return "c:\\Program Files (x86)\\Purr Data\\bin\\pd.exe";
543
#else
544
    if(QFile::exists("/usr/bin/purr-data"))
×
545
      return "/usr/bin/purr-data";
×
546
    else if(QFile::exists("/usr/local/bin/purr-data"))
×
547
      return "/usr/local/bin/purr-data";
×
548

549
#endif
550

551
    if(checkIfBinaryIsInPath("purr-data"))
×
552
      return "purr-data";
×
553

554
    return {};
×
555
  }();
×
556
  return pdbinary;
×
557
}
558

559
const QString& locatePdBinary() noexcept
1✔
560
{
561
  static const QString pdbinary = []() -> QString {
2✔
562
#if __APPLE__
563
    {
564
      QStringList applist = QDir{"/Applications"}.entryList();
565
      applist.sort();
566

567
      // First try to look for the exact Pd version used to build score
568
      auto this_pd_folder = QString("Pd-%1.%2-%3")
569
                                .arg(PD_MAJOR_VERSION)
570
                                .arg(PD_MINOR_VERSION)
571
                                .arg(PD_BUGFIX_VERSION);
572
      for(const auto& app : applist)
573
      {
574
        if(app == this_pd_folder)
575
        {
576
          QString pd_path = "/Applications/" + app + "/Contents/MacOS/Pd";
577
          if(QFile::exists(pd_path))
578
            return pd_path;
579
        }
580
      }
581

582
      // Then try other versions
583
      for(const auto& app : applist)
584
      {
585
        if(app.startsWith("Pd-"))
586
        {
587
          QString pd_path = "/Applications/" + app + "/Contents/MacOS/Pd";
588
          if(QFile::exists(pd_path))
589
            return pd_path;
590
        }
591
      }
592
    }
593
#endif
594

595
#if _WIN32
596

597
    if(QFile::exists("c:\\Program Files\\Pd\\bin\\pd.exe"))
598
      return "c:\\Program Files\\Pd\\bin\\pd.exe";
599
    else if(QFile::exists("c:\\Program Files (x86)\\Pd\\bin\\pd.exe"))
600
      return "c:\\Program Files (x86)\\Pd\\bin\\pd.exe";
601
    else if(QString k = readKeyFromRegistry(
602
                "HKEY_LOCAL_"
603
                "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\App "
604
                "Paths\\pd.exe",
605
                "64");
606
            !k.isEmpty())
607
      return k + "\\bin";
608
    else if(QString k = readKeyFromRegistry(
609
                "HKEY_LOCAL_"
610
                "MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\App "
611
                "Paths\\pd.exe",
612
                "32");
613
            !k.isEmpty())
614
      return k + "\\bin";
615
    else if(QString k = readKeyFromRegistry(
616
                "HKEY_CURRENT_"
617
                "USER\\Software\\Microsoft\\Windows\\CurrentVersion\\App "
618
                "Paths\\pd.exe",
619
                "64");
620
            !k.isEmpty())
621
      return k + "\\bin";
622
    else if(QString k = readKeyFromRegistry(
623
                "HKEY_CURRENT_"
624
                "USER\\Software\\Microsoft\\Windows\\CurrentVersion\\App "
625
                "Paths\\pd.exe",
626
                "32");
627
            !k.isEmpty())
628
      return k + "\\bin";
629
#else
630
    if(QFile::exists("/usr/bin/pd"))
1✔
631
      return "/usr/bin/pd";
×
632
    else if(QFile::exists("/usr/local/bin/pd"))
1✔
633
      return "/usr/local/bin/pd";
×
634
#endif
635

636
    if(checkIfBinaryIsInPath("pd"))
1✔
637
      return "pd";
×
638

639
    return {};
1✔
640
  }();
1✔
641
  return pdbinary;
1✔
642
}
643

644
ProcessModel::ProcessModel(
2✔
645
    const TimeVal& duration, const QString& pdpatch, const Id<Process::ProcessModel>& id,
646
    QObject* parent)
647
    : Process::ProcessModel{
2✔
648
        duration, id, Metadata<ObjectKey_k, ProcessModel>::get(), parent}
2✔
649
{
2✔
650
  metadata().setInstanceName(*this);
2✔
651
  init();
2✔
652
  (void)setScript(pdpatch);
2✔
653
}
×
654

655
bool ProcessModel::hasExternalUI() const noexcept
1✔
656
{
657
  const QString& pathToPd = locatePdBinary();
1✔
658
  return !pathToPd.isEmpty();
1✔
659
}
660

661
ProcessModel::~ProcessModel()
8✔
662
{
4✔
663
  libpd_set_instance(m_instance->instance);
4✔
664

665
  if(m_instance->ui_open)
4✔
666
  {
667
    m_instance->ui_open = false;
×
668
    libpd_stop_gui();
×
669
  }
×
670

671
  if(m_instance->file_handle)
4✔
672
  {
673
    libpd_closefile(m_instance->file_handle);
×
674
  }
×
675
}
8✔
676

677
int ProcessModel::audioInputs() const
2✔
678
{
679
  return m_audioInputs;
2✔
680
}
681

682
int ProcessModel::audioOutputs() const
2✔
683
{
684
  return m_audioOutputs;
2✔
685
}
686

687
bool ProcessModel::midiInput() const
2✔
688
{
689
  return m_midiInput;
2✔
690
}
691

692
bool ProcessModel::midiOutput() const
2✔
693
{
694
  return m_midiOutput;
2✔
695
}
696

697
void ProcessModel::setAudioInputs(int audioInputs)
×
698
{
699
  if(m_audioInputs == audioInputs)
×
700
    return;
×
701

702
  m_audioInputs = audioInputs;
×
703
  audioInputsChanged(m_audioInputs);
×
704
}
×
705

706
void ProcessModel::setAudioOutputs(int audioOutputs)
×
707
{
708
  if(m_audioOutputs == audioOutputs)
×
709
    return;
×
710

711
  m_audioOutputs = audioOutputs;
×
712
  audioOutputsChanged(m_audioOutputs);
×
713
}
×
714

715
void ProcessModel::setMidiInput(bool midiInput)
×
716
{
717
  if(m_midiInput == midiInput)
×
718
    return;
×
719

720
  m_midiInput = midiInput;
×
721
  midiInputChanged(m_midiInput);
×
722
}
×
723

724
void ProcessModel::setMidiOutput(bool midiOutput)
×
725
{
726
  if(m_midiOutput == midiOutput)
×
727
    return;
×
728

729
  m_midiOutput = midiOutput;
×
730
  midiOutputChanged(m_midiOutput);
×
731
}
×
732

733
void ProcessModel::init()
4✔
734
{
735
  m_instance = std::make_shared<Instance>();
4✔
736
}
4✔
737

738
static void add_pd_search_paths(const QString& folder)
×
739
{
740
  // Add the path of the patch folder to pd's search path
741
  libpd_add_to_search_path(folder.toUtf8().data());
×
742

743
  // Add Pd global search paths
744

745
  // Note: we use QString to make sure things do not disappear with AppImage's /usr clearing
746
  QSet<QString> paths;
×
747
  if(QDir f(QStringLiteral("/usr/lib64/puredata/extra")); f.exists())
×
748
    paths.insert(f.canonicalPath());
×
749
  if(QDir f(QStringLiteral("/usr/lib/puredata/extra")); f.exists())
×
750
    paths.insert(f.canonicalPath());
×
751
  if(QDir f(QStringLiteral("/usr/lib64/pd/extra")); f.exists())
×
752
    paths.insert(f.canonicalPath());
×
753
  if(QDir f(QStringLiteral("/usr/lib/pd/extra")); f.exists())
×
754
    paths.insert(f.canonicalPath());
×
755

756
  // home
757
  auto home = qgetenv("HOME");
×
758
  if(QDir f(home + QStringLiteral("/.local/lib/puredata/extra")); f.exists())
×
759
    paths.insert(f.canonicalPath());
×
760
  if(QDir f(home + QStringLiteral("/.local/lib/pd/extra")); f.exists())
×
761
    paths.insert(f.canonicalPath());
×
762

763
  // pd install path
764
  {
765
    auto pd_path = locatePdBinary();
×
766
    QFileInfo f(pd_path);
×
767
    QDir d = f.dir();
×
768

769
    if(d.cd("extra"))
×
770
    {
771
      paths.insert(d.canonicalPath());
×
772
    }
×
773
    else
774
    {
775
      if(d.cdUp())
×
776
      {
777
        if(d.cd("extra"))
×
778
        {
779
          paths.insert(d.canonicalPath());
×
780
        }
×
781
      }
×
782
    }
783
  }
×
784

785
  for(auto& path : paths)
×
786
    libpd_add_to_search_path(path.toStdString().c_str());
×
787
}
×
788

789
Process::ScriptChangeResult ProcessModel::setScript(const QString& script)
4✔
790
{
791
  Process::ScriptChangeResult res;
4✔
792
  m_script = score::locateFilePath(script, score::IDocument::documentContext(*this));
4✔
793
  QFile f(m_script);
4✔
794
  if(f.open(QIODevice::ReadOnly))
4✔
795
  {
796
    m_spec.receives.clear();
×
797
    m_spec.sends.clear();
×
798
    setMidiInput(false);
×
799
    setMidiOutput(false);
×
800

801
    res.inlets = score::clearAndDeleteLater(m_inlets);
×
802
    res.outlets = score::clearAndDeleteLater(m_outlets);
×
803

804
    int i = 0;
×
805
    auto get_next_id = [&] {
×
806
      i++;
×
807
      return Id<Process::Port>(i);
×
808
    };
809

810
    QString patch = score::readFileAsQString(f);
×
811
    {
812
      static const QRegularExpression adc_regex{"adc~"};
×
813
      auto m = adc_regex.match(patch);
×
814
      if(m.hasMatch())
×
815
      {
816
        auto p = new Process::AudioInlet{"Audio In", get_next_id(), this};
×
817
        setAudioInputs(2);
×
818
        m_inlets.push_back(p);
×
819
      }
×
820
    }
×
821

822
    {
823
      static const QRegularExpression dac_regex{"dac~"};
×
824
      auto m = dac_regex.match(patch);
×
825
      if(m.hasMatch())
×
826
      {
827
        auto p = new Process::AudioOutlet{"Audio Out", get_next_id(), this};
×
828
        p->setPropagate(true);
×
829
        setAudioOutputs(2);
×
830
        m_outlets.push_back(p);
×
831
      }
×
832
    }
×
833

834
    {
835
      static const QRegularExpression midi_regex{"(midiin|notein|ctlin)"};
×
836
      auto m = midi_regex.match(patch);
×
837
      if(m.hasMatch())
×
838
      {
839
        auto p = new Process::MidiInlet{"MIDI In", get_next_id(), this};
×
840
        m_inlets.push_back(p);
×
841

842
        setMidiInput(true);
×
843
      }
×
844
    }
×
845

846
    {
847
      static const QRegularExpression midi_regex{"(midiiout|noteout|ctlout)"};
×
848
      auto m = midi_regex.match(patch);
×
849
      if(m.hasMatch())
×
850
      {
851
        auto p = new Process::MidiOutlet{"MIDI Out", get_next_id(), this};
×
852
        m_outlets.push_back(p);
×
853

854
        setMidiOutput(true);
×
855
      }
×
856
    }
×
857

858
    {
859
      static const QRegularExpression recv_regex{
×
860
          R"_((r|receive)\s+\\\$0-(.*?)(,\s+f\s+[0-9]+)?;)_",
×
861
          QRegularExpression::DotMatchesEverythingOption};
×
862
      auto it = recv_regex.globalMatch(patch);
×
863
      while(it.hasNext())
×
864
      {
865
        const auto& m = it.next();
×
866
        if(m.hasMatch())
×
867
        {
868
          if(const auto var = m.captured(2); !var.isEmpty())
×
869
          {
870
            PatchSpec::Control ctl = parseControlSpec(var);
×
871

872
            auto p = makeInletFromSpec(ctl, get_next_id(), this);
×
873
            m_inlets.push_back(p);
×
874

875
            m_spec.receives.push_back(ctl);
×
876
          }
×
877
        }
×
878
      }
×
879
    }
×
880

881
    {
882
      static const QRegularExpression send_regex{
×
883
          R"_((s|send)\s+\\\$0-(.*?)(,\s+f\s+[0-9]+)?;)_",
×
884
          QRegularExpression::DotMatchesEverythingOption};
×
885
      auto it = send_regex.globalMatch(patch);
×
886
      while(it.hasNext())
×
887
      {
888
        const auto& m = it.next();
×
889
        if(m.hasMatch())
×
890
        {
891
          if(const auto var = m.captured(2); !var.isEmpty())
×
892
          {
893
            PatchSpec::Control ctl = parseControlSpec(var);
×
894

895
            Process::Outlet* p{};
×
896
            p = new Process::ValueOutlet{ctl.name, get_next_id(), this};
×
897
            m_outlets.push_back(p);
×
898

899
            m_spec.sends.push_back(ctl);
×
900
          }
×
901
        }
×
902
      }
×
903
    }
×
904

905
    res.valid = true;
×
906
  }
×
907

908
  // Create instance
909
  libpd_set_instance(m_instance->instance);
4✔
910

911
  if(m_instance->file_handle)
4✔
912
  {
913
    libpd_closefile(m_instance->file_handle);
×
914
    m_instance->file_handle = nullptr;
×
915
  }
×
916

917
  // Enable audio
918
  libpd_init_audio(
4✔
919
      m_audioInputs, m_audioOutputs,
4✔
920
      score::AppContext().settings<Audio::Settings::Model>().getRate());
4✔
921

922
  libpd_start_message(1);
4✔
923
  libpd_add_float(1.0f);
4✔
924
  libpd_finish_message("pd", "dsp");
4✔
925

926
  // Open. With no script (empty path) there is nothing to open: libpd would
927
  // dereference an empty filename out of bounds, so leave the instance patchless.
928
  QFileInfo fileinfo{f};
4✔
929
  const auto pdFileName = fileinfo.fileName();
4✔
930
  if(!pdFileName.isEmpty())
4✔
931
  {
932
    auto folder = fileinfo.canonicalPath();
×
933
    add_pd_search_paths(folder);
×
934

935
    m_instance->file_handle
×
936
        = libpd_openfile(pdFileName.toUtf8().data(), folder.toUtf8().data());
×
937
    m_instance->dollarzero = libpd_getdollarzero(m_instance->file_handle);
×
938

939
    std::vector<float> temp_buff;
×
940
    temp_buff.resize(libpd_blocksize() * (std::max(m_audioInputs, m_audioOutputs)));
×
941

942
    libpd_process_raw(temp_buff.data(), temp_buff.data());
×
943
  }
×
944

945
  scriptChanged(script);
4✔
946
  return res;
4✔
947
}
4✔
948

949
const QString& ProcessModel::script() const
2✔
950
{
951
  return m_script;
2✔
952
}
953

NEW
954
void ProcessModel::mapExternalFiles(Process::ExternalFileMap& map)
×
955
{
NEW
956
  Process::ProcessModel::mapExternalFiles(map);
×
957

NEW
958
  if(m_script.isEmpty())
×
NEW
959
    return;
×
960

NEW
961
  const QString next = map.map(
×
NEW
962
      {.path = m_script,
×
963
       .kind = score::FileKind::Script,
964
       .usage = Process::FileUsage::Input,
965
       .directory = false,
966
       .rewritable = true,
NEW
967
       .owner = map.owner});
×
968

969
  // Reloading a patch rebuilds the ports: EditPdPath is the command that saves
970
  // and restores the cables around that.
NEW
971
  if(!next.isEmpty())
×
NEW
972
    map.addCommand(
×
NEW
973
        new Pd::EditPdPath{*this, next, score::IDocument::documentContext(*this)});
×
NEW
974
}
×
975

976
QString ProcessModel::effect() const noexcept
2✔
977
{
978
  return m_script;
2✔
979
}
980

981
void ProcessModel::loadPreset(const Process::Preset& preset)
×
982
{
983
  Process::loadScriptProcessPreset<ProcessModel::p_script>(*this, preset);
×
984
}
×
985

986
Process::Preset ProcessModel::savePreset() const noexcept
×
987
{
988
  return Process::saveScriptProcessPreset(*this, this->m_script);
×
989
}
990
}
991

992
template <>
993
void DataStreamReader::read(const Pd::ProcessModel& proc)
2✔
994
{
995
  insertDelimiter();
2✔
996

997
  // setScript() resolves the patch to an absolute path; write it back
998
  // relative to the document so that the .score stays portable.
999
  m_stream << score::relativizeFilePath(
6✔
1000
      proc.m_script, score::IDocument::documentContext(proc))
2✔
1001
           << proc.m_audioInputs << proc.m_audioOutputs << proc.m_midiInput
2✔
1002
           << proc.m_midiOutput;
2✔
1003

1004
  readPorts(*this, proc.m_inlets, proc.m_outlets);
2✔
1005

1006
  insertDelimiter();
2✔
1007
}
2✔
1008

1009
template <>
1010
void DataStreamWriter::write(Pd::ProcessModel& proc)
1✔
1011
{
1012
  checkDelimiter();
1✔
1013

1014
  QString script;
1✔
1015
  m_stream >> script >> proc.m_audioInputs >> proc.m_audioOutputs >> proc.m_midiInput
1✔
1016
      >> proc.m_midiOutput;
1✔
1017
  (void)proc.setScript(script);
1✔
1018

1019
  writePorts(
1✔
1020
      *this, components.interfaces<Process::PortFactoryList>(), proc.m_inlets,
1✔
1021
      proc.m_outlets, &proc);
1✔
1022

1023
  checkDelimiter();
1✔
1024
}
1✔
1025

1026
template <>
1027
void JSONReader::read(const Pd::ProcessModel& proc)
2✔
1028
{
1029
  obj["Script"] = score::relativizeFilePath(
2✔
1030
      proc.script(), score::IDocument::documentContext(proc));
2✔
1031
  obj["AudioInputs"] = proc.audioInputs();
2✔
1032
  obj["AudioOutputs"] = proc.audioOutputs();
2✔
1033
  obj["MidiInput"] = proc.midiInput();
2✔
1034
  obj["MidiOutput"] = proc.midiOutput();
2✔
1035

1036
  readPorts(*this, proc.m_inlets, proc.m_outlets);
2✔
1037
}
2✔
1038

1039
template <>
1040
void JSONWriter::write(Pd::ProcessModel& proc)
1✔
1041
{
1042
  (void)proc.setScript(obj["Script"].toString());
1✔
1043
  proc.m_audioInputs = obj["AudioInputs"].toInt();
1✔
1044
  proc.m_audioOutputs = obj["AudioOutputs"].toInt();
1✔
1045
  proc.m_midiInput = obj["MidiInput"].toBool();
1✔
1046
  proc.m_midiOutput = obj["MidiOutput"].toBool();
1✔
1047

1048
  // TODO what happens if the patch's inputs / outputs changed??
1049
  // Maybe there should be the "edit script" algorithm available in a more general way
1050
  writePorts(
1✔
1051
      *this, components.interfaces<Process::PortFactoryList>(), proc.m_inlets,
1✔
1052
      proc.m_outlets, &proc);
1✔
1053
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc