DEV Community

Vlad Gorlov
Vlad Gorlov

Posted on • Edited on

Using SwiftUI in VST2 Plug-In

On this Page

Let's make an Audio Plug-In in VST 2.4.2 format which uses SwiftUI in Effect editor. The processor part of this plug-in will change the gain of incoming audio signal.

Few historical notes:

Virtual Studio Technology - Wikipedia

VST was developed by Steinberg Media Technologies in 1996. It creates a complete, professional studio environment on the PC or Mac.

Swift (programming language) - Wikipedia

During WWDC 2019, Apple announced SwiftUI, which provides a framework for declarative UI structure design across all Apple platforms.

In this tutorial we will use VST SDK v2.4.2 which was published in 2006. Thus the distance in time between two technologies is 13 years ๐Ÿ™€.

Preparations

We need tree pieces: Xcode 11.4, VST SDK v2.4.2 and VST Host software to run our audio plug-in.

Xcode

The easiest part is to get Xcode 11.4. It can be downloaded from Mac App Store or from Apple Developer portal.

VST Host software

For VST Host software we can use Renoise 3.2.1. Demo version of this app has several limitations, but it is enough to run our plug-in in debug mode. Another good VST Host is a REAPER. But it has protection against debugging and reverse engineering. So, we can use it to test plug-in in non-debug mode only.

VST SDK v2.4.2

Most interesting part is to get VST SDK v2.4.2. It is discounted and not available for download anymore from official Steinberg website.

One more thing we would like to point out at this stage: with the release of the VST 3.6.11, the SDK for VST 2 has officially been discontinued. We are happy that third-party developers are now looking forward and will continue VST plug-in development with the SDK of VST 3.

A lot of VST Host software still using VST SDK v2.4.2 plug-ins due their simplicity. Also worth to mention that not every VST Host software has newer VST3 audio plug-ins support. There is a various of discussions about VST SDK v2.4.2 deprecation.

BUT. We still can download VST SDK v2.4.2 from internet archives. Here is some links:

Another way โ€“ is to extract VST SDK v2.4.2 sources from projects hosted on GitHub.

In this tutorial we will use VST SDK v2.4.2 from Internet Archive website.

VST SDK Download

After downloading you should have file structure like shown below. Whole VST SDK v2.4.2 consists with only 9 files ๐Ÿ™ƒ.

Contents of VST SDK Download

Creating Plug-In scaffold

First we need to make a scaffold of the plug-in Xcode project with integrated VST SDK v2.4.2.

  1. Configure path to VST SDK v2.4.2 in Xcode. We will use this setting later in the project โœ….

VST SDK Location

  1. Create Xcode project with two frameworks. First Obj-C Framework with name AttenuatorVST2 โ€“ the processor, and second Swift Framework with name AttenuatorVST2UI โ€“ an editor.

Framework AttenuatorVST2
Framework AttenuatorVST2UI

  1. Establish dependency between AttenuatorVST2 and AttenuatorVST2UI frameworks.

โš ๏ธ Note: The framework AttenuatorVST2UI should be not only liked, but also copied into Frameworks folder inside AttenuatorVST2.

Dependencies between frameworks

Now we have scaffold of the plug-in Xcode project. You can build it. The framework AttenuatorVST2UI should be bundled inside framework AttenuatorVST.

AttenuatorVST2UI inside AttenuatorVST

Summary of this step marked with git tag Creating-Plug-In-scaffold.

Creating Processing part of Plug-In

Time to write some C++ code.

  1. Add to the framework AttenuatorVST2 two C++ classes: AttenuatorProcessor.cpp and AttenuatorEditor.cpp.
  2. Add to the framework AttenuatorVST2 one C++ file (without header): vst2sdk.cpp.
  3. Replace contents file vst2sdk.cpp with code shown below.
   #include "vstplugmain.cpp"
   #include "audioeffect.cpp"
   #include "audioeffectx.cpp"
Enter fullscreen mode Exit fullscreen mode

After adding processor sources the structure will look like shown below.

Added files to AttenuatorVST2

Xcode already showing missed file error ๐Ÿ“›. We need to update build settings for framework AttenuatorVST2. The easiest way is to create AttenuatorVST2.xcconfig file and assign it to AttenuatorVST2 target in Xcode project preferences.

// AttenuatorVST2.xcconfig

HEADER_SEARCH_PATHS = $(VST2_SDK)/**
Enter fullscreen mode Exit fullscreen mode

Added xcconfig-file to AttenuatorVST2

Attempt to build will lead to another error ๐Ÿ“› addressed compiler dialect.

/Users/Shared/Developer/libs/Audio/vstsdk2.4rev2/public.sdk/source/vst2.x/audioeffect.cpp:512:20: error: non-constant-expression cannot be narrowed from type 'int' to 'char' in initializer list [-Wc++11-narrowing]
                        char temp[2] = {'0' + (char)digit, '\0'};
Enter fullscreen mode Exit fullscreen mode

Well the VST SDK we are using was made in 2006. So, we need to downgrade compiler dialect to gnu++98 to turn it to warning ๐Ÿ˜ฌ.

// AttenuatorVST2.xcconfig

HEADER_SEARCH_PATHS = $(VST2_SDK)/**
CLANG_CXX_LANGUAGE_STANDARD = gnu++98

// To suppress doc warnings
CLANG_WARN_DOCUMENTATION_COMMENTS = NO
Enter fullscreen mode Exit fullscreen mode

Attempt to build will lead to another error ๐Ÿ“› addressed missed symbol. But this is expected since we not implemented required functions in plug-in processor yet. In order to fix we will implement simple gain plug-in. The functionality is the same as in again plug-in shipped with VST SDK in $VST2_SDK/public.sdk/samples/vst2.x/again.

// AttenuatorProcessor.hpp

#ifndef AttenuatorProcessor_hpp
#define AttenuatorProcessor_hpp

#include "audioeffectx.h"

class AttenuatorProcessor : public AudioEffectX {
public:

   AttenuatorProcessor(audioMasterCallback audioMaster);
   virtual ~AttenuatorProcessor();

   // MARK: - Processing
   virtual void processReplacing(float **inputs, float **outputs, VstInt32 sampleFrames);
   virtual void processDoubleReplacing (double** inputs, double** outputs, VstInt32 sampleFrames);

   // MARK: - Programs
   virtual void setProgramName (char* name);
   virtual void getProgramName (char* name);

   // MARK: - Parameters
   virtual void setParameterAutomated (VstInt32 index, float value);
   virtual void setParameter (VstInt32 index, float value);
   virtual float getParameter (VstInt32 index);
   virtual void getParameterLabel (VstInt32 index, char* label);
   virtual void getParameterDisplay (VstInt32 index, char* text);
   virtual void getParameterName (VstInt32 index, char* text);

   // MARK: - Metadata
   virtual bool getEffectName (char* name);
   virtual bool getVendorString (char* text);
   virtual bool getProductString (char* text);

   virtual VstInt32 getVendorVersion () { return 1000; }
   virtual VstPlugCategory getPlugCategory () { return kPlugCategEffect; }

private:
   float mGain;
   char programName[kVstMaxProgNameLen + 1];
};

#endif /* AttenuatorProcessor_hpp */
Enter fullscreen mode Exit fullscreen mode
// AttenuatorProcessor.cpp

#include "AttenuatorProcessor.hpp"

AudioEffect* createEffectInstance(audioMasterCallback audioMaster) {
   return new AttenuatorProcessor(audioMaster);
}

AttenuatorProcessor::AttenuatorProcessor(audioMasterCallback audioMaster)
: AudioEffectX(audioMaster, 1, 1) { // 1 program and 1 parameter only.
   setNumInputs (2);       // stereo in
   setNumOutputs (2);      // stereo out
   setUniqueID ('MyAg');   // identify. Kind of unique ID of plug-in.
   canProcessReplacing (); // supports replacing output
   canDoubleReplacing ();  // supports double precision processing

   mGain = 1.f;           // default to 0 dB
   vst_strncpy(programName, "Default", kVstMaxProgNameLen);   // default program name
}

AttenuatorProcessor::~AttenuatorProcessor() {
   // Nothing to do at the moment.
}

// MARK: - Processing

void AttenuatorProcessor::processReplacing (float** inputs, float** outputs, VstInt32 sampleFrames) {

   float* in1  =  inputs[0];
   float* in2  =  inputs[1];
   float* out1 = outputs[0];
   float* out2 = outputs[1];

   while (--sampleFrames >= 0) {
      (*out1++) = (*in1++) * mGain;
      (*out2++) = (*in2++) * mGain;
   }
}

void AttenuatorProcessor::processDoubleReplacing (double** inputs, double** outputs, VstInt32 sampleFrames) {

   double* in1  =  inputs[0];
   double* in2  =  inputs[1];
   double* out1 = outputs[0];
   double* out2 = outputs[1];
   double dGain = mGain;

   while (--sampleFrames >= 0) {
      (*out1++) = (*in1++) * dGain;
      (*out2++) = (*in2++) * dGain;
   }
}

// MARK: - Programs

void AttenuatorProcessor::setProgramName (char* name) {
   vst_strncpy(programName, name, kVstMaxProgNameLen);
}

void AttenuatorProcessor::getProgramName (char* name) {
   vst_strncpy(name, programName, kVstMaxProgNameLen);
}

// MARK: - Parameters

void AttenuatorProcessor::setParameter (VstInt32 index, float value) {
   mGain = value;
}

void AttenuatorProcessor::setParameterAutomated (VstInt32 index, float value) {
   AudioEffectX::setParameterAutomated(index, value);
}

float AttenuatorProcessor::getParameter (VstInt32 index) {
   return mGain;
}

void AttenuatorProcessor::getParameterName (VstInt32 index, char* label) {
   vst_strncpy(label, "Gain", kVstMaxParamStrLen);
}

void AttenuatorProcessor::getParameterDisplay (VstInt32 index, char* text) {
   dB2string(mGain, text, kVstMaxParamStrLen);
}

void AttenuatorProcessor::getParameterLabel (VstInt32 index, char* label) {
   vst_strncpy(label, "dB", kVstMaxParamStrLen);
}

// MARK: - Metadata

bool AttenuatorProcessor::getEffectName (char* name) {
   vst_strncpy(name, "Attenuator VST2", kVstMaxEffectNameLen);
   return true;
}

bool AttenuatorProcessor::getProductString (char* text) {
   vst_strncpy(text, "Examples", kVstMaxProductStrLen);
   return true;
}

bool AttenuatorProcessor::getVendorString (char* text) {
   vst_strncpy(text, "Vlad Gorlov", kVstMaxVendorStrLen);
   return true;
}
Enter fullscreen mode Exit fullscreen mode

Now we can build project. It will compiled and linked successfully.

Time to try using it in VST Host Renoise. Before doing this we need to configure Xcode build scheme and update build settings to make it VST Host compatible.

First lets update build settings in file AttenuatorVST2.xcconfig.

// AttenuatorVST2.xcconfig

HEADER_SEARCH_PATHS = $(VST2_SDK)/**
CLANG_CXX_LANGUAGE_STANDARD = gnu++98

// To suppress doc warnings
CLANG_WARN_DOCUMENTATION_COMMENTS = NO

// To make Plug-In VST Host compatible
DEPLOYMENT_LOCATION = YES
DSTROOT = $(HOME)/Library/Audio/Plug-Ins/VST/Development
INSTALL_PATH = /
SKIP_INSTALL = NO
GENERATE_PKGINFO_FILE = YES
WRAPPER_EXTENSION = vst
MACH_O_TYPE = mh_bundle

// Runtime settings
DYLIB_INSTALL_NAME_BASE = @rpath
LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks @loader_path/../Frameworks @loader_path/Frameworks
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES

// Other settings
CODE_SIGN_STYLE = Automatic
PRODUCT_NAME = $(TARGET_NAME:c99extidentifier)
DEFINES_MODULE = YES
INFOPLIST_FILE = AttenuatorVST2/Info.plist
PRODUCT_BUNDLE_IDENTIFIER = abc.example.AttenuatorVST2
COMBINE_HIDPI_IMAGES = YES
Enter fullscreen mode Exit fullscreen mode

With the settings above the build product will be created directly in folder $HOME/Library/Audio/Plug-Ins/VST which is a special system location in which VST Host can find our plug-in.

Now we can go to AttenuatorVST2 framework target and delete โŒ all build settings automatically made by Xcode.

Removing generated build settings

Also we need to change product type of AttenuatorVST2 framework target from com.apple.product-type.framework to com.apple.product-type.bundle. This can't ๐Ÿ˜ฎ be changed within Xcode, so we need to manually edit file AttenuatorVST2.xcodeproj/project.pbxproj in text editor. After editing file please close and reopen Xcode project โ€ผ๏ธ.

diff --git a/AttenuatorVST2.xcodeproj/project.pbxproj b/AttenuatorVST2.xcodeproj/project.pbxproj
index dc58c17606358ff83949c7427b85c29fed34b0c3..d40b71a6f404183f0891e0e478c4d5027832cc32 100644
--- a/AttenuatorVST2.xcodeproj/project.pbxproj
+++ b/AttenuatorVST2.xcodeproj/project.pbxproj
@@ -170,8 +170,8 @@
         );
         name = AttenuatorVST2;
         productName = AttenuatorVST2;
-        productReference = 808BBC7D2431F41F0053814A /* AttenuatorVST2.framework */;
-        productType = "com.apple.product-type.framework";
+        productReference = 808BBC7D2431F41F0053814A /* AttenuatorVST2.vst */;
+        productType = "com.apple.product-type.bundle";
      };
      808BBC8C2431F46A0053814A /* AttenuatorVST2UI */ = {
         isa = PBXNativeTarget;

Enter fullscreen mode Exit fullscreen mode

After reopening Xcode project the icon of AttenuatorVST2 product will be changed to one which represents bundle.

AttenuatorVST2 framework converted to bundle

Before trying out plug-in in VST Host we need to update Run configuration of the AttenuatorVST2 build schema in the way so that Xcode should ask us every time which application to launch.

Updating Run configuration of the AttenuatorVST2 framework

Finally we can run our plug-in in VST Host environment. We can also change Gain parameter to alter volume on Master bus in VST Host application.

AttenuatorVST2 inside Renoise

Summary of this step marked with git tag Creating-Processing-part-of-Plug-In.

Creating Editor part of Plug-In

In Xcode 11.4 there still an issue when previewing SwiftUI made for macOS when UI files placed into separate framework. Attempt to preview raises an error ๐Ÿ˜ฎ๐Ÿ“›.

 GenericHumanReadableError: unexpected error occurred

 messageRepliedWithError(
    "Connecting to launched interactive agent 11092",
    Optional(Error Domain=com.apple.dt.xcodepreviews.service Code=17 "connectToPreviewHost: Failed to connect to 11092: (null)" UserInfo={NSLocalizedDescription=connectToPreviewHost: Failed to connect to 11092: (null)})
)
Enter fullscreen mode Exit fullscreen mode

But we still can use Playground to design SwiftUI for macOS. Once SwiftUI design is done, then we can add new file with name MainUI.swift and copy/paste contents from Playground.

Slider in Playground

// MainUI.swift

import Foundation
import Combine
import SwiftUI

final class SliderData: ObservableObject {

   @Published var gain: Float = 100
}

@objc(MainView)
public class MainView: NSView {

   private let sliderData = SliderData()

   @objc public var onDidChange: ((Float) -> Void)?

   public override init(frame frameRect: NSRect) {
      super.init(frame: frameRect)
      wantsLayer = true
      layer?.backgroundColor = NSColor.lightGray.cgColor
      let view = NSHostingView(rootView: MainUI { [weak self] in
         let value = $0 / 100
         print("MainView> Value to Host: \(value)")
         self?.onDidChange?(value)
      }.environmentObject(sliderData))
      view.translatesAutoresizingMaskIntoConstraints = false
      addSubview(view)
      leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
      trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
      topAnchor.constraint(equalTo: view.topAnchor).isActive = true
      bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
   }

   public required dynamic init?(coder aDecoder: NSCoder) {
      fatalError()
   }

   @objc public func setGain(_ value: Float) {
      print("MainView> Value from Host: \(value)")
      sliderData.gain = 100 * value
   }
}

struct MainUI: View {

   @EnvironmentObject var sliderData: SliderData
   @State var gain: Float = 100

   private var onChanged: (Float) -> Void

   init(onChanged: @escaping (Float) -> Void) {
      self.onChanged = onChanged
   }

   var body: some View {
      VStack {
         Slider(value: Binding<Float>(get: { self.gain }, set: {
            self.gain = $0
            self.onChanged($0)
         }), in: 0...100, step: 2)
         Text("Gain: \(Int(gain))")
      }.onReceive(sliderData.$gain, perform: { self.gain = $0 })
   }
}
Enter fullscreen mode Exit fullscreen mode

We can't expose SwiftUI view MainUI directly into Obj-C world. That's why we need to wrap it into MainView which can be used in VST Host environment. Here is how MainView exposed to Obj-C. It has method setGain and callback onDidChange which together allow to keep plug-in UI and VST Host in sync.

SWIFT_CLASS_NAMED("MainView")
@interface MainView : NSView
@property (nonatomic, copy) void (^ _Nullable onDidChange)(float);
- (nonnull instancetype)initWithFrame:(NSRect)frameRect OBJC_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder * _Nonnull)aDecoder OBJC_DESIGNATED_INITIALIZER;
- (void)setGain:(float)value;
@end
Enter fullscreen mode Exit fullscreen mode

In order to use Obj-C from C++ we need to rename file AttenuatorEditor.cpp to AttenuatorEditor.mm. By doing this we informing Xcode that C++ class AttenuatorEditor in now type of Obj-C++ in which Obj-C and C++ can coexist together ๐Ÿค.

// AttenuatorEditor.hpp

#ifndef AttenuatorEditor_hpp
#define AttenuatorEditor_hpp

#include "aeffeditor.h"

// Forward declarations.
#ifdef __OBJC__
@class MainView;
#else
typedef struct objc_object MainView;
#endif

class AttenuatorEditor : public AEffEditor
{
public:
   AttenuatorEditor (AudioEffect* ptr);
   virtual ~AttenuatorEditor();

   virtual bool open (void* ptr);
   virtual void close ();
   virtual bool getRect (ERect** rect);

   virtual void setParameter (VstInt32 index, float value);

private:

   ERect   rect;
   MainView* mView;
};

#endif /* AttenuatorEditor_hpp */
Enter fullscreen mode Exit fullscreen mode
// AttenuatorEditor.mm

#include "AttenuatorEditor.hpp"
#import <Cocoa/Cocoa.h>
#import <AttenuatorVST2UI/AttenuatorVST2UI-Swift.h>

AttenuatorEditor::AttenuatorEditor(AudioEffect *effect) : AEffEditor (effect)  {
   mView = nil;
   effect->setEditor(this);
   this->rect.top = 0;
   this->rect.left = 0;
   this->rect.right = 200;
   this->rect.bottom = 100;
}

AttenuatorEditor::~AttenuatorEditor() {
   mView = nil;
}

bool AttenuatorEditor::open (void* ptr) {
   AEffEditor::open(ptr);

   NSView * parent = (__bridge NSView*)ptr;
   // Should be same dimensions as in `rect` property.
   NSRect proxyRect = NSMakeRect(0, 0, parent.bounds.size.width, parent.bounds.size.height);
   MainView * plugInView = [ [MainView alloc] initWithFrame: proxyRect];
   plugInView.translatesAutoresizingMaskIntoConstraints = true;
   plugInView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
   [parent addSubview: plugInView];
   [plugInView setNeedsDisplay:true];
   mView = plugInView;
   plugInView.onDidChange = ^(float gain) {
      effect->setParameterAutomated(0, gain); // We have only one parameter. Passing `0` as index.
   };
   [plugInView setGain: effect->getParameter(0)];
   return true;
}

void AttenuatorEditor::close() {
   [mView removeFromSuperview];
   mView = nil;
}

bool AttenuatorEditor::getRect(ERect** inRect) {
   *inRect = &rect;
   return true;
}

void AttenuatorEditor::setParameter(VstInt32 index, float value) {
   // Ignoring `index`. We have only one parameter.
   dispatch_async(dispatch_get_main_queue(), ^{
      [mView setGain: value];
   });
}
Enter fullscreen mode Exit fullscreen mode

In short: Inside function call bool AttenuatorEditor::open(void* ptr) VST Host provides us pointer to NSView *. We are using that view to attach plug-in UI.

Attempt to build the project will raise various errors addressed build configuration ๐Ÿ“›๐Ÿ˜ฌ. The easiest way to fix is to create AttenuatorVST2UI.xcconfig file and remove build settings for AttenuatorVST2UI automatically created by Xcode.


// AttenuatorVST2UI.xcconfig

INFOPLIST_FILE = AttenuatorVST2UI/Info.plist
PRODUCT_BUNDLE_IDENTIFIER = abc.example.AttenuatorVST2UI
PRODUCT_NAME = $(TARGET_NAME:c99extidentifier)
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES

CODE_SIGN_STYLE = Automatic
SWIFT_INSTALL_OBJC_HEADER = YES
DEFINES_MODULE = YES
SWIFT_VERSION = 5.0

COMBINE_HIDPI_IMAGES = YES
SKIP_INSTALL = YES
DYLIB_COMPATIBILITY_VERSION = 1
DYLIB_CURRENT_VERSION = 1
DYLIB_INSTALL_NAME_BASE = @rpath
LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks @loader_path/Frameworks
Enter fullscreen mode Exit fullscreen mode

File AttenuatorVST2UI.xcconfig in Project settings

Now all build errors are gone. We can start consuming AttenuatorEditor in AttenuatorProcessor by implementing changes shown below.

// AttenuatorProcessor.hpp

class AttenuatorProcessor : public AudioEffectX {
public:

   // Existing declarations skipped. They are not changed.

private:
   bool mIsUpdatingGain; // 1๏ธโƒฃ Used to avoid parameter change loopback.
   float mGain;
   char programName[kVstMaxProgNameLen + 1];
};
Enter fullscreen mode Exit fullscreen mode
// AttenuatorProcessor.cpp

#include "AttenuatorProcessor.hpp"
#include "AttenuatorEditor.hpp" // 1๏ธโƒฃ Imported new header.

AttenuatorProcessor::AttenuatorProcessor(audioMasterCallback audioMaster)
: AudioEffectX(audioMaster, 1, 1) { // 1 program and 1 parameter only.
   setNumInputs (2);       // stereo in
   setNumOutputs (2);      // stereo out
   setUniqueID ('MyAg');   // identify. Kind of unique ID of plug-in.
   canProcessReplacing (); // supports replacing output
   canDoubleReplacing ();  // supports double precision processing

   mGain = 1.f;           // default to 0 dB
   vst_strncpy(programName, "Default", kVstMaxProgNameLen);   // default program name

   this->setEditor (new AttenuatorEditor(this)); // 2๏ธโƒฃ Using editor.
}

void AttenuatorProcessor::setParameterAutomated (VstInt32 index, float value) {
   mIsUpdatingGain = true; // 3๏ธโƒฃ Needed to avoid loopback.
   AudioEffectX::setParameterAutomated(index, value);
   mIsUpdatingGain = false; // 3๏ธโƒฃ Needed to avoid loopback.
}

void AttenuatorProcessor::setParameter (VstInt32 index, float value) {
   mGain = value;
   bool shouldUpdate = editor && editor->isOpen() && !mIsUpdatingGain;
   if(shouldUpdate) {
      ((AttenuatorEditor *)editor)->setParameter(index, value); // 4๏ธโƒฃ Updating UI from VST Host.
   }
}
Enter fullscreen mode Exit fullscreen mode

That's it ๐Ÿ™‚. Now we can run our plug-in in VST Host environment. Now VST Host shows Ext. Editor button which means that our plugin has custom UI.

SwiftUI in VST2 Plug-In

We can change Gain in VST Host or in Plug-In UI. Changes are in sync. In another VST Host, the Reaper, behavior is similar.

SwiftUI in VST2 Plug-In

Summary of this step marked with git tag Creating-Editor-part-of-Plug-In.

Good luck with SwiftUI and VST ๐Ÿ‘‹!

Sources of Plug-In can be found at GitHub.

Top comments (0)