@soundor/vst3-host 0.0.0-stage → 0.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 (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,72 @@
1
+ // The native window an editor (IPlugView) is attached to: an X11 window on
2
+ // Linux, an NSWindow on macOS and an HWND on Windows. Sizes are in the
3
+ // view's coordinates: physical pixels on Linux and Windows, points on macOS
4
+ // (IPlugView's convention).
5
+ #pragma once
6
+
7
+ #include "EventLoop.h"
8
+
9
+ #include <cstdint>
10
+ #include <functional>
11
+ #include <memory>
12
+ #include <optional>
13
+ #include <string>
14
+ #include <utility>
15
+
16
+ namespace soundor::vst3host
17
+ {
18
+ class EditorWindow
19
+ {
20
+ public:
21
+ struct Callbacks
22
+ {
23
+ /** The user resized the window; the editor gets the new size. */
24
+ std::function<void(int width, int height)> resized;
25
+ /** The user closed the window. */
26
+ std::function<void()> closed;
27
+ std::function<void(bool focused)> focusChanged;
28
+ /** Windows and Linux: the monitor's scale changed. */
29
+ std::function<void(double scale)> scaleChanged;
30
+ };
31
+
32
+ virtual ~EditorWindow() = default;
33
+
34
+ /** A window that is the editor alone (the `editor` presentation) on
35
+ macOS and Windows; nullptr and `error` when none can be made. The
36
+ host interface's window, and Linux's in either presentation, is a
37
+ HostWindow. */
38
+ static std::unique_ptr<EditorWindow> create(EventLoop& loop, const std::string& title, int width, int height,
39
+ Callbacks callbacks, std::string& error);
40
+
41
+ /** The IPlugView platform type and the parent handle to attach to. */
42
+ [[nodiscard]] virtual const char* platformType() const = 0;
43
+ [[nodiscard]] virtual void* parentHandle() const = 0;
44
+
45
+ virtual void setResizable(bool resizable) = 0;
46
+ /** Resizes the editor's area to the editor's size, without calling
47
+ `resized`. */
48
+ virtual void setContentSize(int width, int height) = 0;
49
+ /** The smallest size the editor keeps to: the window need not be
50
+ smaller than what that takes. */
51
+ virtual void setEditorMinimum(int width, int height) { (void)width, (void)height; }
52
+ /** The largest editor there is: a window that is the editor need not
53
+ be larger. */
54
+ virtual void setEditorMaximum(int width, int height) { (void)width, (void)height; }
55
+ /** The largest editor the window has room for, where it is not to
56
+ grow for one (the host presentation, as in the web host): at least
57
+ the editor's size. */
58
+ [[nodiscard]] virtual std::optional<std::pair<int, int>> editorRoom() const { return std::nullopt; }
59
+ /** The editor's limits corrected a size from `resized`. */
60
+ virtual void constrainContentSize(int width, int height) { setContentSize(width, height); }
61
+ virtual void show() = 0;
62
+ /** The platform's scale for this window (1 on macOS, whose views are
63
+ in points). */
64
+ [[nodiscard]] virtual double scale() const = 0;
65
+ /** "editor" (the window is the editor) or "host" (with the host interface). */
66
+ [[nodiscard]] virtual const char* presentation() const { return "editor"; }
67
+ /** The window's content size, in the editor's coordinates. */
68
+ [[nodiscard]] virtual std::pair<int, int> windowSize() const = 0;
69
+ /** The host interface's frames presented so far; none without it. */
70
+ [[nodiscard]] virtual std::optional<uint64_t> interfaceFrames() const { return std::nullopt; }
71
+ };
72
+ } // namespace soundor::vst3host
@@ -0,0 +1,134 @@
1
+ // An NSWindow whose content view hosts the editor. Notifications arrive
2
+ // through block observers: the host registers no Objective-C classes.
3
+ #include "EditorWindow.h"
4
+
5
+ #import <Cocoa/Cocoa.h>
6
+
7
+ #include <vector>
8
+
9
+ namespace soundor::vst3host
10
+ {
11
+ namespace
12
+ {
13
+ class MacEditorWindow final : public EditorWindow
14
+ {
15
+ public:
16
+ MacEditorWindow(EventLoop& loop, const std::string& title, int width, int height, Callbacks callbacks)
17
+ : loop_(loop), callbacks_(std::move(callbacks))
18
+ {
19
+ const NSRect frame = NSMakeRect(0, 0, width, height);
20
+ window_ = [[NSWindow alloc]
21
+ initWithContentRect:frame
22
+ styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable
23
+ backing:NSBackingStoreBuffered
24
+ defer:NO];
25
+ [window_ setReleasedWhenClosed:NO];
26
+ // It shows the editor at the editor's size: not maximized or
27
+ // minimized.
28
+ [window_ setCollectionBehavior:NSWindowCollectionBehaviorFullScreenNone];
29
+ [[window_ standardWindowButton:NSWindowZoomButton] setEnabled:NO];
30
+ [window_ setTitle:[NSString stringWithUTF8String:title.c_str()]];
31
+ content_ = [[NSView alloc] initWithFrame:frame];
32
+ [window_ setContentView:content_];
33
+ [window_ center];
34
+
35
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
36
+ auto observe = [&](NSNotificationName name, void (^block)(NSNotification*)) {
37
+ observers_.push_back([[center addObserverForName:name object:window_ queue:nil
38
+ usingBlock:block] retain]);
39
+ };
40
+ observe(NSWindowDidResizeNotification, ^(NSNotification*) {
41
+ if (ignoreResize_ || ! callbacks_.resized)
42
+ return;
43
+ const NSSize size = [content_ frame].size;
44
+ callbacks_.resized(static_cast<int>(size.width), static_cast<int>(size.height));
45
+ });
46
+ observe(NSWindowDidBecomeKeyNotification, ^(NSNotification*) {
47
+ if (callbacks_.focusChanged)
48
+ callbacks_.focusChanged(true);
49
+ });
50
+ observe(NSWindowDidResignKeyNotification, ^(NSNotification*) {
51
+ if (callbacks_.focusChanged)
52
+ callbacks_.focusChanged(false);
53
+ });
54
+ observe(NSWindowWillCloseNotification, ^(NSNotification*) {
55
+ if (closing_ || ! callbacks_.closed)
56
+ return;
57
+ // The callback destroys this window.
58
+ loop_.post(callbacks_.closed);
59
+ });
60
+ }
61
+
62
+ ~MacEditorWindow() override
63
+ {
64
+ closing_ = true;
65
+ for (id observer : observers_)
66
+ {
67
+ [[NSNotificationCenter defaultCenter] removeObserver:observer];
68
+ [observer release];
69
+ }
70
+ [window_ orderOut:nil];
71
+ [window_ close];
72
+ [content_ release];
73
+ [window_ release];
74
+ }
75
+
76
+ [[nodiscard]] const char* platformType() const override { return "NSView"; }
77
+ [[nodiscard]] void* parentHandle() const override { return static_cast<void*>(content_); }
78
+
79
+ void setResizable(bool resizable) override
80
+ {
81
+ NSWindowStyleMask mask = [window_ styleMask];
82
+ mask = resizable ? (mask | NSWindowStyleMaskResizable) : (mask & ~NSWindowStyleMaskResizable);
83
+ [window_ setStyleMask:mask];
84
+ [[window_ standardWindowButton:NSWindowZoomButton] setEnabled:NO];
85
+ }
86
+
87
+ void setContentSize(int width, int height) override
88
+ {
89
+ if (windowSize() == std::pair(width, height))
90
+ return;
91
+ ignoreResize_ = true;
92
+ [window_ setContentSize:NSMakeSize(width, height)];
93
+ ignoreResize_ = false;
94
+ }
95
+
96
+ void show() override
97
+ {
98
+ [window_ makeKeyAndOrderFront:nil];
99
+ #pragma clang diagnostic push
100
+ #pragma clang diagnostic ignored "-Wdeprecated-declarations"
101
+ // Deprecated in macOS 14 for -activate, which 12 and 13 lack.
102
+ [NSApp activateIgnoringOtherApps:YES];
103
+ #pragma clang diagnostic pop
104
+ }
105
+
106
+ [[nodiscard]] double scale() const override { return 1.0; }
107
+ [[nodiscard]] std::pair<int, int> windowSize() const override
108
+ {
109
+ const NSSize size = [content_ frame].size;
110
+ return { static_cast<int>(size.width), static_cast<int>(size.height) };
111
+ }
112
+
113
+ private:
114
+ EventLoop& loop_;
115
+ Callbacks callbacks_;
116
+ NSWindow* window_ = nil;
117
+ NSView* content_ = nil;
118
+ std::vector<id> observers_;
119
+ bool ignoreResize_ = false;
120
+ bool closing_ = false;
121
+ };
122
+ } // namespace
123
+
124
+ std::unique_ptr<EditorWindow> EditorWindow::create(EventLoop& loop, const std::string& title, int width, int height,
125
+ Callbacks callbacks, std::string& error)
126
+ {
127
+ if (NSApp == nil)
128
+ {
129
+ error = "the editor needs UI mode";
130
+ return nullptr;
131
+ }
132
+ return std::make_unique<MacEditorWindow>(loop, title, width, height, std::move(callbacks));
133
+ }
134
+ } // namespace soundor::vst3host
@@ -0,0 +1,148 @@
1
+ #include "EditorWindow.h"
2
+
3
+ #ifndef NOMINMAX
4
+ #define NOMINMAX
5
+ #endif
6
+ #include <windows.h>
7
+
8
+ #include <algorithm>
9
+ #include <string>
10
+
11
+ namespace soundor::vst3host
12
+ {
13
+ namespace
14
+ {
15
+ std::wstring widen(const std::string& text)
16
+ {
17
+ const int size = MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, nullptr, 0);
18
+ std::wstring result(static_cast<size_t>(std::max(size, 1)), L'\0');
19
+ MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, result.data(), size);
20
+ result.resize(static_cast<size_t>(std::max(size - 1, 0)));
21
+ return result;
22
+ }
23
+
24
+ // It shows the editor at the editor's size: not maximized or minimized.
25
+ constexpr DWORD resizableStyle = (WS_OVERLAPPEDWINDOW & ~(WS_MINIMIZEBOX | WS_MAXIMIZEBOX)) | WS_CLIPCHILDREN;
26
+ constexpr DWORD fixedStyle = resizableStyle & ~WS_THICKFRAME;
27
+
28
+ class WindowsEditorWindow final : public EditorWindow
29
+ {
30
+ public:
31
+ WindowsEditorWindow(EventLoop& loop, const std::string& title, int width, int height, Callbacks callbacks)
32
+ : loop_(loop), callbacks_(std::move(callbacks))
33
+ {
34
+ static const ATOM windowClass = []
35
+ {
36
+ WNDCLASSEXW description {};
37
+ description.cbSize = sizeof description;
38
+ description.style = CS_HREDRAW | CS_VREDRAW;
39
+ description.lpfnWndProc = &WindowsEditorWindow::windowProc;
40
+ description.hInstance = GetModuleHandleW(nullptr);
41
+ description.hCursor = LoadCursorW(nullptr, IDC_ARROW);
42
+ description.hbrBackground = static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH));
43
+ description.lpszClassName = L"SoundorVst3HostEditor";
44
+ return RegisterClassExW(&description);
45
+ }();
46
+ window_ =
47
+ CreateWindowExW(0, MAKEINTATOM(windowClass), widen(title).c_str(), resizableStyle, CW_USEDEFAULT,
48
+ CW_USEDEFAULT, width, height, nullptr, nullptr, GetModuleHandleW(nullptr), nullptr);
49
+ SetWindowLongPtrW(window_, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
50
+ setContentSize(width, height);
51
+ }
52
+
53
+ ~WindowsEditorWindow() override
54
+ {
55
+ SetWindowLongPtrW(window_, GWLP_USERDATA, 0);
56
+ DestroyWindow(window_);
57
+ }
58
+
59
+ [[nodiscard]] const char* platformType() const override { return "HWND"; }
60
+ [[nodiscard]] void* parentHandle() const override { return window_; }
61
+
62
+ void setResizable(bool resizable) override
63
+ {
64
+ resizable_ = resizable;
65
+ SetWindowLongPtrW(window_, GWL_STYLE,
66
+ static_cast<LONG_PTR>(resizable ? resizableStyle : fixedStyle)
67
+ | (IsWindowVisible(window_) ? WS_VISIBLE : 0));
68
+ RECT client {};
69
+ GetClientRect(window_, &client);
70
+ setContentSize(client.right - client.left, client.bottom - client.top);
71
+ }
72
+
73
+ void setContentSize(int width, int height) override
74
+ {
75
+ RECT rect { 0, 0, width, height };
76
+ const DWORD style = resizable_ ? resizableStyle : fixedStyle;
77
+ AdjustWindowRectExForDpi(&rect, style, FALSE, 0, GetDpiForWindow(window_));
78
+ ignoreResize_ = true;
79
+ SetWindowPos(window_, nullptr, 0, 0, rect.right - rect.left, rect.bottom - rect.top,
80
+ SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
81
+ ignoreResize_ = false;
82
+ }
83
+
84
+ void show() override
85
+ {
86
+ ShowWindow(window_, SW_SHOWNORMAL);
87
+ SetForegroundWindow(window_);
88
+ }
89
+
90
+ [[nodiscard]] double scale() const override { return static_cast<double>(GetDpiForWindow(window_)) / 96.0; }
91
+ [[nodiscard]] std::pair<int, int> windowSize() const override
92
+ {
93
+ RECT client {};
94
+ GetClientRect(window_, &client);
95
+ return { static_cast<int>(client.right - client.left), static_cast<int>(client.bottom - client.top) };
96
+ }
97
+
98
+ private:
99
+ static LRESULT CALLBACK windowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam)
100
+ {
101
+ auto* self = reinterpret_cast<WindowsEditorWindow*>(GetWindowLongPtrW(window, GWLP_USERDATA));
102
+ if (self == nullptr)
103
+ return DefWindowProcW(window, message, wParam, lParam);
104
+ switch (message)
105
+ {
106
+ case WM_SIZE:
107
+ if (! self->ignoreResize_ && wParam != SIZE_MINIMIZED && self->callbacks_.resized)
108
+ self->callbacks_.resized(LOWORD(lParam), HIWORD(lParam));
109
+ return 0;
110
+ case WM_ACTIVATE:
111
+ if (self->callbacks_.focusChanged)
112
+ self->callbacks_.focusChanged(LOWORD(wParam) != WA_INACTIVE);
113
+ break;
114
+ case WM_DPICHANGED:
115
+ {
116
+ const auto* suggested = reinterpret_cast<const RECT*>(lParam);
117
+ SetWindowPos(window, nullptr, suggested->left, suggested->top,
118
+ suggested->right - suggested->left, suggested->bottom - suggested->top,
119
+ SWP_NOZORDER | SWP_NOACTIVATE);
120
+ if (self->callbacks_.scaleChanged)
121
+ self->callbacks_.scaleChanged(static_cast<double>(HIWORD(wParam)) / 96.0);
122
+ return 0;
123
+ }
124
+ case WM_CLOSE:
125
+ // The callback destroys this window.
126
+ if (self->callbacks_.closed)
127
+ self->loop_.post(self->callbacks_.closed);
128
+ return 0;
129
+ default:
130
+ break;
131
+ }
132
+ return DefWindowProcW(window, message, wParam, lParam);
133
+ }
134
+
135
+ EventLoop& loop_;
136
+ Callbacks callbacks_;
137
+ HWND window_ = nullptr;
138
+ bool resizable_ = true;
139
+ bool ignoreResize_ = false;
140
+ };
141
+ } // namespace
142
+
143
+ std::unique_ptr<EditorWindow> EditorWindow::create(EventLoop& loop, const std::string& title, int width, int height,
144
+ Callbacks callbacks, std::string& /*error*/)
145
+ {
146
+ return std::make_unique<WindowsEditorWindow>(loop, title, width, height, std::move(callbacks));
147
+ }
148
+ } // namespace soundor::vst3host
@@ -0,0 +1,50 @@
1
+ // The main thread's loop. Every SDK call except process() runs on it, as
2
+ // VST3 requires of the "UI thread"; other threads hand work to it with post().
3
+ //
4
+ // Headless mode creates no window and initializes no video backend, but still
5
+ // runs this loop: plugins post work to their main thread (dispatch's main
6
+ // queue on macOS, window messages on Windows, IRunLoop handlers on Linux).
7
+ #pragma once
8
+
9
+ #include <functional>
10
+ #include <memory>
11
+
12
+ namespace soundor::vst3host
13
+ {
14
+ class EventLoop
15
+ {
16
+ public:
17
+ virtual ~EventLoop() = default;
18
+
19
+ /** `ui`: the platform's application object is set up for windows
20
+ (NSApplication on macOS); otherwise nothing visual is touched. */
21
+ static std::unique_ptr<EventLoop> create(bool ui);
22
+
23
+ /** Runs until quit(). */
24
+ virtual void run() = 0;
25
+ /** Main thread only. */
26
+ virtual void quit() = 0;
27
+ /** Any thread. Tasks run on the main thread, in order. */
28
+ virtual void post(std::function<void()> task) = 0;
29
+
30
+ using Id = unsigned long;
31
+ /** A repeating timer on the main thread. Main thread only. */
32
+ virtual Id addTimer(unsigned milliseconds, std::function<void()> fire) = 0;
33
+ virtual void removeTimer(Id id) = 0;
34
+
35
+ #if defined(__APPLE__)
36
+ /** In UI mode, waits for and dispatches Cocoa's events instead of the
37
+ loop's own wait (the host interface's SDL window reads them
38
+ itself); nullptr goes back to the loop's. */
39
+ virtual void setWaiter(std::function<void(double seconds)> wait) = 0;
40
+ #endif
41
+
42
+ #if defined(__linux__)
43
+ // With the timers, Linux::IRunLoop's primitives (HostContext.cpp),
44
+ // also used by the X11 windows for their display connections. Main
45
+ // thread only.
46
+ virtual Id addFd(int fd, std::function<void()> readable) = 0;
47
+ virtual void removeFd(Id id) = 0;
48
+ #endif
49
+ };
50
+ } // namespace soundor::vst3host
@@ -0,0 +1,204 @@
1
+ #include "EventLoop.h"
2
+
3
+ #include <fcntl.h>
4
+ #include <poll.h>
5
+ #include <unistd.h>
6
+
7
+ #include <algorithm>
8
+ #include <cerrno>
9
+ #include <chrono>
10
+ #include <deque>
11
+ #include <mutex>
12
+ #include <vector>
13
+
14
+ namespace soundor::vst3host
15
+ {
16
+ namespace
17
+ {
18
+ class LinuxEventLoop final : public EventLoop
19
+ {
20
+ public:
21
+ LinuxEventLoop()
22
+ {
23
+ if (pipe2(wake_, O_CLOEXEC | O_NONBLOCK) != 0)
24
+ wake_[0] = wake_[1] = -1;
25
+ }
26
+
27
+ ~LinuxEventLoop() override
28
+ {
29
+ for (const int fd : wake_)
30
+ if (fd >= 0)
31
+ close(fd);
32
+ }
33
+
34
+ void run() override
35
+ {
36
+ running_ = true;
37
+ while (running_)
38
+ {
39
+ runTasks();
40
+ if (! running_)
41
+ break;
42
+
43
+ std::vector<pollfd> fds;
44
+ std::vector<Id> ids;
45
+ fds.push_back({ wake_[0], POLLIN, 0 });
46
+ ids.push_back(0);
47
+ for (const auto& handler : fds_)
48
+ {
49
+ fds.push_back({ handler.fd, POLLIN, 0 });
50
+ ids.push_back(handler.id);
51
+ }
52
+
53
+ const int ready = poll(fds.data(), fds.size(), timeoutMs());
54
+ if (ready < 0 && errno != EINTR)
55
+ break;
56
+ if (ready > 0)
57
+ {
58
+ if ((fds[0].revents & POLLIN) != 0)
59
+ {
60
+ char buffer[64];
61
+ while (read(wake_[0], buffer, sizeof buffer) > 0)
62
+ {
63
+ }
64
+ }
65
+ for (size_t i = 1; i < fds.size(); ++i)
66
+ if ((fds[i].revents & (POLLIN | POLLHUP | POLLERR)) != 0)
67
+ fireFd(ids[i]);
68
+ }
69
+ fireTimers();
70
+ }
71
+ }
72
+
73
+ void quit() override
74
+ {
75
+ running_ = false;
76
+ wake();
77
+ }
78
+
79
+ void post(std::function<void()> task) override
80
+ {
81
+ {
82
+ const std::scoped_lock lock(mutex_);
83
+ tasks_.push_back(std::move(task));
84
+ }
85
+ wake();
86
+ }
87
+
88
+ Id addFd(int fd, std::function<void()> readable) override
89
+ {
90
+ const Id id = ++nextId_;
91
+ fds_.push_back({ id, fd, std::move(readable) });
92
+ return id;
93
+ }
94
+
95
+ void removeFd(Id id) override
96
+ {
97
+ std::erase_if(fds_, [id](const FdHandler& handler) { return handler.id == id; });
98
+ }
99
+
100
+ Id addTimer(unsigned milliseconds, std::function<void()> fire) override
101
+ {
102
+ const Id id = ++nextId_;
103
+ const auto interval = std::chrono::milliseconds(std::max(1U, milliseconds));
104
+ timers_.push_back({ id, interval, Clock::now() + interval, std::move(fire) });
105
+ return id;
106
+ }
107
+
108
+ void removeTimer(Id id) override
109
+ {
110
+ std::erase_if(timers_, [id](const Timer& timer) { return timer.id == id; });
111
+ }
112
+
113
+ private:
114
+ using Clock = std::chrono::steady_clock;
115
+ struct FdHandler
116
+ {
117
+ Id id;
118
+ int fd;
119
+ std::function<void()> readable;
120
+ };
121
+ struct Timer
122
+ {
123
+ Id id;
124
+ Clock::duration interval;
125
+ Clock::time_point due;
126
+ std::function<void()> fire;
127
+ };
128
+
129
+ void wake()
130
+ {
131
+ if (wake_[1] >= 0)
132
+ {
133
+ const char byte = 1;
134
+ [[maybe_unused]] const auto written = write(wake_[1], &byte, 1);
135
+ }
136
+ }
137
+
138
+ void runTasks()
139
+ {
140
+ std::deque<std::function<void()>> tasks;
141
+ {
142
+ const std::scoped_lock lock(mutex_);
143
+ tasks.swap(tasks_);
144
+ }
145
+ for (auto& task : tasks)
146
+ task();
147
+ }
148
+
149
+ [[nodiscard]] int timeoutMs() const
150
+ {
151
+ if (timers_.empty())
152
+ return -1;
153
+ const auto now = Clock::now();
154
+ auto next = timers_.front().due;
155
+ for (const auto& timer : timers_)
156
+ next = std::min(next, timer.due);
157
+ if (next <= now)
158
+ return 0;
159
+ return static_cast<int>(std::chrono::ceil<std::chrono::milliseconds>(next - now).count());
160
+ }
161
+
162
+ // Handlers may add or remove handlers; each is looked up again.
163
+ void fireFd(Id id)
164
+ {
165
+ const auto it = std::ranges::find(fds_, id, &FdHandler::id);
166
+ if (it == fds_.end())
167
+ return;
168
+ const auto callback = it->readable;
169
+ callback();
170
+ }
171
+
172
+ void fireTimers()
173
+ {
174
+ const auto now = Clock::now();
175
+ std::vector<Id> due;
176
+ for (const auto& timer : timers_)
177
+ if (timer.due <= now)
178
+ due.push_back(timer.id);
179
+ for (const Id id : due)
180
+ {
181
+ const auto it = std::ranges::find(timers_, id, &Timer::id);
182
+ if (it == timers_.end())
183
+ continue;
184
+ it->due = now + it->interval;
185
+ const auto callback = it->fire;
186
+ callback();
187
+ }
188
+ }
189
+
190
+ int wake_[2] = { -1, -1 };
191
+ bool running_ = false;
192
+ std::mutex mutex_;
193
+ std::deque<std::function<void()>> tasks_;
194
+ std::vector<FdHandler> fds_;
195
+ std::vector<Timer> timers_;
196
+ Id nextId_ = 0;
197
+ };
198
+ } // namespace
199
+
200
+ std::unique_ptr<EventLoop> EventLoop::create(bool /*ui*/)
201
+ {
202
+ return std::make_unique<LinuxEventLoop>();
203
+ }
204
+ } // namespace soundor::vst3host