Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/cmake)

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_C_STANDARD 11)
option(SL_BUILD_VST3 "Build the VST3 plug-in" OFF)

if(SL_BUILD_VST3)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
endif()

if(MSVC)
add_compile_options("/W3")
Expand Down Expand Up @@ -87,22 +92,21 @@ set(STATIC
CACHE BOOL "")

set(MODULES
alsa
auconv
auresamp
vp8
coreaudio
dtls_srtp
g711
ice
jack
netroam
opus
portaudio
pulse
turn
CACHE STRING "")

if(NOT SL_BUILD_VST3)
list(APPEND MODULES alsa coreaudio jack pulse portaudio)
endif()

# Baresip dependencies
add_subdirectory(external/re EXCLUDE_FROM_ALL)
add_subdirectory(external/baresip EXCLUDE_FROM_ALL)
Expand All @@ -112,6 +116,11 @@ find_package(re CONFIG REQUIRED)

add_compile_definitions(SL_VERSION="${PROJECT_VERSION}")


add_subdirectory(libsl)
add_subdirectory(test)
add_subdirectory(app/cli)

if(SL_BUILD_VST3)
add_subdirectory(app/vst3)
endif()
21 changes: 20 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ third_party/portaudio:
$(HIDE)cd third_party && \
git clone ${PORTAUDIO_MIRROR}/portaudio.git && \
cd portaudio && \
cmake -B build -DBUILD_SHARED_LIBS=0 \
cmake -B build -DBUILD_SHARED_LIBS=0 -DPA_USE_SNDIO=OFF \
-DPA_USE_WMME=OFF -DPA_USE_DS=OFF -DPA_USE_WDMKS=OFF && \
cmake --build build -j && \
cp -a build/libportaudio.a ../lib/ && \
Expand Down Expand Up @@ -205,6 +205,25 @@ external/baresip:
patch -p1 < ../../patches/baresip_portaudio.patch


.PHONY: vst3sdk
vst3sdk: external/vst3sdk

# Only the submodules the plug-in actually needs. VSTGUI is skipped, the
# plug-in has no editor of its own.
external/vst3sdk:
$(HIDE) [ ! -d external/vst3sdk ] && \
git -C external clone -b $(VST3SDK_VERSION) --depth=1 \
$(VST3SDK_MIRROR) vst3sdk && \
cd external/vst3sdk && \
git submodule update --init --depth=1 \
base cmake pluginterfaces public.sdk

.PHONY: vst3
vst3: third_party external external/vst3sdk
$(HIDE)[ -d build ] || cmake -B build -GNinja \
-DCMAKE_BUILD_TYPE=Release -DSL_BUILD_VST3=ON
$(HIDE)cmake --build build -j $(CMAKE_VERBOSE) --target studiolink_vst3

##############################################################################
#
# Tools & Cleanup
Expand Down
137 changes: 137 additions & 0 deletions app/vst3/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#
# CMakeLists.txt
#
# StudioLink VST3 plug-in
#
# Copyright (C) 2026 Sebastian Reimers
#

##############################################################################
#
# Project and Versioning
#
project(studiolink_vst3 VERSION 26.01.0 LANGUAGES C CXX)

set(SL_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../..)
set(vst3sdk_SOURCE_DIR ${SL_ROOT}/external/vst3sdk)

if(NOT EXISTS ${vst3sdk_SOURCE_DIR}/CMakeLists.txt)
message(STATUS "VST3 SDK not found in external/vst3sdk"
" - skipping VST3 plug-in (run: make vst3sdk)")
return()
endif()

##############################################################################
#
# Compile options
#
# The top level CMakeLists.txt adds a set of C only warning flags. They would
# be rejected when compiling the C++ sources of the SDK, so start from a
# clean slate here and re-apply them per language below.
#

set_directory_properties(PROPERTIES COMPILE_OPTIONS "")

set(SL_VST_C_FLAGS
-Wall
-Wextra
-Wmissing-declarations
-Wmissing-prototypes
-Wold-style-definition
-Wshadow
-Wstrict-prototypes
-Wuninitialized
-Wvla)

if(MSVC)
set(SL_VST_C_FLAGS "/W3")
endif()

##############################################################################
#
# VST3 SDK
#
# Examples, hosting tools and VSTGUI are not needed: StudioLink is operated
# through its web frontend, so the plug-in ships without an editor.
#

set(SMTG_ENABLE_VST3_PLUGIN_EXAMPLES OFF CACHE BOOL "" FORCE)
set(SMTG_ENABLE_VST3_HOSTING_EXAMPLES OFF CACHE BOOL "" FORCE)
set(SMTG_ENABLE_VSTGUI_SUPPORT OFF CACHE BOOL "" FORCE)
set(SMTG_CREATE_PLUGIN_LINK OFF CACHE BOOL "" FORCE)
set(SMTG_RUN_VST_VALIDATOR OFF CACHE BOOL "" FORCE)

list(APPEND CMAKE_MODULE_PATH ${vst3sdk_SOURCE_DIR}/cmake/modules)

add_subdirectory(${vst3sdk_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}/vst3sdk
EXCLUDE_FROM_ALL)

smtg_enable_vst3_sdk()

##############################################################################
#
# Source section
#

find_package(SampleRate REQUIRED)

# vstsinglecomponenteffect.cpp is not part of the "sdk" library target and
# has to be compiled into the plug-in, like the SDK samples do.
set(SRCS
${vst3sdk_SOURCE_DIR}/public.sdk/source/vst/vstsinglecomponenteffect.cpp
src/driver.c
src/engine.c
src/io.c
src/entry.cpp
src/plugin.cpp
)

set(HEADERS
include/sl_vst.h
src/cids.h
src/plugin.h
src/version.h
)

##############################################################################
#
# Main target
#

set(target studiolink_vst3)

smtg_add_vst3plugin(${target} PACKAGE_NAME "StudioLink" SOURCES_LIST ${SRCS}
${HEADERS})

target_include_directories(${target}
PRIVATE
include
${SAMPLERATE_INCLUDE_DIR}
)

target_link_libraries(${target}
PRIVATE
sdk
sl
baresip
re
${SAMPLERATE_LIBRARIES}
)

target_compile_definitions(${target} PRIVATE ${RE_DEFINITIONS})

target_compile_features(${target} PRIVATE cxx_std_17)

target_compile_options(${target}
PRIVATE
$<$<COMPILE_LANGUAGE:C>:${SL_VST_C_FLAGS}>
)

if(SMTG_MAC)
smtg_target_set_bundle(${target}
BUNDLE_IDENTIFIER "de.studio-link.vst3.studiolink"
COMPANY_NAME "Studio Link"
)
elseif(SMTG_WIN)
target_sources(${target} PRIVATE resource/studiolink.rc)
endif()
111 changes: 111 additions & 0 deletions app/vst3/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# StudioLink VST3 Plug-in

Runs StudioLink inside a DAW. The DAW replaces the sound card: the plug-in
input is sent to the remote peers, the plug-in output carries the mix of all
remote peers.

```
DAW track input ──► local StudioLink track ──► remote peers (SIP/Opus)
DAW track output ◄── aumix of all remote tracks
```

The plug-in has no editor. StudioLink is operated through its existing web
frontend, which the "Open Web UI" parameter launches.

## Build

```bash
make vst3sdk # fetch external/vst3sdk (once)
make vst3
```

The result is `build/VST3/<config>/StudioLink.vst3`. Copy it to your VST3
folder (`~/.vst3` on Linux, `~/Library/Audio/Plug-Ins/VST3` on macOS,
`%COMMONPROGRAMFILES%\VST3` on Windows).

The plug-in is off by default in the normal build; `-DSL_BUILD_VST3=ON`
enables it.

## How it hooks into libsl

`libsl` picks the audio device driver for the local track from
`sl_conf()->play.mod` / `src.mod`, which defaults to `portaudio`. The plug-in
registers a baresip `ausrc`/`auplay` pair named `vst` and points the config at
it *before* `sl_init()` runs, because the local track chooses its driver
during `sl_tracks_init()`. From there on everything else in libsl is
unchanged.

| File | Role |
| --- | --- |
| `src/engine.c` | Shared, reference counted StudioLink instance running `re_main()` on its own thread |
| `src/driver.c` | The `vst` baresip driver and the two ring buffers to the audio thread |
| `src/io.c` | Per instance channel mapping, format and samplerate conversion |
| `src/resample.c` | libsamplerate wrapper for non 48 kHz hosts |
| `src/plugin.cpp` | VST3 `SingleComponentEffect` |
| `src/entry.cpp` | VST3 module factory |

### Threading

Three threads are involved:

- **DAW audio thread** — `process()` converts, pushes into `ab_in`, drives the
20 ms frame exchange with baresip, pulls from `ab_out`. It never allocates
and never blocks: the driver lock is taken with `mtx_trylock()` and simply
skipped when the StudioLink thread is restarting the driver, in which case
`aubuf` zero fills for a few blocks.
- **StudioLink thread** — `re_main()`, same as the CLI app.
- **DAW GUI thread** — parameter edits. These are marshalled onto the
StudioLink thread through an `mqueue`, since libre objects must only be
touched from there. Commands are only sent on actual value changes, so the
audio thread stays free of syscalls in the steady state.

This is the same arrangement baresip's own `portaudio` module uses, where the
read/write handlers are likewise called from a foreign callback thread into
lock protected `aumix`/`aubuf` objects.

### Clock drift

The DAW clock and the aumix thread's OS clock drift apart just like a sound
card and the OS clock do. The correction lives in libsl (`libsl/src/drift.c`,
applied in `driver_write_handler`), because the buffer on that clock boundary
is `ab_mix` inside libsl either way, so the plug-in inherits it for free.

### Samplerate

StudioLink is fixed at 48 kHz stereo S16LE with 20 ms frames. libre's
`auresamp` only handles integer ratios, so 44.1 kHz hosts would be rejected —
`libsamplerate` is used instead (`libsl/src/resample.c`, shared with the
drift correction), with
`SRC_SINC_FASTEST` and preallocated state so `src_process()` does not allocate
on the audio thread. At 48 kHz the resampler degenerates to a copy.

## Known limitations

- **One active instance per host process.** libsl currently supports a single
local track (`tracks.c`: *TODO: refactor allow multiple local tracks*) and
keeps one global libre loop, HTTP server and track list. The first instance
claims the audio bridge; any further instance loads but stays silent. Lifting
this needs multi local track support in libsl first.
- **Stereo in / stereo out only.** Mono input is duplicated to both sides.
- **32 bit float processing only.** Double precision would only add
conversions on the way to 16 bit.
- **Latency is not reported.** The delay is a network path, so the host cannot
compensate it; `getLatencySamples()` returns 0 and the plug-in declares an
infinite tail so hosts keep calling `process()` while remote peers are
talking.
- `sl_open_webui()` launches Chromium with fixed flags on non-Windows hosts. A
`sl_http_port()` getter in libsl would let the plug-in open the URL with the
platform default browser instead.
- `sl_baresip_init()` calls `setbuf(stdout, NULL)` and `sys_coredump_set(true)`
process wide, which is fine for a standalone app but intrusive inside a host
process.

## Licensing

This repository is MIT. The Steinberg VST3 SDK is **not** — it is dual
licensed under GPLv3 or Steinberg's proprietary licence, and it is fetched
into `external/` rather than vendored. Distributing binaries of this plug-in
means accepting one of those two, and a GPLv3 build would relicense the
combined work. Steinberg also requires registering as a VST3 licensee before
shipping. This is a maintainer decision, not something the build system can
settle.
Loading
Loading