@thedecipherist/claude-packs 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +275 -18
  3. package/bin/claude-packs.mjs +18 -10
  4. package/configurations/base/config.json +1 -1
  5. package/configurations/cpp/config.json +2 -1
  6. package/configurations/full-stack/config.json +2 -1
  7. package/configurations/go/config.json +3 -1
  8. package/configurations/node/config.json +3 -1
  9. package/configurations/python/config.json +3 -1
  10. package/configurations/rust/config.json +106 -0
  11. package/configurations/synth-creator/config.json +1 -1
  12. package/global/hooks/lib/shell-writes.mjs +2 -293
  13. package/lib/build-queue.mjs +22 -0
  14. package/lib/claude-md.mjs +4 -2
  15. package/lib/config-cli.mjs +1 -1
  16. package/lib/coverage.mjs +10 -2
  17. package/lib/doctor.mjs +14 -3
  18. package/lib/fix-plan.mjs +2 -1
  19. package/lib/found-folders.mjs +25 -0
  20. package/lib/global.mjs +3 -3
  21. package/lib/hook-proof.mjs +24 -7
  22. package/lib/kit-command.mjs +14 -0
  23. package/lib/layout.mjs +27 -3
  24. package/lib/logs.mjs +45 -5
  25. package/lib/new.mjs +1 -1
  26. package/lib/packs.mjs +4 -2
  27. package/lib/rule-tests.mjs +42 -0
  28. package/lib/scan-detect.mjs +41 -5
  29. package/lib/scan.mjs +5 -2
  30. package/lib/suggest.mjs +16 -3
  31. package/lib/sync.mjs +5 -2
  32. package/lib/workorder.mjs +26 -0
  33. package/package.json +1 -1
  34. package/packs/audio-core/hooks/param-id-lock.mjs +59 -16
  35. package/packs/audio-core/pack.json +5 -0
  36. package/packs/audio-core/rules/a-control-is-proved-through-its-input.md +26 -0
  37. package/packs/audio-core/rules/cpu-worst-case-per-block.md +3 -1
  38. package/packs/audio-core/rules/house-control-behaviours.md +1 -1
  39. package/packs/audio-core/rules/scoped-no-denormals.md +1 -1
  40. package/packs/audio-core/rules/straight-on-and-aligned.md +1 -1
  41. package/packs/audio-core/rules/ui-matches-reference-look.md +2 -0
  42. package/packs/audio-core/skills/analyse-reference-look/SKILL.md +25 -11
  43. package/packs/audio-core/skills/prove-control/SKILL.md +2 -0
  44. package/packs/base/checks/one-walk-per-template.json +1 -1
  45. package/packs/base/hooks/code-shape.mjs +10 -6
  46. package/packs/base/hooks/dispatch-once.mjs +58 -21
  47. package/packs/base/hooks/folder-law.mjs +3 -0
  48. package/packs/base/hooks/git-guard.mjs +22 -109
  49. package/packs/base/hooks/lesson-checks.mjs +9 -1
  50. package/packs/base/hooks/lib/code-shapes.mjs +11 -6
  51. package/packs/base/hooks/lib/crash-policy.mjs +47 -0
  52. package/packs/base/hooks/lib/dup-index.mjs +6 -1
  53. package/packs/base/hooks/lib/dup.mjs +2 -2
  54. package/packs/base/hooks/lib/event-log.mjs +4 -3
  55. package/packs/base/hooks/lib/git-adds.mjs +2 -1
  56. package/packs/base/hooks/lib/git-auth.mjs +44 -0
  57. package/packs/base/hooks/lib/git-guard-selftest.mjs +93 -0
  58. package/packs/base/hooks/lib/languages.mjs +5 -2
  59. package/packs/base/hooks/lib/layout.mjs +6 -2
  60. package/packs/base/hooks/lib/making-check-selftest.mjs +3 -0
  61. package/packs/base/hooks/lib/near-copies.mjs +1 -1
  62. package/packs/base/hooks/lib/program-read.mjs +6 -3
  63. package/packs/base/hooks/lib/rust-scan.mjs +139 -0
  64. package/packs/base/hooks/lib/shape-lists.mjs +31 -0
  65. package/packs/base/hooks/lib/shape-prose.mjs +78 -0
  66. package/packs/base/hooks/lib/shape.mjs +18 -39
  67. package/packs/base/hooks/lib/turn.mjs +6 -1
  68. package/packs/base/hooks/lib/unread-writes.mjs +24 -0
  69. package/packs/base/hooks/making-check.mjs +12 -7
  70. package/packs/base/hooks/run-hooks.mjs +15 -7
  71. package/packs/base/hooks/shape-law.mjs +11 -4
  72. package/packs/base/hooks/suite-once.mjs +22 -6
  73. package/packs/base/rules/a-control-is-seen-to-fire.md +1 -1
  74. package/packs/base/rules/behaviour-lives-with-its-kind.md +1 -1
  75. package/packs/base/rules/bug-fix-starts-red.md +1 -1
  76. package/packs/base/rules/field-map-over-if-runs.md +1 -1
  77. package/packs/base/rules/instances-by-index-identities-by-id.md +2 -2
  78. package/packs/base/rules/lookup-over-if-chains.md +1 -1
  79. package/packs/base/rules/plan-and-logs.md +1 -1
  80. package/packs/base/rules/refusals-are-judged.md +1 -0
  81. package/packs/base/rules/test-the-template-once.md +2 -1
  82. package/packs/base/rules/unattended-runs.md +15 -0
  83. package/packs/base/skills/new-project/SKILL.md +23 -0
  84. package/packs/base/skills/write-a-workorder/SKILL.md +41 -0
  85. package/packs/cpp/checks/cpp-find-with-the-library.json +2 -2
  86. package/packs/go/pack.json +5 -0
  87. package/packs/hook-proof/rules/a-refusal-is-exit-2.md +2 -2
  88. package/packs/instrument-shell/rules/render-the-extremes.md +2 -2
  89. package/packs/midi/checks/position-not-integrated.json +5 -1
  90. package/packs/postgres/checks/pg-bind-values.json +33 -0
  91. package/packs/postgres/checks/pg-concurrently-in-txn.json +18 -0
  92. package/packs/postgres/checks/pg-fk-not-valid.json +18 -0
  93. package/packs/postgres/checks/pg-index-concurrently.json +18 -0
  94. package/packs/postgres/checks/pg-no-float-money.json +19 -0
  95. package/packs/postgres/checks/pg-no-money.json +18 -0
  96. package/packs/postgres/checks/pg-no-required-column.json +18 -0
  97. package/packs/postgres/checks/pg-no-volatile-default.json +18 -0
  98. package/packs/postgres/checks/pg-not-exists.json +18 -0
  99. package/packs/postgres/checks/pg-timestamptz.json +18 -0
  100. package/packs/postgres/hooks/db-wipe-guard.mjs +95 -0
  101. package/packs/postgres/pack.json +48 -0
  102. package/packs/postgres/rules/postgres-migrations-never-stop-traffic.md +28 -0
  103. package/packs/postgres/rules/postgres-one-pool-per-process.md +21 -0
  104. package/packs/postgres/rules/postgres-queries-are-null-safe-and-bounded.md +26 -0
  105. package/packs/postgres/rules/postgres-types-say-what-they-hold.md +22 -0
  106. package/packs/postgres/rules/postgres-values-are-bound.md +130 -0
  107. package/packs/postgres/skills/postgres-safe-migration/SKILL.md +45 -0
  108. package/packs/reverse-engineering/rules/addresses-name-their-space.md +1 -0
  109. package/packs/reverse-engineering/rules/reproduction-or-original.md +1 -1
  110. package/packs/reverse-engineering/skills/gather-sources/SKILL.md +1 -0
  111. package/packs/rust/checks/rust-allow-needs-reason.json +16 -0
  112. package/packs/rust/checks/rust-blocking-in-async.json +32 -0
  113. package/packs/rust/checks/rust-if-chain-is-a-match.json +32 -0
  114. package/packs/rust/checks/rust-static-mut.json +32 -0
  115. package/packs/rust/checks/rust-string-as-error.json +34 -0
  116. package/packs/rust/checks/rust-stub-left-in.json +32 -0
  117. package/packs/rust/checks/rust-unsafe-needs-safety.json +31 -0
  118. package/packs/rust/checks/rust-unwrap-in-library.json +34 -0
  119. package/packs/rust/checks/rust-wildcard-dependency.json +16 -0
  120. package/packs/rust/clippy.toml +5 -0
  121. package/packs/rust/pack.json +27 -0
  122. package/packs/rust/rules/rust-async-never-blocks.md +20 -0
  123. package/packs/rust/rules/rust-borrow-before-you-clone.md +19 -0
  124. package/packs/rust/rules/rust-errors-are-returned.md +22 -0
  125. package/packs/rust/rules/rust-lints-are-the-gate.md +23 -0
  126. package/packs/rust/rules/rust-repeats-become-templates.md +28 -0
  127. package/packs/rust/rules/rust-unsafe-is-proved.md +21 -0
  128. package/packs/rust/skills/rust-extract-template/SKILL.md +50 -0
  129. package/packs/rust/skills/rust-port/SKILL.md +22 -0
  130. package/packs/rust/skills/rust-review/SKILL.md +24 -0
  131. package/packs/rust/tools/rust-lints.toml +19 -0
  132. package/packs/sound-design/skills/bounce-a-plugin/SKILL.md +40 -0
  133. package/packs/sound-design/tools/vst3host/build.sh +73 -0
  134. package/packs/sound-design/tools/vst3host/host_events.h +101 -0
  135. package/packs/sound-design/tools/vst3host/options.h +172 -0
  136. package/packs/sound-design/tools/vst3host/render.cpp +245 -0
  137. package/packs/sound-design/tools/vst3host/session.h +241 -0
  138. package/packs/synth-creator/hooks/build-guard.mjs +7 -7
  139. package/packs/synth-creator/hooks/protect-vendor.mjs +2 -1
  140. package/packs/synth-creator/hooks/stage-guard.mjs +1 -1
  141. package/packs/synth-creator/rules/timing-from-synthcore.md +1 -1
  142. package/packs/synth-creator/skills/build-through-the-queue/SKILL.md +4 -2
  143. package/packs/synth-creator/tools/buildq.mjs +74 -0
  144. package/prompts/workorder/PROMPT.md +53 -0
  145. package/prompts/workorder/run.sh +36 -0
  146. package/prompts/workorder/workorder.md +59 -0
  147. package/global/hooks/lib/program-read.mjs +0 -196
  148. package/global/hooks/lib/program-writes.mjs +0 -177
@@ -0,0 +1,172 @@
1
+ // The renderer's command line. One row per flag: a new flag is one new row.
2
+ #pragma once
3
+
4
+ #include <cstdint>
5
+ #include <cstdlib>
6
+ #include <functional>
7
+ #include <map>
8
+ #include <sstream>
9
+ #include <stdexcept>
10
+ #include <string>
11
+ #include <vector>
12
+
13
+ namespace vst3host {
14
+
15
+ struct Axis {
16
+ uint32_t offset = 0;
17
+ int steps = 0;
18
+ int max = 127;
19
+ };
20
+
21
+ struct Options {
22
+ std::string plugin;
23
+ std::string out = "render.wav";
24
+ std::string dir = ".";
25
+ double sampleRate = 44100.0;
26
+ double boot = 1.0;
27
+ double hold = 1.5;
28
+ double tail = 0.6;
29
+ double settle = 0.4;
30
+ int note = 60;
31
+ int velocity = 100;
32
+ int block = 512;
33
+ int program = -1;
34
+ int bankMsb = -1;
35
+ int bankLsb = -1;
36
+ bool list = false;
37
+ bool dumpParams = false;
38
+ std::map<uint32_t, double> params;
39
+ uint32_t sweepId = 0;
40
+ int sweepSteps = 0;
41
+ std::vector<std::string> sysexFiles;
42
+ // A Roland DT1 message is `F0 <head> 12 <address> <value> <sum> F7`. <head> is the maker, device and model: `41 10 00 06`.
43
+ std::vector<unsigned char> dt1Head;
44
+ uint32_t dt1Base = 0;
45
+ std::map<uint32_t, int> dt1;
46
+ Axis dt1Sweep;
47
+ std::vector<Axis> axes;
48
+ int probeLo = -1;
49
+ int probeHi = -1;
50
+ std::vector<int> probeValues { 0, 127 };
51
+ int mapLo = -1;
52
+ int mapHi = -1;
53
+ int mapValue = 100;
54
+ };
55
+
56
+ inline std::vector<std::string> split (const std::string& text, char by) {
57
+ std::vector<std::string> parts;
58
+ std::stringstream stream (text);
59
+ for (std::string part; std::getline (stream, part, by);) parts.push_back (part);
60
+ return parts;
61
+ }
62
+
63
+ inline uint32_t hex (const std::string& text) { return (uint32_t) std::stoul (text, nullptr, 16); }
64
+
65
+ // OFFSET:STEPS[:MAX], the offset in hex.
66
+ inline Axis axisOf (const std::string& text) {
67
+ const auto parts = split (text, ':');
68
+ if (parts.size() < 2) throw std::invalid_argument ("expected OFFSET:STEPS[:MAX], got " + text);
69
+ return { hex (parts[0]), std::stoi (parts[1]), parts.size() > 2 ? std::stoi (parts[2]) : 127 };
70
+ }
71
+
72
+ // LO-HI, in hex, into two ints.
73
+ inline void rangeOf (const std::string& text, int& lo, int& hi) {
74
+ const auto parts = split (text, '-');
75
+ if (parts.size() != 2) throw std::invalid_argument ("expected LO-HI, got " + text);
76
+ lo = (int) hex (parts[0]);
77
+ hi = (int) hex (parts[1]);
78
+ }
79
+
80
+ inline void bytesOf (const std::string& text, std::vector<unsigned char>& into) {
81
+ for (const auto& part : split (text, ' ')) into.push_back ((unsigned char) hex (part));
82
+ }
83
+
84
+ inline void probeOf (Options& o, const std::string& text) {
85
+ const auto parts = split (text, ':');
86
+ rangeOf (parts[0], o.probeLo, o.probeHi);
87
+ if (parts.size() < 2) return;
88
+ o.probeValues.clear();
89
+ for (const auto& value : split (parts[1], ',')) o.probeValues.push_back (std::stoi (value));
90
+ }
91
+
92
+ inline void mapOf (Options& o, const std::string& text) {
93
+ const auto parts = split (text, ':');
94
+ rangeOf (parts[0], o.mapLo, o.mapHi);
95
+ if (parts.size() > 1) o.mapValue = std::stoi (parts[1]);
96
+ }
97
+
98
+ inline void paramOf (Options& o, const std::string& text) {
99
+ const auto parts = split (text, '=');
100
+ if (parts.size() != 2) throw std::invalid_argument ("expected ID=VALUE, got " + text);
101
+ o.params[(uint32_t) std::stoul (parts[0])] = std::stod (parts[1]);
102
+ }
103
+
104
+ inline void dt1Of (Options& o, const std::string& text) {
105
+ const auto parts = split (text, '=');
106
+ if (parts.size() != 2) throw std::invalid_argument ("expected OFFSET=VALUE, got " + text);
107
+ o.dt1[hex (parts[0])] = std::stoi (parts[1]);
108
+ }
109
+
110
+ inline void sweepOf (Options& o, const std::string& text) {
111
+ const auto parts = split (text, ':');
112
+ if (parts.size() != 2) throw std::invalid_argument ("expected ID:STEPS, got " + text);
113
+ o.sweepId = (uint32_t) std::stoul (parts[0]);
114
+ o.sweepSteps = std::stoi (parts[1]);
115
+ }
116
+
117
+ inline void bankOf (Options& o, const std::string& text) {
118
+ const auto parts = split (text, ':');
119
+ if (parts.size() != 2) throw std::invalid_argument ("expected MSB:LSB, got " + text);
120
+ o.bankMsb = std::stoi (parts[0]);
121
+ o.bankLsb = std::stoi (parts[1]);
122
+ }
123
+
124
+ using Row = std::function<void (Options&, const std::string&)>;
125
+
126
+ inline const std::map<std::string, Row>& valueFlags() {
127
+ static const std::map<std::string, Row> rows {
128
+ { "--vst3", [] (Options& o, const std::string& v) { o.plugin = v; } },
129
+ { "--out", [] (Options& o, const std::string& v) { o.out = v; } },
130
+ { "--dir", [] (Options& o, const std::string& v) { o.dir = v; } },
131
+ { "--sr", [] (Options& o, const std::string& v) { o.sampleRate = std::stod (v); } },
132
+ { "--boot", [] (Options& o, const std::string& v) { o.boot = std::stod (v); } },
133
+ { "--hold", [] (Options& o, const std::string& v) { o.hold = std::stod (v); } },
134
+ { "--tail", [] (Options& o, const std::string& v) { o.tail = std::stod (v); } },
135
+ { "--settle", [] (Options& o, const std::string& v) { o.settle = std::stod (v); } },
136
+ { "--note", [] (Options& o, const std::string& v) { o.note = std::stoi (v); } },
137
+ { "--vel", [] (Options& o, const std::string& v) { o.velocity = std::stoi (v); } },
138
+ { "--block", [] (Options& o, const std::string& v) { o.block = std::stoi (v); } },
139
+ { "--program", [] (Options& o, const std::string& v) { o.program = std::stoi (v); } },
140
+ { "--bank", bankOf },
141
+ { "--param", paramOf },
142
+ { "--sweep", sweepOf },
143
+ { "--sysex", [] (Options& o, const std::string& v) { o.sysexFiles.push_back (v); } },
144
+ { "--dt1-head", [] (Options& o, const std::string& v) { bytesOf (v, o.dt1Head); } },
145
+ { "--dt1-base", [] (Options& o, const std::string& v) { o.dt1Base = hex (v); } },
146
+ { "--dt1", dt1Of },
147
+ { "--sweep-dt1", [] (Options& o, const std::string& v) { o.dt1Sweep = axisOf (v); } },
148
+ { "--axis", [] (Options& o, const std::string& v) { o.axes.push_back (axisOf (v)); } },
149
+ { "--probe", probeOf },
150
+ { "--map", mapOf },
151
+ };
152
+ return rows;
153
+ }
154
+
155
+ inline Options parseOptions (int argc, char** argv) {
156
+ Options o;
157
+ for (int i = 1; i < argc; ++i) {
158
+ const std::string flag = argv[i];
159
+ if (flag == "--list") o.list = true;
160
+ else if (flag == "--dump-params") o.dumpParams = true;
161
+ else {
162
+ const auto row = valueFlags().find (flag);
163
+ if (row == valueFlags().end()) throw std::invalid_argument ("unknown flag " + flag);
164
+ if (i + 1 >= argc) throw std::invalid_argument (flag + " needs a value");
165
+ row->second (o, argv[++i]);
166
+ }
167
+ }
168
+ if (o.plugin.empty()) throw std::invalid_argument ("name the plugin: --vst3 <path>");
169
+ return o;
170
+ }
171
+
172
+ } // namespace vst3host
@@ -0,0 +1,245 @@
1
+ // Offline VST3 renderer: play a known note through a plugin and write what comes out, with no DAW, audio device or window.
2
+ // render --vst3 Plugin.vst3 --list every parameter, with its id
3
+ // render --vst3 Plugin.vst3 --out a.wav --note 60 --hold 1.5 one note
4
+ // render --vst3 Plugin.vst3 --sweep 38:9 --dir out/ one note per value of parameter 38
5
+ // render --vst3 Plugin.vst3 --sysex patch.syx --dump-params send a patch, and read whether it was taken
6
+ // The `bounce-a-plugin` skill says how to use it and what it took to learn.
7
+ #include "session.h"
8
+
9
+ #include <fstream>
10
+ #include <iterator>
11
+ #include <stdexcept>
12
+
13
+ using namespace vst3host;
14
+
15
+ namespace {
16
+
17
+ template <typename T> void put (FILE* f, T value) { std::fwrite (&value, sizeof value, 1, f); }
18
+
19
+ // 32-bit float, so a plugin that peaks over 1.0 is measured and never clipped.
20
+ void writeWav (const std::string& path, Session& session, double sampleRate) {
21
+ const auto& left = session.keptLeft();
22
+ const auto& right = session.keptRight();
23
+ const auto bytes = (uint32_t) (left.size() * 2 * sizeof (float));
24
+ FILE* f = std::fopen (path.c_str(), "wb");
25
+ if (! f) throw std::runtime_error ("cannot write " + path);
26
+ std::fwrite ("RIFF", 1, 4, f);
27
+ put<uint32_t> (f, 36 + bytes);
28
+ std::fwrite ("WAVEfmt ", 1, 8, f);
29
+ put<uint32_t> (f, 16);
30
+ put<uint16_t> (f, 3);
31
+ put<uint16_t> (f, 2);
32
+ put<uint32_t> (f, (uint32_t) sampleRate);
33
+ put<uint32_t> (f, (uint32_t) sampleRate * 8);
34
+ put<uint16_t> (f, 8);
35
+ put<uint16_t> (f, 32);
36
+ std::fwrite ("data", 1, 4, f);
37
+ put<uint32_t> (f, bytes);
38
+ for (size_t i = 0; i < left.size(); ++i) {
39
+ put<float> (f, left[i]);
40
+ put<float> (f, right[i]);
41
+ }
42
+ std::fclose (f);
43
+ }
44
+
45
+ void oneNote (Session& session, const Options& o, const std::string& file) {
46
+ session.clearKept();
47
+ session.run (o.settle, false); // let the DSP finish slewing its coefficients
48
+ session.noteOn (o.note, o.velocity);
49
+ session.run (o.hold, true);
50
+ session.noteOff (o.note);
51
+ session.run (o.tail, true);
52
+ double peak = 0;
53
+ double sum = 0;
54
+ const auto& left = session.keptLeft();
55
+ const auto& right = session.keptRight();
56
+ for (size_t i = 0; i < left.size(); ++i) {
57
+ const double mid = 0.5 * (left[i] + right[i]);
58
+ peak = std::max (peak, std::abs (mid));
59
+ sum += mid * mid;
60
+ }
61
+ writeWav (file, session, o.sampleRate);
62
+ std::printf (" %s %.2fs peak %.5f rms %.6f\n", file.c_str(), left.size() / o.sampleRate, peak, std::sqrt (sum / std::max<size_t> (1, left.size())));
63
+ }
64
+
65
+ std::vector<unsigned char> dt1Message (const Options& o, uint32_t address, int value) {
66
+ if (o.dt1Head.empty()) throw std::invalid_argument ("a DT1 flag needs --dt1-head, the maker, device and model: `41 10 00 06`");
67
+ std::vector<unsigned char> message { 0xF0 };
68
+ message.insert (message.end(), o.dt1Head.begin(), o.dt1Head.end());
69
+ message.push_back (0x12);
70
+ int sum = 0;
71
+ for (int shift = 24; shift >= 0; shift -= 8) {
72
+ const auto byte = (unsigned char) ((address >> shift) & 0x7F);
73
+ message.push_back (byte);
74
+ sum += byte;
75
+ }
76
+ const auto byte = (unsigned char) (value & 0x7F);
77
+ message.push_back (byte);
78
+ message.push_back ((unsigned char) ((128 - ((sum + byte) % 128)) & 0x7F));
79
+ message.push_back (0xF7);
80
+ return message;
81
+ }
82
+
83
+ void sendDt1 (Session& session, const Options& o, uint32_t offset, int value) { session.sendBytes (dt1Message (o, o.dt1Base + offset, value)); }
84
+
85
+ // One batch, then one settle, then the note: writes spread one per block made levels wander 8 dB between steps of a sweep.
86
+ void sendBaseline (Session& session, const Options& o) {
87
+ for (const auto& [offset, value] : o.dt1) sendDt1 (session, o, offset, value);
88
+ }
89
+
90
+ void sendProgram (Session& session, const Options& o) {
91
+ if (o.program < 0) return;
92
+ if (o.bankMsb >= 0) {
93
+ session.sendBytes ({ 0xB0, 0x00, (unsigned char) o.bankMsb });
94
+ session.sendBytes ({ 0xB0, 0x20, (unsigned char) o.bankLsb });
95
+ }
96
+ session.sendBytes ({ 0xC0, (unsigned char) (o.program & 0x7F) });
97
+ session.run (0.6, false);
98
+ }
99
+
100
+ // A file may hold several messages: split on F0..F7 and sent one per settle, since a synth's MIDI input is a byte stream and a dump is long.
101
+ void sendSysexFile (Session& session, const std::string& path) {
102
+ std::ifstream in (path, std::ios::binary);
103
+ if (! in) throw std::runtime_error ("cannot read " + path);
104
+ const std::vector<unsigned char> all ((std::istreambuf_iterator<char> (in)), std::istreambuf_iterator<char>());
105
+ size_t sent = 0;
106
+ for (size_t i = 0; i < all.size();) {
107
+ const auto start = std::find (all.begin() + (long) i, all.end(), (unsigned char) 0xF0);
108
+ const auto end = std::find (start, all.end(), (unsigned char) 0xF7);
109
+ if (end == all.end()) break;
110
+ session.sendBytes (std::vector<unsigned char> (start, end + 1));
111
+ session.run (0.05, false);
112
+ sent += 1;
113
+ i = (size_t) (end - all.begin()) + 1;
114
+ }
115
+ std::printf (" sent %zu sysex message(s) from %s\n", sent, path.c_str());
116
+ session.run (0.5, false);
117
+ }
118
+
119
+ std::string named (const Options& o, const char* format, unsigned a, int b) {
120
+ char name[256];
121
+ std::snprintf (name, sizeof name, format, a, b);
122
+ return o.dir + "/" + name;
123
+ }
124
+
125
+ // Which parameter is this address? Ask the plugin: what it reports, or else what moved on the controller.
126
+ void mapOffsets (Session& session, const Options& o) {
127
+ auto* controller = session.editController();
128
+ std::printf (" offset -> parameter, as the plugin reports it\n");
129
+ std::vector<double> was;
130
+ const int32 count = controller ? controller->getParameterCount() : 0;
131
+ const auto valueOf = [&] (int32 i) {
132
+ Vst::ParameterInfo info {};
133
+ return controller->getParameterInfo (i, info) == kResultOk ? controller->getParamNormalized (info.id) : 0.0;
134
+ };
135
+ for (int32 i = 0; i < count; ++i) was.push_back (valueOf (i));
136
+ for (int offset = o.mapLo; offset <= o.mapHi; ++offset) {
137
+ session.reported().clear();
138
+ sendDt1 (session, o, (uint32_t) offset, o.mapValue);
139
+ session.run (0.25, false);
140
+ std::printf (" 0x%02x ", offset);
141
+ int moved = session.reported().getParameterCount();
142
+ for (int32 i = 0; i < count; ++i) {
143
+ const double now = valueOf (i);
144
+ if (std::abs (now - was[(size_t) i]) <= 1e-6) continue;
145
+ Vst::ParameterInfo info {};
146
+ controller->getParameterInfo (i, info);
147
+ std::printf (" %s = %.4f", titleOf (info).c_str(), now);
148
+ was[(size_t) i] = now;
149
+ moved += 1;
150
+ }
151
+ std::printf (moved ? "\n" : " (nothing reported, nothing moved)\n");
152
+ }
153
+ }
154
+
155
+ // Which offset IS the control? Each render puts the whole range back to rest first: without it the loop is cumulative, and each offset is measured on the last.
156
+ void probeOffsets (Session& session, const Options& o) {
157
+ for (int offset = o.probeLo; offset <= o.probeHi; ++offset) {
158
+ for (const int value : o.probeValues) {
159
+ for (int other = o.probeLo; other <= o.probeHi; ++other) sendDt1 (session, o, (uint32_t) other, o.probeValues.front());
160
+ sendBaseline (session, o);
161
+ sendDt1 (session, o, (uint32_t) offset, value);
162
+ session.run (0.3, false);
163
+ oneNote (session, o, named (o, "probe_%02x_%03d.wav", (unsigned) offset, value));
164
+ }
165
+ }
166
+ }
167
+
168
+ int stepValue (const Axis& axis, int step) { return axis.steps == 1 ? 0 : step * axis.max / (axis.steps - 1); }
169
+
170
+ // Every combination of the axes, outermost first.
171
+ void walkAxes (Session& session, const Options& o, size_t depth, std::string name) {
172
+ if (depth == o.axes.size()) {
173
+ session.run (0.3, false);
174
+ return oneNote (session, o, o.dir + "/ax" + name + ".wav");
175
+ }
176
+ const auto& axis = o.axes[depth];
177
+ for (int step = 0; step < axis.steps; ++step) {
178
+ const int value = stepValue (axis, step);
179
+ if (depth == 0) sendBaseline (session, o);
180
+ sendDt1 (session, o, axis.offset, value);
181
+ walkAxes (session, o, depth + 1, name + "_" + std::to_string (axis.offset) + "-" + std::to_string (value));
182
+ }
183
+ }
184
+
185
+ void sweepDt1 (Session& session, const Options& o) {
186
+ for (int step = 0; step < o.dt1Sweep.steps; ++step) {
187
+ const int value = stepValue (o.dt1Sweep, step);
188
+ sendBaseline (session, o);
189
+ sendDt1 (session, o, o.dt1Sweep.offset, value);
190
+ session.run (0.3, false);
191
+ oneNote (session, o, named (o, "dt1_%02x_%03d.wav", o.dt1Sweep.offset, value));
192
+ }
193
+ }
194
+
195
+ // One note per value, never a knob turned under a sounding note: a swept knob measures a ramp, and a host may drop the note-off.
196
+ void sweepParam (Session& session, const Options& o) {
197
+ for (int step = 0; step < o.sweepSteps; ++step) {
198
+ const double value = o.sweepSteps > 1 ? (double) step / (o.sweepSteps - 1) : 0.0;
199
+ for (const auto& [id, frozen] : o.params) session.setParam (id, frozen);
200
+ session.setParam (o.sweepId, value);
201
+ oneNote (session, o, named (o, "param%u_step%02d.wav", o.sweepId, step));
202
+ }
203
+ }
204
+
205
+ void render (Session& session, const Options& o) {
206
+ if (o.mapLo >= 0) mapOffsets (session, o);
207
+ else if (o.probeLo >= 0) probeOffsets (session, o);
208
+ else if (! o.axes.empty()) walkAxes (session, o, 0, "");
209
+ else if (o.dt1Sweep.steps > 0) sweepDt1 (session, o);
210
+ else if (o.sweepSteps > 0) sweepParam (session, o);
211
+ else oneNote (session, o, o.out);
212
+ }
213
+
214
+ int run (const Options& o) {
215
+ Session session (o);
216
+ if (const auto failed = session.open (o.plugin); ! failed.empty()) throw std::runtime_error (failed);
217
+ if (o.list) {
218
+ session.printParameters (false);
219
+ return 0;
220
+ }
221
+ if (const auto failed = session.start(); ! failed.empty()) throw std::runtime_error (failed);
222
+ session.run (o.boot, false); // an emulated firmware boots on a clock: it needs real samples before it answers a note
223
+ for (const auto& [id, value] : o.params) session.setParam (id, value);
224
+ sendProgram (session, o);
225
+ for (const auto& file : o.sysexFiles) sendSysexFile (session, file);
226
+ if (! o.dt1.empty()) {
227
+ sendBaseline (session, o);
228
+ session.run (0.3, false);
229
+ }
230
+ if (o.dumpParams) session.printParameters (true);
231
+ render (session, o);
232
+ session.stop();
233
+ return 0;
234
+ }
235
+
236
+ } // namespace
237
+
238
+ int main (int argc, char** argv) {
239
+ try {
240
+ return run (parseOptions (argc, argv));
241
+ } catch (const std::exception& e) {
242
+ std::fprintf (stderr, "render: %s\n", e.what());
243
+ return 1;
244
+ }
245
+ }
@@ -0,0 +1,241 @@
1
+ // One plugin, loaded and running offline: no audio device, no window, no JUCE. A JUCE host cannot load every JUCE plugin (an allocator clash across
2
+ // the boundary aborted one inside its own parameter loader), and every audio path through an operating system can fail differently, one of them by
3
+ // returning silence instead of an error. A renderer that never opens a device has none of those failures, and runs faster than real time.
4
+ #pragma once
5
+
6
+ #include "host_events.h"
7
+ #include "options.h"
8
+
9
+ #include "pluginterfaces/vst/ivstaudioprocessor.h"
10
+ #include "pluginterfaces/vst/ivstcomponent.h"
11
+ #include "pluginterfaces/vst/ivsteditcontroller.h"
12
+ #include "pluginterfaces/vst/ivstprocesscontext.h"
13
+ #include "public.sdk/source/common/memorystream.h"
14
+ #include "public.sdk/source/vst/hosting/hostclasses.h"
15
+ #include "public.sdk/source/vst/hosting/module.h"
16
+
17
+ #include <algorithm>
18
+ #include <array>
19
+ #include <cmath>
20
+ #include <cstdio>
21
+ #include <deque>
22
+ #include <memory>
23
+ #include <string>
24
+ #include <vector>
25
+
26
+ namespace vst3host {
27
+
28
+ inline std::string titleOf (const Vst::ParameterInfo& info) {
29
+ std::string name;
30
+ for (int k = 0; k < 128 && info.title[k]; ++k) name += (char) info.title[k];
31
+ return name;
32
+ }
33
+
34
+ class Session {
35
+ public:
36
+ Session (const Options& options) : sampleRate (options.sampleRate), block (options.block) {}
37
+
38
+ // Loads the module and wires the two halves. An empty return is success; anything else is why it failed.
39
+ std::string open (const std::string& path) {
40
+ std::string error;
41
+ module = VST3::Hosting::Module::create (path, error);
42
+ if (! module) return "load failed: " + error;
43
+ auto factory = module->getFactory();
44
+ for (auto& info : factory.classInfos()) {
45
+ if (info.category() != kVstAudioEffectClass) continue;
46
+ component = factory.createInstance<Vst::IComponent> (info.ID()).take();
47
+ break;
48
+ }
49
+ if (! component) return "no audio class";
50
+ host = new Vst::HostApplication();
51
+ if (component->initialize (host) != kResultOk) return "initialize failed";
52
+ component->queryInterface (Vst::IAudioProcessor::iid, (void**) &processor);
53
+ if (! processor) return "no IAudioProcessor";
54
+ openController (factory);
55
+ return {};
56
+ }
57
+
58
+ void printParameters (bool withValues) {
59
+ if (! controller) return void (std::printf (" no controller, so no parameters to read\n"));
60
+ const int32 count = controller->getParameterCount();
61
+ std::printf (" %d parameters\n", count);
62
+ for (int32 i = 0; i < count; ++i) {
63
+ Vst::ParameterInfo info {};
64
+ if (controller->getParameterInfo (i, info) != kResultOk) continue;
65
+ if (withValues) std::printf (" %-40s %.6f\n", titleOf (info).c_str(), controller->getParamNormalized (info.id));
66
+ else std::printf (" %5d id %-10u %s\n", i, info.id, titleOf (info).c_str());
67
+ }
68
+ }
69
+
70
+ // Every output bus is activated and given a buffer: a plugin handed fewer buses than it declares may write nothing at all.
71
+ std::string start() {
72
+ Vst::ProcessSetup setup {};
73
+ setup.processMode = Vst::kOffline;
74
+ setup.symbolicSampleSize = Vst::kSample32;
75
+ setup.maxSamplesPerBlock = block;
76
+ setup.sampleRate = sampleRate;
77
+ if (processor->setupProcessing (setup) != kResultOk) return "setupProcessing failed";
78
+ activateBuses();
79
+ component->setActive (true);
80
+ processor->setProcessing (true);
81
+ prepareBuffers();
82
+ return {};
83
+ }
84
+
85
+ void stop() {
86
+ processor->setProcessing (false);
87
+ component->setActive (false);
88
+ }
89
+
90
+ // Renders this long. Events and parameter changes go in the first block.
91
+ void run (double seconds, bool keep) {
92
+ const int64 total = (int64) (seconds * sampleRate);
93
+ for (int64 done = 0; done < total; done += block) {
94
+ const auto count = (int32) std::min<int64> (block, total - done);
95
+ processBlock (count);
96
+ if (! keep) continue;
97
+ left.insert (left.end(), buffers[0].begin(), buffers[0].begin() + count);
98
+ right.insert (right.end(), buffers[1].begin(), buffers[1].begin() + count);
99
+ }
100
+ }
101
+
102
+ // Both routes. A firmware emulator's parameters may talk to it only through the controller, and a plain plugin reads inputParameterChanges.
103
+ void setParam (Vst::ParamID id, double value) {
104
+ if (controller) controller->setParamNormalized (id, value);
105
+ int32 index = 0;
106
+ int32 point = 0;
107
+ if (auto* queue = changes.addParameterData (id, index)) queue->addPoint (0, value, point);
108
+ }
109
+
110
+ // VST3 has no event for sysex or a program change: raw MIDI goes in a data event, which a plugin's own MIDI parser reads.
111
+ void sendBytes (std::vector<unsigned char> bytes) {
112
+ kept.push_back (std::move (bytes));
113
+ Vst::Event e {};
114
+ e.type = Vst::Event::kDataEvent;
115
+ e.data.type = Vst::DataEvent::kMidiSysEx;
116
+ e.data.size = (uint32) kept.back().size();
117
+ e.data.bytes = kept.back().data();
118
+ events.addEvent (e);
119
+ }
120
+
121
+ void noteOn (int pitch, int velocity) {
122
+ Vst::Event e {};
123
+ e.type = Vst::Event::kNoteOnEvent;
124
+ e.noteOn.pitch = (int16) pitch;
125
+ e.noteOn.velocity = (float) velocity / 127.0f;
126
+ e.noteOn.noteId = -1;
127
+ events.addEvent (e);
128
+ }
129
+
130
+ void noteOff (int pitch) {
131
+ Vst::Event e {};
132
+ e.type = Vst::Event::kNoteOffEvent;
133
+ e.noteOff.pitch = (int16) pitch;
134
+ e.noteOff.noteId = -1;
135
+ events.addEvent (e);
136
+ }
137
+
138
+ void clearKept() {
139
+ left.clear();
140
+ right.clear();
141
+ }
142
+
143
+ const std::vector<float>& keptLeft() const { return left; }
144
+ const std::vector<float>& keptRight() const { return right; }
145
+ Vst::IEditController* editController() { return controller; }
146
+ ParamChanges& reported() { return outChanges; }
147
+
148
+ private:
149
+ // State first, then the wire: the controller mirrors the component, so it needs the state before its parameters mean anything.
150
+ void openController (VST3::Hosting::PluginFactory& factory) {
151
+ if (component->queryInterface (Vst::IEditController::iid, (void**) &controller) != kResultOk) {
152
+ TUID id;
153
+ if (component->getControllerClassId (id) == kResultOk) controller = factory.createInstance<Vst::IEditController> (VST3::UID (id)).take();
154
+ if (controller) controller->initialize (host);
155
+ }
156
+ if (! controller) return;
157
+ wireHalves();
158
+ MemoryStream state;
159
+ if (component->getState (&state) != kResultOk) return;
160
+ state.seek (0, IBStream::kIBSeekSet, nullptr);
161
+ controller->setComponentState (&state);
162
+ }
163
+
164
+ void wireHalves() {
165
+ Vst::IConnectionPoint* fromComponent = nullptr;
166
+ Vst::IConnectionPoint* fromController = nullptr;
167
+ component->queryInterface (Vst::IConnectionPoint::iid, (void**) &fromComponent);
168
+ controller->queryInterface (Vst::IConnectionPoint::iid, (void**) &fromController);
169
+ if (! fromComponent || ! fromController) return;
170
+ fromComponent->connect (new ConnectionProxy (fromController));
171
+ fromController->connect (new ConnectionProxy (fromComponent));
172
+ }
173
+
174
+ void activateBuses() {
175
+ const int32 outputs = component->getBusCount (Vst::kAudio, Vst::kOutput);
176
+ const int32 eventBuses = component->getBusCount (Vst::kEvent, Vst::kInput);
177
+ std::printf (" buses: %d audio out, %d event in\n", outputs, eventBuses);
178
+ for (int32 bus = 0; bus < outputs; ++bus) component->activateBus (Vst::kAudio, Vst::kOutput, bus, true);
179
+ for (int32 bus = 0; bus < eventBuses; ++bus) component->activateBus (Vst::kEvent, Vst::kInput, bus, true);
180
+ if (component->getBusCount (Vst::kAudio, Vst::kInput) > 0) component->activateBus (Vst::kAudio, Vst::kInput, 0, true);
181
+ }
182
+
183
+ void prepareBuffers() {
184
+ const auto outputs = (size_t) std::max (1, component->getBusCount (Vst::kAudio, Vst::kOutput));
185
+ buffers.assign ((outputs + 1) * 2, std::vector<float> ((size_t) block, 0.0f)); // the last pair is the silent input
186
+ pointers.resize (outputs + 1);
187
+ busBuffers.assign (outputs + 1, Vst::AudioBusBuffers {});
188
+ for (size_t bus = 0; bus <= outputs; ++bus) {
189
+ pointers[bus] = { buffers[bus * 2].data(), buffers[bus * 2 + 1].data() };
190
+ busBuffers[bus].numChannels = 2;
191
+ busBuffers[bus].channelBuffers32 = pointers[bus].data();
192
+ }
193
+ context.state = Vst::ProcessContext::kPlaying | Vst::ProcessContext::kTempoValid;
194
+ context.sampleRate = sampleRate;
195
+ context.tempo = 120.0;
196
+ data.processMode = Vst::kOffline;
197
+ data.symbolicSampleSize = Vst::kSample32;
198
+ data.numInputs = component->getBusCount (Vst::kAudio, Vst::kInput) > 0 ? 1 : 0;
199
+ data.numOutputs = (int32) outputs;
200
+ data.inputs = data.numInputs ? &busBuffers[outputs] : nullptr;
201
+ data.outputs = busBuffers.data();
202
+ data.inputEvents = &events;
203
+ data.inputParameterChanges = &changes;
204
+ data.outputParameterChanges = &outChanges; // left null, a change a plugin's own sysex handler made stays invisible
205
+ data.processContext = &context;
206
+ }
207
+
208
+ void processBlock (int32 count) {
209
+ data.numSamples = count;
210
+ std::fill (buffers[0].begin(), buffers[0].end(), 0.0f);
211
+ std::fill (buffers[1].begin(), buffers[1].end(), 0.0f);
212
+ context.projectTimeSamples = position;
213
+ processor->process (data);
214
+ events.clear();
215
+ changes.clear();
216
+ kept.clear();
217
+ position += count;
218
+ }
219
+
220
+ double sampleRate;
221
+ int block;
222
+ VST3::Hosting::Module::Ptr module;
223
+ Vst::HostApplication* host = nullptr;
224
+ Vst::IComponent* component = nullptr;
225
+ Vst::IAudioProcessor* processor = nullptr;
226
+ Vst::IEditController* controller = nullptr;
227
+ std::vector<std::vector<float>> buffers;
228
+ std::vector<std::array<float*, 2>> pointers;
229
+ std::vector<Vst::AudioBusBuffers> busBuffers;
230
+ Vst::ProcessContext context {};
231
+ Vst::ProcessData data {};
232
+ EventList events;
233
+ ParamChanges changes;
234
+ ParamChanges outChanges;
235
+ std::deque<std::vector<unsigned char>> kept;
236
+ std::vector<float> left;
237
+ std::vector<float> right;
238
+ int64 position = 0;
239
+ };
240
+
241
+ } // namespace vst3host