diff --git a/src/common/serialization/vst3/base.h b/src/common/serialization/vst3/base.h
index a2437457..5a1b12d0 100644
--- a/src/common/serialization/vst3/base.h
+++ b/src/common/serialization/vst3/base.h
@@ -28,8 +28,9 @@
// Yet Another layer of includes, but these are some VST3-specific typedefs that
// we'll need for all of our interfaces
-using Steinberg::TBool, Steinberg::int8, Steinberg::int32, Steinberg::int64,
- Steinberg::uint32, Steinberg::uint64, Steinberg::tresult;
+using Steinberg::TBool, Steinberg::int8, Steinberg::int16, Steinberg::int32,
+ Steinberg::int64, Steinberg::uint8, Steinberg::uint32, Steinberg::uint64,
+ Steinberg::tresult;
/**
* Both `TUID` (`int8_t[16]`) and `FIDString` (`char*`) are hard to work with
diff --git a/src/common/serialization/vst3/event-list.h b/src/common/serialization/vst3/event-list.h
new file mode 100644
index 00000000..ac3e93d7
--- /dev/null
+++ b/src/common/serialization/vst3/event-list.h
@@ -0,0 +1,294 @@
+// yabridge: a Wine VST bridge
+// Copyright (C) 2020 Robbert van der Helm
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+#pragma once
+
+#include
+#include
+
+#include "base.h"
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wnon-virtual-dtor"
+
+/**
+ * A wrapper around `DataEvent` for serialization purposes, as this event
+ * contains a heap array.
+ */
+struct YaDataEvent {
+ YaDataEvent();
+
+ /**
+ * Copy data from an existing `DataEvent`.
+ */
+ YaDataEvent(const Steinberg::Vst::DataEvent& event);
+
+ /**
+ * Reconstruct a `DataEvent` from this object.
+ *
+ * @note This object may contain pointers to data stored in this object, and
+ * must thus not outlive it.
+ */
+ Steinberg::Vst::DataEvent get() const;
+
+ uint32 type;
+ std::vector data;
+
+ template
+ void serialize(S& s) {
+ s.value4b(type);
+ s.container1b(data, 1 << 16);
+ }
+};
+
+/**
+ * A wrapper around `NoteExpressionTextEvent` for serialization purposes, as
+ * this event contains a heap array.
+ */
+struct YaNoteExpressionTextEvent {
+ YaNoteExpressionTextEvent();
+
+ /**
+ * Copy data from an existing `NoteExpressionTextEvent`.
+ */
+ YaNoteExpressionTextEvent(
+ const Steinberg::Vst::NoteExpressionTextEvent& event);
+
+ /**
+ * Reconstruct a `NoteExpressionTextEvent` from this object.
+ *
+ * @note This object may contain pointers to data stored in this object, and
+ * must thus not outlive it.
+ */
+ Steinberg::Vst::NoteExpressionTextEvent get() const;
+
+ Steinberg::Vst::NoteExpressionTypeID type_id;
+ int32 note_id;
+
+ std::u16string text;
+
+ template
+ void serialize(S& s) {
+ s.value4b(type_id);
+ s.value4b(note_id);
+ s.container2b(text, 1 << 16);
+ }
+};
+
+/**
+ * A wrapper around `ChordEvent` for serialization purposes, as this event
+ * contains a heap array.
+ */
+struct YaChordEvent {
+ YaChordEvent();
+
+ /**
+ * Copy data from an existing `ChordEvent`.
+ */
+ YaChordEvent(const Steinberg::Vst::ChordEvent& event);
+
+ /**
+ * Reconstruct a `ChordEvent` from this object.
+ *
+ * @note This object may contain pointers to data stored in this object, and
+ * must thus not outlive it.
+ */
+ Steinberg::Vst::ChordEvent get() const;
+
+ int16 root;
+ int16 bass_note;
+ int16 mask;
+
+ std::u16string text;
+
+ template
+ void serialize(S& s) {
+ s.value2b(root);
+ s.value2b(bass_note);
+ s.value2b(mask);
+ s.container2b(text, 1 << 16);
+ }
+};
+
+/**
+ * A wrapper around `ScaleEvent` for serialization purposes, as this event
+ * contains a heap array.
+ */
+struct YaScaleEvent {
+ YaScaleEvent();
+
+ /**
+ * Copy data from an existing `ScaleEvent`.
+ */
+ YaScaleEvent(const Steinberg::Vst::ScaleEvent& event);
+
+ /**
+ * Reconstruct a `ScaleEvent` from this object.
+ *
+ * @note This object may contain pointers to data stored in this object, and
+ * must thus not outlive it.
+ */
+ Steinberg::Vst::ScaleEvent get() const;
+
+ int16 root;
+ int16 mask;
+
+ std::u16string text;
+
+ template
+ void serialize(S& s) {
+ s.value2b(root);
+ s.value2b(mask);
+ s.container2b(text, 1 << 16);
+ }
+};
+
+/**
+ * A wrapper around `Event` for serialization purposes, as some event types
+ * include heap pointers.
+ */
+struct YaEvent {
+ YaEvent();
+
+ /**
+ * Copy data from an `Event`.
+ */
+ YaEvent(const Steinberg::Vst::Event& event);
+
+ /**
+ * Reconstruct an `Event` from this object.
+ *
+ * @note This object may contain pointers to data stored in this object, and
+ * must thus not outlive it.
+ */
+ Steinberg::Vst::Event get() const;
+
+ // These fields directly reflect those from `Event`
+ int32 bus_index;
+ int32 sample_offset;
+ Steinberg::Vst::TQuarterNotes ppq_position;
+ Steinberg::Vst::Event::EventFlags flags;
+
+ // `Event` stores an event type and a union, we'll encode both in a variant.
+ // We can use simple types directly, and we need serializable wrappers
+ // around move event types with heap pointers.
+ std::variant
+ payload;
+
+ template
+ void serialize(S& s) {
+ s.value4b(bus_index);
+ s.value4b(sample_offset);
+ s.value8b(ppq_position);
+ s.value2b(flags);
+ s.ext(payload, bitsery::ext::StdVariant{});
+ }
+};
+
+/**
+ * Wraps around `IEventList` for serialization purposes. Used in
+ * `YaProcessData`.
+ */
+class YaEventList : public Steinberg::Vst::IEventList {
+ public:
+ /**
+ * Default constructor with an empty event list. The plugin can use this to
+ * output data.
+ */
+ YaEventList();
+
+ /**
+ * Read data from an existing `IEventList` object.
+ */
+ YaEventList(Steinberg::Vst::IEventList& original_events);
+
+ ~YaEventList();
+
+ DECLARE_FUNKNOWN_METHODS
+
+ // From `IEventList`
+ virtual int32 PLUGIN_API getEventCount() override;
+ // We're making the assumption here that events are immutable (which should
+ // be the case, but it's never mentioned anywhere)
+ virtual tresult PLUGIN_API
+ getEvent(int32 index, Steinberg::Vst::Event& e /*out*/) override;
+ virtual tresult PLUGIN_API
+ addEvent(Steinberg::Vst::Event& e /*in*/) override;
+
+ template
+ void serialize(S& s) {
+ s.container(events, 1 << 16);
+ }
+
+ private:
+ std::vector events;
+};
+
+namespace Steinberg {
+namespace Vst {
+template
+void serialize(S& s, NoteOnEvent& event) {
+ s.value2b(event.channel);
+ s.value2b(event.pitch);
+ s.value4b(event.tuning);
+ s.value4b(event.velocity);
+ s.value4b(event.length);
+ s.value4b(event.noteId);
+}
+
+template
+void serialize(S& s, NoteOffEvent& event) {
+ s.value2b(event.channel);
+ s.value2b(event.pitch);
+ s.value4b(event.velocity);
+ s.value4b(event.noteId);
+ s.value4b(event.tuning);
+}
+
+template
+void serialize(S& s, PolyPressureEvent& event) {
+ s.value2b(event.channel);
+ s.value2b(event.pitch);
+ s.value4b(event.pressure);
+ s.value4b(event.noteId);
+}
+
+template
+void serialize(S& s, NoteExpressionValueEvent& event) {
+ s.value4b(event.typeId);
+ s.value4b(event.noteId);
+ s.value8b(event.value);
+}
+
+template
+void serialize(S& s, LegacyMIDICCOutEvent& event) {
+ s.value1b(event.controlNumber);
+ s.value1b(event.channel);
+ s.value1b(event.value);
+ s.value1b(event.value2);
+}
+} // namespace Vst
+} // namespace Steinberg
+
+#pragma GCC diagnostic pop