From 556e6871366c5b9e5645f43408295026fe5a7bcd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 03:34:29 +0000 Subject: [PATCH 1/3] Add VST3 .vstpreset interchange codec and provider backend Implements the .vstpreset interchange from the .NET VST3 effects host plan (upstream roadmap item #5), so ktsu persistence can round-trip host preset files. - VstPresetFile: a faithful reader/writer for the Steinberg VST3 .vstpreset container format (VST3 header + class id + component/controller/info chunks + trailing little-endian chunk list). Stream and byte[] overloads. Files it writes are loadable by hosts, and host-written presets read back. - VstPreset: the decoded model (class id, component state, optional controller state, optional XML metadata, version). - VstPresetSerializationProvider: an ISerializationProvider that wraps an inner provider (e.g. JSON) and packages its output as the component chunk of a .vstpreset, Base64-encoded to satisfy the string-based interface. For direct byte/file interop, VstPresetFile is used directly. All additive and netstandard2.0-compatible. Includes MSTest/FluentAssertions coverage: header validation, full round-trips (component only, all chunks, empty state), malformed-input and non-seekable-stream guards, and provider round-trip producing a real Base64-encoded .vstpreset. --- SerializationProvider.Tests/VstPresetTests.cs | 135 +++++++++++ SerializationProvider/VstPreset.cs | 63 +++++ SerializationProvider/VstPresetFile.cs | 221 ++++++++++++++++++ .../VstPresetSerializationProvider.cs | 94 ++++++++ 4 files changed, 513 insertions(+) create mode 100644 SerializationProvider.Tests/VstPresetTests.cs create mode 100644 SerializationProvider/VstPreset.cs create mode 100644 SerializationProvider/VstPresetFile.cs create mode 100644 SerializationProvider/VstPresetSerializationProvider.cs diff --git a/SerializationProvider.Tests/VstPresetTests.cs b/SerializationProvider.Tests/VstPresetTests.cs new file mode 100644 index 0000000..a552655 --- /dev/null +++ b/SerializationProvider.Tests/VstPresetTests.cs @@ -0,0 +1,135 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.SerializationProvider.Tests; + +using System.IO; +using System.Text; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class VstPresetTests +{ + private const string SampleClassId = "565354416463416E6F746167666F6F62"; // 32 ASCII chars + + [TestMethod] + public void ToBytes_ProducesValidVst3Header() + { + VstPreset preset = new(SampleClassId, [1, 2, 3, 4]); + + byte[] bytes = VstPresetFile.ToBytes(preset); + + Encoding.ASCII.GetString(bytes, 0, 4).Should().Be("VST3"); + bytes.Length.Should().BeGreaterThan(48); + } + + [TestMethod] + public void RoundTrip_ComponentStateOnly_IsPreserved() + { + byte[] state = [10, 20, 30, 40, 50]; + VstPreset preset = new(SampleClassId, state); + + VstPreset decoded = VstPresetFile.FromBytes(VstPresetFile.ToBytes(preset)); + + decoded.ClassId.Should().Be(SampleClassId); + decoded.ComponentState.Should().Equal(state); + decoded.ControllerState.Should().BeNull(); + decoded.MetaInfo.Should().BeNull(); + decoded.Version.Should().Be(VstPresetFile.FormatVersion); + } + + [TestMethod] + public void RoundTrip_AllChunks_ArePreserved() + { + byte[] component = [1, 2, 3]; + byte[] controller = [9, 8, 7, 6]; + const string meta = "hello"; + VstPreset preset = new(SampleClassId, component, controller, meta); + + VstPreset decoded = VstPresetFile.FromBytes(VstPresetFile.ToBytes(preset)); + + decoded.ComponentState.Should().Equal(component); + decoded.ControllerState.Should().Equal(controller); + decoded.MetaInfo.Should().Be(meta); + } + + [TestMethod] + public void RoundTrip_EmptyComponentState_IsPreserved() + { + VstPreset preset = new(SampleClassId, []); + + VstPreset decoded = VstPresetFile.FromBytes(VstPresetFile.ToBytes(preset)); + + decoded.ComponentState.Should().BeEmpty(); + } + + [TestMethod] + public void Read_NonVstData_Throws() + { + byte[] garbage = Encoding.ASCII.GetBytes("NOPE this is not a preset file at all"); + + Action act = () => VstPresetFile.FromBytes(garbage); + + act.Should().Throw(); + } + + [TestMethod] + public void Write_NonSeekableStream_Throws() + { + VstPreset preset = new(SampleClassId, [1]); + + Action act = () => VstPresetFile.Write(new NonSeekableStream(), preset); + + act.Should().Throw(); + } + + [TestMethod] + public void Provider_RoundTrips_ThroughInnerProvider() + { + VstPresetSerializationProvider provider = new(new JsonInner(), SampleClassId); + Payload original = new("delay", 0.45); + + string serialized = provider.Serialize(original); + Payload restored = provider.Deserialize(serialized); + + provider.ProviderName.Should().Be("VST3 Preset"); + provider.ContentType.Should().Be("application/vnd.steinberg.vstpreset"); + restored.Should().Be(original); + } + + [TestMethod] + public void Provider_Output_IsBase64OfRealVstPreset() + { + VstPresetSerializationProvider provider = new(new JsonInner(), SampleClassId); + + string serialized = provider.Serialize(new Payload("gain", 1.0)); + byte[] presetBytes = Convert.FromBase64String(serialized); + + Encoding.ASCII.GetString(presetBytes, 0, 4).Should().Be("VST3"); + VstPreset decoded = VstPresetFile.FromBytes(presetBytes); + decoded.ClassId.Should().Be(SampleClassId); + } + + private sealed record Payload(string Name, double Value); + + private sealed class JsonInner : ISerializationProvider + { + public string ProviderName => "Json"; + public string ContentType => "application/json"; + public string Serialize(T obj) => System.Text.Json.JsonSerializer.Serialize(obj); + public string Serialize(object obj, Type type) => System.Text.Json.JsonSerializer.Serialize(obj, type); + public T Deserialize(string data) => System.Text.Json.JsonSerializer.Deserialize(data)!; + public object Deserialize(string data, Type type) => System.Text.Json.JsonSerializer.Deserialize(data, type)!; + public Task SerializeAsync(T obj, CancellationToken cancellationToken = default) => Task.FromResult(Serialize(obj)); + public Task SerializeAsync(object obj, Type type, CancellationToken cancellationToken = default) => Task.FromResult(Serialize(obj, type)); + public Task DeserializeAsync(string data, CancellationToken cancellationToken = default) => Task.FromResult(Deserialize(data)); + public Task DeserializeAsync(string data, Type type, CancellationToken cancellationToken = default) => Task.FromResult(Deserialize(data, type)); + } + + private sealed class NonSeekableStream : MemoryStream + { + public override bool CanSeek => false; + } +} diff --git a/SerializationProvider/VstPreset.cs b/SerializationProvider/VstPreset.cs new file mode 100644 index 0000000..58dab35 --- /dev/null +++ b/SerializationProvider/VstPreset.cs @@ -0,0 +1,63 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.SerializationProvider; + +using System.Diagnostics.CodeAnalysis; + +/// +/// The decoded contents of a VST3 .vstpreset file. +/// +/// +/// A VST3 preset is a small binary container that pairs a plugin's class identifier with one or more +/// opaque state blobs: the processor (component) state, an optional controller state, and optional XML +/// metadata. See for reading and writing the on-disk format. +/// +public sealed record VstPreset +{ + /// + /// Gets the plugin class identifier (the 32-character ASCII representation of the VST3 FUID). + /// + public string ClassId { get; } + + /// + /// Gets the processor (component) state blob. This is the primary plugin state. + /// + [SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "A preset state chunk is an opaque binary blob written verbatim to the file; a byte array is the natural representation.")] + public byte[] ComponentState { get; } + + /// + /// Gets the optional controller state blob, or when the preset has none. + /// + [SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "A preset state chunk is an opaque binary blob written verbatim to the file; a byte array is the natural representation.")] + public byte[]? ControllerState { get; } + + /// + /// Gets the optional metadata, typically an XML document describing the preset, or . + /// + public string? MetaInfo { get; } + + /// + /// Gets the preset format version stored in the file header. + /// + public int Version { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The plugin class identifier. + /// The processor (component) state blob. + /// The optional controller state blob. + /// The optional XML metadata. + /// The preset format version. + /// Thrown when or is null. + public VstPreset(string classId, byte[] componentState, byte[]? controllerState = null, string? metaInfo = null, int version = VstPresetFile.FormatVersion) + { + ClassId = classId ?? throw new ArgumentNullException(nameof(classId)); + ComponentState = componentState ?? throw new ArgumentNullException(nameof(componentState)); + ControllerState = controllerState; + MetaInfo = metaInfo; + Version = version; + } +} diff --git a/SerializationProvider/VstPresetFile.cs b/SerializationProvider/VstPresetFile.cs new file mode 100644 index 0000000..8a1a315 --- /dev/null +++ b/SerializationProvider/VstPresetFile.cs @@ -0,0 +1,221 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.SerializationProvider; + +using System.IO; +using System.Text; + +/// +/// Reads and writes the Steinberg VST3 .vstpreset container format. +/// +/// +/// The format is a header followed by the state blobs and a trailing chunk list: +/// +/// Header: the ASCII tag VST3, an version, a 32-byte ASCII class id, and an offset to the chunk list. +/// Data: the component state, optional controller state, and optional metadata, written back to back. +/// Chunk list: the ASCII tag List, an entry count, then for each entry a 4-byte id (Comp, Cont, Info), an offset and an size. +/// +/// All integers are little-endian, matching the reference implementation in the VST3 SDK, so files +/// written here can be loaded by hosts and host-written presets can be read back. +/// +public static class VstPresetFile +{ + /// The current preset format version. + public const int FormatVersion = 1; + + private const string HeaderTag = "VST3"; + private const string ListTag = "List"; + private const string ComponentChunkId = "Comp"; + private const string ControllerChunkId = "Cont"; + private const string MetaInfoChunkId = "Info"; + private const int ClassIdLength = 32; + + /// + /// Writes a preset to a seekable, writable stream. + /// + /// The destination stream; must support seeking and writing. + /// The preset to write. + /// Thrown when or is null. + /// Thrown when is not seekable or writable. + public static void Write(Stream stream, VstPreset preset) + { + _ = stream ?? throw new ArgumentNullException(nameof(stream)); + _ = preset ?? throw new ArgumentNullException(nameof(preset)); + if (!stream.CanSeek || !stream.CanWrite) + { + throw new ArgumentException("Stream must be seekable and writable.", nameof(stream)); + } + + using BinaryWriter writer = new(stream, Encoding.ASCII, leaveOpen: true); + + writer.Write(Encoding.ASCII.GetBytes(HeaderTag)); + writer.Write(preset.Version); + writer.Write(EncodeClassId(preset.ClassId)); + long listOffsetField = stream.Position; + writer.Write(0L); // Placeholder for the chunk-list offset; patched once the list position is known. + + List<(string Id, long Offset, long Size)> entries = []; + + AppendChunk(writer, stream, entries, ComponentChunkId, preset.ComponentState); + if (preset.ControllerState is not null) + { + AppendChunk(writer, stream, entries, ControllerChunkId, preset.ControllerState); + } + + if (preset.MetaInfo is not null) + { + AppendChunk(writer, stream, entries, MetaInfoChunkId, Encoding.UTF8.GetBytes(preset.MetaInfo)); + } + + long listOffset = stream.Position; + writer.Write(Encoding.ASCII.GetBytes(ListTag)); + writer.Write(entries.Count); + foreach ((string id, long offset, long size) in entries) + { + writer.Write(Encoding.ASCII.GetBytes(id)); + writer.Write(offset); + writer.Write(size); + } + + writer.Flush(); + + // Patch the header's chunk-list offset now that we know where the list landed. + stream.Seek(listOffsetField, SeekOrigin.Begin); + writer.Write(listOffset); + writer.Flush(); + stream.Seek(0, SeekOrigin.End); + } + + /// + /// Serializes a preset to a new byte array. + /// + /// The preset to serialize. + /// The encoded .vstpreset bytes. + public static byte[] ToBytes(VstPreset preset) + { + using MemoryStream stream = new(); + Write(stream, preset); + return stream.ToArray(); + } + + /// + /// Reads a preset from a seekable, readable stream. + /// + /// The source stream; must support seeking and reading. + /// The decoded . + /// Thrown when is null. + /// Thrown when is not seekable or readable. + /// Thrown when the stream is not a valid VST3 preset. + public static VstPreset Read(Stream stream) + { + _ = stream ?? throw new ArgumentNullException(nameof(stream)); + if (!stream.CanSeek || !stream.CanRead) + { + throw new ArgumentException("Stream must be seekable and readable.", nameof(stream)); + } + + using BinaryReader reader = new(stream, Encoding.ASCII, leaveOpen: true); + + stream.Seek(0, SeekOrigin.Begin); + if (ReadTag(reader) != HeaderTag) + { + throw new InvalidDataException("Not a VST3 preset: missing 'VST3' header tag."); + } + + int version = reader.ReadInt32(); + string classId = DecodeClassId(ReadExact(reader, ClassIdLength)); + long listOffset = reader.ReadInt64(); + + stream.Seek(listOffset, SeekOrigin.Begin); + if (ReadTag(reader) != ListTag) + { + throw new InvalidDataException("Corrupt VST3 preset: missing 'List' chunk tag."); + } + + int entryCount = reader.ReadInt32(); + if (entryCount < 0) + { + throw new InvalidDataException("Corrupt VST3 preset: negative chunk count."); + } + + List<(string Id, long Offset, long Size)> entries = new(entryCount); + for (int i = 0; i < entryCount; i++) + { + string id = ReadTag(reader); + long offset = reader.ReadInt64(); + long size = reader.ReadInt64(); + entries.Add((id, offset, size)); + } + + byte[]? component = null; + byte[]? controller = null; + string? metaInfo = null; + + foreach ((string id, long offset, long size) in entries) + { + stream.Seek(offset, SeekOrigin.Begin); + byte[] data = ReadExact(reader, checked((int)size)); + switch (id) + { + case ComponentChunkId: + component = data; + break; + case ControllerChunkId: + controller = data; + break; + case MetaInfoChunkId: + metaInfo = Encoding.UTF8.GetString(data); + break; + default: + // Unknown chunk: preserved by hosts but not modelled here; ignore. + break; + } + } + + return component is null + ? throw new InvalidDataException("Corrupt VST3 preset: no component ('Comp') chunk.") + : new VstPreset(classId, component, controller, metaInfo, version); + } + + /// + /// Deserializes a preset from a byte array. + /// + /// The encoded .vstpreset bytes. + /// The decoded . + /// Thrown when is null. + public static VstPreset FromBytes(byte[] bytes) + { + _ = bytes ?? throw new ArgumentNullException(nameof(bytes)); + using MemoryStream stream = new(bytes, writable: false); + return Read(stream); + } + + private static void AppendChunk(BinaryWriter writer, Stream stream, List<(string Id, long Offset, long Size)> entries, string id, byte[] data) + { + long offset = stream.Position; + writer.Write(data); + entries.Add((id, offset, data.Length)); + } + + private static byte[] EncodeClassId(string classId) + { + byte[] buffer = new byte[ClassIdLength]; + byte[] source = Encoding.ASCII.GetBytes(classId); + Array.Copy(source, buffer, Math.Min(source.Length, ClassIdLength)); + return buffer; + } + + private static string DecodeClassId(byte[] raw) => Encoding.ASCII.GetString(raw).TrimEnd('\0'); + + private static string ReadTag(BinaryReader reader) => Encoding.ASCII.GetString(ReadExact(reader, 4)); + + private static byte[] ReadExact(BinaryReader reader, int count) + { + byte[] data = reader.ReadBytes(count); + return data.Length != count + ? throw new InvalidDataException("Corrupt VST3 preset: unexpected end of stream.") + : data; + } +} diff --git a/SerializationProvider/VstPresetSerializationProvider.cs b/SerializationProvider/VstPresetSerializationProvider.cs new file mode 100644 index 0000000..7739013 --- /dev/null +++ b/SerializationProvider/VstPresetSerializationProvider.cs @@ -0,0 +1,94 @@ +// Copyright (c) ktsu.dev +// All rights reserved. +// Licensed under the MIT license. + +namespace ktsu.SerializationProvider; + +using System.Text; + +/// +/// An that wraps another provider and packages its output inside a +/// VST3 .vstpreset container. +/// +/// +/// The inner provider produces the logical state (for example JSON); this provider stores that state as +/// the component chunk of a tagged with a configured class id. Because the +/// contract is string-based, the binary preset is returned +/// Base64-encoded. For direct file interop — writing bytes a host can load, or reading a host-written +/// .vstpreset — use directly. +/// +public sealed class VstPresetSerializationProvider : ISerializationProvider +{ + private readonly ISerializationProvider inner; + private readonly string classId; + + /// + /// Initializes a new instance of the class. + /// + /// The provider that serializes the logical state stored inside the preset. + /// The VST3 plugin class id (32-character ASCII FUID) to tag presets with. + /// Thrown when or is null. + public VstPresetSerializationProvider(ISerializationProvider innerProvider, string presetClassId) + { + inner = innerProvider ?? throw new ArgumentNullException(nameof(innerProvider)); + classId = presetClassId ?? throw new ArgumentNullException(nameof(presetClassId)); + } + + /// + public string ProviderName => "VST3 Preset"; + + /// + public string ContentType => "application/vnd.steinberg.vstpreset"; + + /// + public string Serialize(T obj) => Wrap(inner.Serialize(obj)); + + /// + public string Serialize(object obj, Type type) => Wrap(inner.Serialize(obj, type)); + + /// + public T Deserialize(string data) => inner.Deserialize(Unwrap(data)); + + /// + public object Deserialize(string data, Type type) => inner.Deserialize(Unwrap(data), type); + + /// + public Task SerializeAsync(T obj, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(Serialize(obj)); + } + + /// + public Task SerializeAsync(object obj, Type type, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(Serialize(obj, type)); + } + + /// + public Task DeserializeAsync(string data, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(Deserialize(data)); + } + + /// + public Task DeserializeAsync(string data, Type type, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(Deserialize(data, type)); + } + + private string Wrap(string state) + { + VstPreset preset = new(classId, Encoding.UTF8.GetBytes(state)); + return Convert.ToBase64String(VstPresetFile.ToBytes(preset)); + } + + private static string Unwrap(string data) + { + VstPreset preset = VstPresetFile.FromBytes(Convert.FromBase64String(data)); + return Encoding.UTF8.GetString(preset.ComponentState); + } +} From e534101a87c96ca22b36e8fb60c287759bd5a528 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 03:40:41 +0000 Subject: [PATCH 2/3] Use a primary constructor in VstPresetSerializationProvider CI's code-style analyzers (which the local sandbox SDK cannot load) flag the explicit constructor as IDE0290. Convert to a primary constructor with validating field initializers; behaviour is unchanged. --- .../VstPresetSerializationProvider.cs | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/SerializationProvider/VstPresetSerializationProvider.cs b/SerializationProvider/VstPresetSerializationProvider.cs index 7739013..efe4d43 100644 --- a/SerializationProvider/VstPresetSerializationProvider.cs +++ b/SerializationProvider/VstPresetSerializationProvider.cs @@ -17,22 +17,12 @@ namespace ktsu.SerializationProvider; /// Base64-encoded. For direct file interop — writing bytes a host can load, or reading a host-written /// .vstpreset — use directly. /// -public sealed class VstPresetSerializationProvider : ISerializationProvider +/// The provider that serializes the logical state stored inside the preset. +/// The VST3 plugin class id (32-character ASCII FUID) to tag presets with. +public sealed class VstPresetSerializationProvider(ISerializationProvider innerProvider, string presetClassId) : ISerializationProvider { - private readonly ISerializationProvider inner; - private readonly string classId; - - /// - /// Initializes a new instance of the class. - /// - /// The provider that serializes the logical state stored inside the preset. - /// The VST3 plugin class id (32-character ASCII FUID) to tag presets with. - /// Thrown when or is null. - public VstPresetSerializationProvider(ISerializationProvider innerProvider, string presetClassId) - { - inner = innerProvider ?? throw new ArgumentNullException(nameof(innerProvider)); - classId = presetClassId ?? throw new ArgumentNullException(nameof(presetClassId)); - } + private readonly ISerializationProvider inner = innerProvider ?? throw new ArgumentNullException(nameof(innerProvider)); + private readonly string classId = presetClassId ?? throw new ArgumentNullException(nameof(presetClassId)); /// public string ProviderName => "VST3 Preset"; From d868c083aafe4036b62567de384a1f8696a7c8a8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 03:48:33 +0000 Subject: [PATCH 3/3] Use Ensure.NotNull for argument validation The ktsu.Sdk analyzer KTSU0004 (which the local sandbox SDK cannot load) requires Ensure.NotNull(x) instead of manual `?? throw new ArgumentNullException`. Replace the null checks in VstPreset, VstPresetFile, and VstPresetSerializationProvider accordingly; Ensure.NotNull returns the validated value, so it also works in the primary-constructor field initializers. Behaviour is unchanged. --- SerializationProvider/VstPreset.cs | 4 ++-- SerializationProvider/VstPresetFile.cs | 8 ++++---- SerializationProvider/VstPresetSerializationProvider.cs | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/SerializationProvider/VstPreset.cs b/SerializationProvider/VstPreset.cs index 58dab35..d3c8e3c 100644 --- a/SerializationProvider/VstPreset.cs +++ b/SerializationProvider/VstPreset.cs @@ -54,8 +54,8 @@ public sealed record VstPreset /// Thrown when or is null. public VstPreset(string classId, byte[] componentState, byte[]? controllerState = null, string? metaInfo = null, int version = VstPresetFile.FormatVersion) { - ClassId = classId ?? throw new ArgumentNullException(nameof(classId)); - ComponentState = componentState ?? throw new ArgumentNullException(nameof(componentState)); + ClassId = Ensure.NotNull(classId); + ComponentState = Ensure.NotNull(componentState); ControllerState = controllerState; MetaInfo = metaInfo; Version = version; diff --git a/SerializationProvider/VstPresetFile.cs b/SerializationProvider/VstPresetFile.cs index 8a1a315..6aa1d94 100644 --- a/SerializationProvider/VstPresetFile.cs +++ b/SerializationProvider/VstPresetFile.cs @@ -41,8 +41,8 @@ public static class VstPresetFile /// Thrown when is not seekable or writable. public static void Write(Stream stream, VstPreset preset) { - _ = stream ?? throw new ArgumentNullException(nameof(stream)); - _ = preset ?? throw new ArgumentNullException(nameof(preset)); + Ensure.NotNull(stream); + Ensure.NotNull(preset); if (!stream.CanSeek || !stream.CanWrite) { throw new ArgumentException("Stream must be seekable and writable.", nameof(stream)); @@ -110,7 +110,7 @@ public static byte[] ToBytes(VstPreset preset) /// Thrown when the stream is not a valid VST3 preset. public static VstPreset Read(Stream stream) { - _ = stream ?? throw new ArgumentNullException(nameof(stream)); + Ensure.NotNull(stream); if (!stream.CanSeek || !stream.CanRead) { throw new ArgumentException("Stream must be seekable and readable.", nameof(stream)); @@ -187,7 +187,7 @@ public static VstPreset Read(Stream stream) /// Thrown when is null. public static VstPreset FromBytes(byte[] bytes) { - _ = bytes ?? throw new ArgumentNullException(nameof(bytes)); + Ensure.NotNull(bytes); using MemoryStream stream = new(bytes, writable: false); return Read(stream); } diff --git a/SerializationProvider/VstPresetSerializationProvider.cs b/SerializationProvider/VstPresetSerializationProvider.cs index efe4d43..ab56d1e 100644 --- a/SerializationProvider/VstPresetSerializationProvider.cs +++ b/SerializationProvider/VstPresetSerializationProvider.cs @@ -21,8 +21,8 @@ namespace ktsu.SerializationProvider; /// The VST3 plugin class id (32-character ASCII FUID) to tag presets with. public sealed class VstPresetSerializationProvider(ISerializationProvider innerProvider, string presetClassId) : ISerializationProvider { - private readonly ISerializationProvider inner = innerProvider ?? throw new ArgumentNullException(nameof(innerProvider)); - private readonly string classId = presetClassId ?? throw new ArgumentNullException(nameof(presetClassId)); + private readonly ISerializationProvider inner = Ensure.NotNull(innerProvider); + private readonly string classId = Ensure.NotNull(presetClassId); /// public string ProviderName => "VST3 Preset";