@soundor/vst3-host 0.0.0-stage → 0.1.1

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 (54) hide show
  1. package/CMakeLists.txt +194 -0
  2. package/CMakePresets.json +115 -0
  3. package/LICENSE +21 -0
  4. package/NOTICE.md +17 -0
  5. package/README.md +40 -2
  6. package/cmake/Vst3HostDependencies.cmake +179 -0
  7. package/dist/cli.d.mts +1 -0
  8. package/dist/cli.mjs +88 -0
  9. package/dist/cli.mjs.map +1 -0
  10. package/dist/index.d.mts +586 -0
  11. package/dist/index.d.mts.map +1 -0
  12. package/dist/index.mjs +1 -0
  13. package/dist/prepare-ayjTVhfk.mjs +1149 -0
  14. package/dist/prepare-ayjTVhfk.mjs.map +1 -0
  15. package/native/src/Bundle.cpp +253 -0
  16. package/native/src/Bundle.h +32 -0
  17. package/native/src/EditorArea.h +92 -0
  18. package/native/src/EditorAreaMac.mm +52 -0
  19. package/native/src/EditorAreaWindows.cpp +66 -0
  20. package/native/src/EditorAreaX11.cpp +512 -0
  21. package/native/src/EditorWindow.h +72 -0
  22. package/native/src/EditorWindowMac.mm +134 -0
  23. package/native/src/EditorWindowWindows.cpp +148 -0
  24. package/native/src/EventLoop.h +50 -0
  25. package/native/src/EventLoopLinux.cpp +204 -0
  26. package/native/src/EventLoopMac.mm +146 -0
  27. package/native/src/EventLoopWindows.cpp +132 -0
  28. package/native/src/HostContext.cpp +217 -0
  29. package/native/src/HostContext.h +116 -0
  30. package/native/src/HostInterface.cpp +1026 -0
  31. package/native/src/HostInterface.h +138 -0
  32. package/native/src/HostWindow.cpp +901 -0
  33. package/native/src/HostWindow.h +187 -0
  34. package/native/src/Ipc.cpp +158 -0
  35. package/native/src/Ipc.h +91 -0
  36. package/native/src/Json.cpp +426 -0
  37. package/native/src/Json.h +85 -0
  38. package/native/src/LiveAudio.cpp +416 -0
  39. package/native/src/LiveAudio.h +121 -0
  40. package/native/src/Main.cpp +178 -0
  41. package/native/src/Paths.h +20 -0
  42. package/native/src/Plugin.cpp +1865 -0
  43. package/native/src/Plugin.h +321 -0
  44. package/native/src/Session.cpp +656 -0
  45. package/native/src/Session.h +82 -0
  46. package/native/src/Settings.cpp +93 -0
  47. package/native/src/Settings.h +23 -0
  48. package/native/src/Version.h.in +11 -0
  49. package/native/src/X11Library.cpp +127 -0
  50. package/native/src/X11Library.h +83 -0
  51. package/native/tools/Embed.cpp +49 -0
  52. package/package.json +58 -3
  53. package/protocol.json +7 -0
  54. package/targets.json +33 -0
@@ -0,0 +1,187 @@
1
+ // An SDL window drawing the host interface (HostInterface) around a native view
2
+ // the editor attaches to (EditorArea).
3
+ //
4
+ // In the `host` presentation the editor's area always has the editor's size:
5
+ // resizing or maximizing the window, or opening or closing the panel, changes
6
+ // the stage around it, never the editor, whether or not the editor can be
7
+ // resized. As in the web host, its grip and its own requests can use the stage,
8
+ // and the window never grows for them. The window's minimum keeps room for the
9
+ // editor.
10
+ //
11
+ // On Linux, the `editor` presentation is a bare window that is the editor. Its
12
+ // border asks the editor for each size, and the editor takes the answer at once
13
+ // (VST3 has no call for a shape, so an edge that the answer refuses is asked
14
+ // again, with the other side left to the editor). The window manager owns the
15
+ // window through a drag: the window takes the editor's size once it has stopped.
16
+ //
17
+ // A change of the editor's geometry is presented as one, as a browser's are:
18
+ // where the platform needs it, the window draws the editor's images itself
19
+ // until the change is over (EditorArea's live presentation), and a window
20
+ // manager waits for the frame of a size it asks for (_NET_WM_SYNC_REQUEST).
21
+ // The window and the interface follow the user every frame, with the editor's
22
+ // last image stretched to its new size; the editor is asked for one size at a
23
+ // time, and for another only once it has painted the last.
24
+ //
25
+ // The interface is presented when its drawing changes, never in a loop: a
26
+ // frame is built on input, at the monitor refresh rate while audio plays, and every 100 ms
27
+ // otherwise (values change without input too), and presented only if it
28
+ // differs from the last one.
29
+ #pragma once
30
+
31
+ #include "EditorArea.h"
32
+ #include "EditorWindow.h"
33
+ #include "HostInterface.h"
34
+
35
+ #include <array>
36
+ #include <chrono>
37
+ #include <cstdint>
38
+ #include <memory>
39
+
40
+ struct SDL_Renderer;
41
+ struct SDL_Texture;
42
+ struct SDL_Window;
43
+ union SDL_Event;
44
+ struct ImGuiContext;
45
+
46
+ namespace soundor::vst3host
47
+ {
48
+ class HostWindow final : public EditorWindow
49
+ {
50
+ public:
51
+ /** nullptr and `error` when no window can be made (no display). A bare
52
+ window has no interface: the editor is all of it, its border resizes
53
+ the editor, and it cannot be maximized or minimized. */
54
+ static std::unique_ptr<EditorWindow> create(EventLoop& loop, HostModel& model, const std::string& title,
55
+ int width, int height, bool bare, Callbacks callbacks,
56
+ std::string& error);
57
+
58
+ HostWindow(EventLoop& loop, HostModel& model, bool bare, Callbacks callbacks);
59
+ ~HostWindow() override;
60
+ HostWindow(const HostWindow&) = delete;
61
+ HostWindow& operator=(const HostWindow&) = delete;
62
+
63
+ [[nodiscard]] const char* platformType() const override { return area_->platformType(); }
64
+ [[nodiscard]] void* parentHandle() const override { return area_->handle(); }
65
+ void setResizable(bool resizable) override;
66
+ void setContentSize(int width, int height) override;
67
+ void constrainContentSize(int width, int height) override;
68
+ void setEditorMinimum(int width, int height) override;
69
+ void setEditorMaximum(int width, int height) override;
70
+ [[nodiscard]] std::optional<std::pair<int, int>> editorRoom() const override;
71
+ void show() override;
72
+ [[nodiscard]] double scale() const override;
73
+ [[nodiscard]] const char* presentation() const override { return bare_ ? "editor" : "host"; }
74
+ [[nodiscard]] std::pair<int, int> windowSize() const override;
75
+
76
+ /** Tests check that an idle window draws nothing. */
77
+ [[nodiscard]] std::optional<uint64_t> interfaceFrames() const override { return presented_; }
78
+
79
+ private:
80
+ bool open(const std::string& title, int width, int height, std::string& error);
81
+ void pump();
82
+ void handle(const SDL_Event& event);
83
+ /** The window's natural size for the editor and the panel. */
84
+ [[nodiscard]] std::pair<int, int> naturalSize() const;
85
+ /** What the editor, the dock and the panel take, however short the
86
+ panel scrolls. */
87
+ [[nodiscard]] std::pair<int, int> requiredSize() const;
88
+ void windowResized();
89
+ /** Maximized or restored since the last frame: the editor follows. */
90
+ /** The window for the editor and the panel, keeping the space the
91
+ user made: `extraWidth` more of it for a panel that opened. */
92
+ void fitWindow(int extraWidth = 0);
93
+ void applyLimits();
94
+ void layout();
95
+ void askWindowSize(int width, int height);
96
+ /** A bare window's border has changed the editor it is. */
97
+ void bareWindowResized();
98
+ /** Gives a bare window the editor's size after a drag has stopped. */
99
+ void settleWindow(std::chrono::steady_clock::time_point now);
100
+ /** What the interface takes around the editor: nothing in a bare window. */
101
+ [[nodiscard]] float paddingSpace() const;
102
+ [[nodiscard]] float dockHeight() const;
103
+ [[nodiscard]] float dockSpan() const;
104
+ /** What the panel takes from the stage now. */
105
+ [[nodiscard]] float panelSpace() const;
106
+ void applyScale();
107
+ void applyRefreshRate();
108
+ void invalidate(int frames = 2) { pending_ = std::max(pending_, frames); }
109
+ void render();
110
+ /** The editor's geometry is changing: presents it live until it stops. */
111
+ void touchLive();
112
+ void endLive();
113
+ /** The editor's size for the window's stage. */
114
+ [[nodiscard]] std::pair<int, int> stageEditorSize() const;
115
+ void updateLiveImage();
116
+
117
+ EventLoop& loop_;
118
+ HostModel& model_;
119
+ Callbacks callbacks_;
120
+ SDL_Window* window_ = nullptr;
121
+ SDL_Renderer* renderer_ = nullptr;
122
+ ImGuiContext* context_ = nullptr;
123
+ std::unique_ptr<EditorArea> area_;
124
+ std::unique_ptr<HostInterface> interface_;
125
+ EventLoop::Id timer_ = 0;
126
+
127
+ int editorWidth_ = 0;
128
+ int editorHeight_ = 0;
129
+ std::array<int, 4> editorFrame_ {};
130
+ bool resizable_ = false;
131
+ bool shown_ = false;
132
+ bool live_ = false;
133
+ std::chrono::steady_clock::time_point liveTouched_ {};
134
+ SDL_Texture* liveTexture_ = nullptr;
135
+ int liveWidth_ = 0;
136
+ int liveHeight_ = 0;
137
+ /** The part of the image that holds the editor's pixels, and where it
138
+ goes in the window; none for a whole image, which fills the editor. */
139
+ struct LivePart
140
+ {
141
+ float x, y, width, height, u0, v0, u1, v1;
142
+ };
143
+ std::optional<LivePart> livePart_;
144
+ bool livePartial_ = false;
145
+ bool viewHidden_ = false;
146
+ /** The image to draw is an older one, not what the view shows. */
147
+ bool hideViewFirst_ = false;
148
+ std::chrono::steady_clock::time_point windowResizedAt_ {};
149
+ int minimumWidth_ = 1;
150
+ int minimumHeight_ = 1;
151
+ /** The panel the stage leaves room for: panelOpen_ once the window has
152
+ taken its width. */
153
+ bool stagePanelOpen_ = false;
154
+ std::chrono::steady_clock::time_point stageFollowsAt_ {};
155
+ /** The user's newest grip drag, for the editor to take when it can. */
156
+ std::optional<std::pair<int, int>> gripSize_;
157
+ bool bare_ = false;
158
+ int maximumWidth_ = 0;
159
+ int maximumHeight_ = 0;
160
+ /** A drag has changed a bare window: it takes the editor's size once
161
+ the drag has stopped. */
162
+ bool settling_ = false;
163
+ int settlingAsks_ = 0;
164
+ /** The bare window's size at the last step of a drag. */
165
+ std::pair<int, int> draggedSize_ { 0, 0 };
166
+ /** A size a bare window has asked for and not yet got. */
167
+ std::optional<std::pair<int, int>> askedSize_;
168
+ std::chrono::steady_clock::time_point askedUntil_ {};
169
+ bool panelOpen_ = false;
170
+ float scale_ = 1;
171
+ /** The natural size the window was last fitted to. */
172
+ std::pair<int, int> fitted_ { 0, 0 };
173
+ /** The window's limits, as last set. */
174
+ std::pair<int, int> limits_ { 0, 0 };
175
+ int pending_ = 0;
176
+ bool forcePresent_ = true;
177
+ uint64_t lastHash_ = 0;
178
+ uint64_t presented_ = 0;
179
+ std::chrono::steady_clock::time_point lastFrame_ {};
180
+ std::chrono::steady_clock::time_point nextFrame_ {};
181
+ std::chrono::steady_clock::duration frameInterval_ {};
182
+ bool closing_ = false;
183
+ /** False once this window is gone. On macOS, SDL's wait for events runs
184
+ the main queue's work, which can close the window it is waiting for. */
185
+ std::shared_ptr<bool> alive_ = std::make_shared<bool>(true);
186
+ };
187
+ } // namespace soundor::vst3host
@@ -0,0 +1,158 @@
1
+ #include "Ipc.h"
2
+
3
+ #include <algorithm>
4
+ #include <array>
5
+ #include <cstring>
6
+
7
+ #ifdef _WIN32
8
+ #include <windows.h>
9
+ #else
10
+ #include <unistd.h>
11
+
12
+ #include <cerrno>
13
+ #endif
14
+
15
+ namespace soundor::vst3host
16
+ {
17
+ void Payload::resize(size_t size)
18
+ {
19
+ storage_.assign((size + sizeof(double) - 1) / sizeof(double), 0.0);
20
+ size_ = size;
21
+ }
22
+
23
+ namespace
24
+ {
25
+ uint32_t readU32(const uint8_t* bytes)
26
+ {
27
+ return static_cast<uint32_t>(bytes[0]) | (static_cast<uint32_t>(bytes[1]) << 8U)
28
+ | (static_cast<uint32_t>(bytes[2]) << 16U) | (static_cast<uint32_t>(bytes[3]) << 24U);
29
+ }
30
+
31
+ void writeU32(uint8_t* bytes, uint32_t value)
32
+ {
33
+ bytes[0] = static_cast<uint8_t>(value);
34
+ bytes[1] = static_cast<uint8_t>(value >> 8U);
35
+ bytes[2] = static_cast<uint8_t>(value >> 16U);
36
+ bytes[3] = static_cast<uint8_t>(value >> 24U);
37
+ }
38
+ } // namespace
39
+
40
+ bool FrameReader::readExact(uint8_t* data, size_t size, bool& end) const
41
+ {
42
+ size_t done = 0;
43
+ while (done < size)
44
+ {
45
+ #ifdef _WIN32
46
+ DWORD count = 0;
47
+ const DWORD chunk = static_cast<DWORD>(std::min<size_t>(size - done, 1U << 30U));
48
+ if (! ReadFile(reinterpret_cast<HANDLE>(handle_), data + done, chunk, &count, nullptr) || count == 0)
49
+ {
50
+ end = done == 0;
51
+ return false;
52
+ }
53
+ #else
54
+ const ssize_t count = ::read(static_cast<int>(handle_), data + done, size - done);
55
+ if (count < 0 && errno == EINTR)
56
+ continue;
57
+ if (count <= 0)
58
+ {
59
+ end = done == 0;
60
+ return false;
61
+ }
62
+ #endif
63
+ done += static_cast<size_t>(count);
64
+ }
65
+ return true;
66
+ }
67
+
68
+ FrameReader::Status FrameReader::read(Frame& frame, std::string& error)
69
+ {
70
+ std::array<uint8_t, frameHeaderBytes> prefix {};
71
+ bool end = false;
72
+ if (! readExact(prefix.data(), prefix.size(), end))
73
+ {
74
+ if (end)
75
+ return Status::End;
76
+ error = "the request stream ended inside a frame header";
77
+ return Status::Error;
78
+ }
79
+ if (readU32(prefix.data()) != frameMagic)
80
+ {
81
+ error = "the request stream is not framed (bad magic)";
82
+ return Status::Error;
83
+ }
84
+ const uint32_t headerSize = readU32(prefix.data() + 4);
85
+ const uint32_t payloadSize = readU32(prefix.data() + 8);
86
+ if (headerSize > maxHeader_)
87
+ {
88
+ error = "a request header of " + std::to_string(headerSize) + " bytes exceeds the limit of "
89
+ + std::to_string(maxHeader_);
90
+ return Status::Error;
91
+ }
92
+ if (payloadSize > maxPayload_)
93
+ {
94
+ error = "a request payload of " + std::to_string(payloadSize) + " bytes exceeds the limit of "
95
+ + std::to_string(maxPayload_);
96
+ return Status::Error;
97
+ }
98
+ std::string header(headerSize, '\0');
99
+ if (headerSize > 0 && ! readExact(reinterpret_cast<uint8_t*>(header.data()), headerSize, end))
100
+ {
101
+ error = "the request stream ended inside a frame";
102
+ return Status::Error;
103
+ }
104
+ frame.payload.resize(payloadSize);
105
+ if (payloadSize > 0 && ! readExact(frame.payload.data(), payloadSize, end))
106
+ {
107
+ error = "the request stream ended inside a frame";
108
+ return Status::Error;
109
+ }
110
+ try
111
+ {
112
+ frame.header = Json::parse(header);
113
+ }
114
+ catch (const JsonError&)
115
+ {
116
+ frame.header = Json();
117
+ }
118
+ return Status::Frame;
119
+ }
120
+
121
+ bool FrameWriter::writeAll(const uint8_t* data, size_t size) const
122
+ {
123
+ size_t done = 0;
124
+ while (done < size)
125
+ {
126
+ #ifdef _WIN32
127
+ DWORD count = 0;
128
+ const DWORD chunk = static_cast<DWORD>(std::min<size_t>(size - done, 1U << 30U));
129
+ if (! WriteFile(reinterpret_cast<HANDLE>(handle_), data + done, chunk, &count, nullptr))
130
+ return false;
131
+ #else
132
+ const ssize_t count = ::write(static_cast<int>(handle_), data + done, size - done);
133
+ if (count < 0 && errno == EINTR)
134
+ continue;
135
+ if (count <= 0)
136
+ return false;
137
+ #endif
138
+ done += static_cast<size_t>(count);
139
+ }
140
+ return true;
141
+ }
142
+
143
+ bool FrameWriter::write(const Json& header, std::span<const uint8_t> payload)
144
+ {
145
+ const std::string text = header.dump();
146
+ std::array<uint8_t, frameHeaderBytes> prefix {};
147
+ writeU32(prefix.data(), frameMagic);
148
+ writeU32(prefix.data() + 4, static_cast<uint32_t>(text.size()));
149
+ writeU32(prefix.data() + 8, static_cast<uint32_t>(payload.size()));
150
+ const std::scoped_lock lock(mutex_);
151
+ if (broken_)
152
+ return false;
153
+ broken_ = ! writeAll(prefix.data(), prefix.size())
154
+ || ! writeAll(reinterpret_cast<const uint8_t*>(text.data()), text.size())
155
+ || ! writeAll(payload.data(), payload.size());
156
+ return ! broken_;
157
+ }
158
+ } // namespace soundor::vst3host
@@ -0,0 +1,91 @@
1
+ // Framing of the host's protocol (README, "Protocol"). Each frame is
2
+ //
3
+ // magic "SVH1" | u32 LE header length | u32 LE payload length | header | payload
4
+ //
5
+ // The header is a UTF-8 JSON object; the payload is raw bytes (audio as
6
+ // little-endian float32, plugin state). Requests arrive on stdin, responses
7
+ // and events leave on the process's original stdout, which Main.cpp moves
8
+ // out of the way of plugins writing to fd 1.
9
+ #pragma once
10
+
11
+ #include "Json.h"
12
+
13
+ #include <cstdint>
14
+ #include <memory>
15
+ #include <mutex>
16
+ #include <span>
17
+ #include <string>
18
+ #include <vector>
19
+
20
+ namespace soundor::vst3host
21
+ {
22
+ inline constexpr uint32_t frameMagic = 0x31485653; // "SVH1", little-endian
23
+ inline constexpr size_t frameHeaderBytes = 12;
24
+
25
+ /** A byte buffer whose data is aligned for float and double access. */
26
+ class Payload
27
+ {
28
+ public:
29
+ Payload() = default;
30
+ explicit Payload(size_t size) { resize(size); }
31
+ void resize(size_t size);
32
+ uint8_t* data() { return storage_.empty() ? nullptr : reinterpret_cast<uint8_t*>(storage_.data()); }
33
+ const uint8_t* data() const
34
+ {
35
+ return storage_.empty() ? nullptr : reinterpret_cast<const uint8_t*>(storage_.data());
36
+ }
37
+ size_t size() const { return size_; }
38
+
39
+ private:
40
+ std::vector<double> storage_;
41
+ size_t size_ = 0;
42
+ };
43
+
44
+ struct Frame
45
+ {
46
+ Json header;
47
+ Payload payload;
48
+ };
49
+
50
+ /** A native file handle: an fd on POSIX, a HANDLE on Windows. */
51
+ using NativeHandle = intptr_t;
52
+
53
+ class FrameReader
54
+ {
55
+ public:
56
+ enum class Status
57
+ {
58
+ Frame,
59
+ End,
60
+ Error
61
+ };
62
+ FrameReader(NativeHandle handle, size_t maxHeader, size_t maxPayload)
63
+ : handle_(handle), maxHeader_(maxHeader), maxPayload_(maxPayload)
64
+ {
65
+ }
66
+ /** Blocks for the next frame. On Error, `error` says why; the stream
67
+ cannot be resynchronised afterwards. A header that is not valid
68
+ JSON still returns a Frame whose header is null. */
69
+ Status read(Frame& frame, std::string& error);
70
+
71
+ private:
72
+ bool readExact(uint8_t* data, size_t size, bool& end) const;
73
+ NativeHandle handle_;
74
+ size_t maxHeader_;
75
+ size_t maxPayload_;
76
+ };
77
+
78
+ class FrameWriter
79
+ {
80
+ public:
81
+ explicit FrameWriter(NativeHandle handle) : handle_(handle) {}
82
+ /** Thread-safe; false once the reader has gone. */
83
+ bool write(const Json& header, std::span<const uint8_t> payload = {});
84
+
85
+ private:
86
+ bool writeAll(const uint8_t* data, size_t size) const;
87
+ std::mutex mutex_;
88
+ NativeHandle handle_;
89
+ bool broken_ = false;
90
+ };
91
+ } // namespace soundor::vst3host