I'm setting up POC to imitate DAW (nano) with LV2 loaded.
The only way I could get uirelays running on separate thread was to initialise in main. I ran the updates in separate thread. which I guess is how daws work.
Main is DAW (host) in my example. I think this should be okay as DAW runs many Lv2 and I understand it provides orchestration of Lv2 GUI / Audio thread.
I took the hello world example from uirealy and split it apart.
The last thing I'm not sure of how can I pass data, lets says a parameter float value for gain - from UI thread to Audio thread?
Bear in mind in a real daw there will several Lv2/VST but they will handle non-blocking audio thread as well as data transfer - I expect it will need to handle something for databinding from UI to LV2 audio, VST audio and so on. Eventually Many to Many but happy to work with abstact LV2 for now with 1 audio thread.
With this example, gain value from UI thread to Audio thread.
I appreciate this UI lib uses callbacks so thinking of hooking into handleEvent() to pass values. LV2 uses Atom messages with ports to communicate with host - the host sends the parameter to Audio DSP.
import uirelays
# Global state
var layout: ScreenLayout
var width, height: int
var fm: FontMetrics
var font: Font
var mouseX, mouseY = 0
var clickMsg = "Click anywhere"
proc initPlugin*() =
## Called once when plugin loads
layout = createWindow(640, 480)
width = layout.width
height = layout.height
font = openFont("", 18, fm) # empty path = platform default font
setWindowTitle("Hello uirelays")
proc handleEvents*() =
## Call this when host sends events (mouse, keyboard, etc.)
var e: Event
while pollEvent(e):
case e.kind
of QuitEvent, WindowCloseEvent:
discard # Host controls this
of WindowResizeEvent:
width = e.x
height = e.y
of MouseDownEvent:
mouseX = e.x
mouseY = e.y
clickMsg = "Clicked at " & $e.x & ", " & $e.y
if e.button == RightButton:
clickMsg &= " (right)"
of KeyDownEvent:
if e.key == KeyEsc or (e.key == KeyQ and CtrlPressed in e.mods):
discard # Host controls quit
else: discard
proc drawFrame*() =
## Call this when host requests a redraw
let bg = color(30, 30, 46)
let fg = color(205, 214, 244)
let accent = color(137, 180, 250)
let pink = color(245, 194, 231)
let green = color(166, 227, 161)
# background
fillRect(rect(0, 0, width, height), bg)
# title bar
fillRect(rect(0, 0, width, 40), color(49, 50, 68))
discard drawText(font, 12, 10, "Hello, uirelays!", fg, color(49, 50, 68))
# colored boxes
fillRect(rect(40, 60, 120, 80), accent)
fillRect(rect(180, 60, 120, 80), pink)
fillRect(rect(320, 60, 120, 80), green)
# labels
discard drawText(font, 60, 90, "Accent", bg, accent)
discard drawText(font, 205, 90, "Pink", bg, pink)
discard drawText(font, 345, 90, "Green", bg, green)
# diagonal lines
for i in 0 ..< 8:
let x = 40 + i * 20
drawLine(x, 170, x + 100, 270, accent)
# click feedback
discard drawText(font, 40, 300, clickMsg, fg, bg)
# instructions
discard drawText(font, 40, height - 40,
"Press ESC or Ctrl+Q to quit", color(128, 128, 148), bg)
refresh()
proc shutdownPlugin*() =
## Called when plugin unloads
closeFont(font)
shutdown()
# --- VST Simulation: Main Thread for DAW, UI and Audio on Worker Threads ---
when isMainModule:
var uiThread: Thread[void]
var audioThread: Thread[void]
var audioRunning = true
var uiRunning = true
proc uiProcessor() {.gcsafe.} =
echo " → UI Thread: Running GUI event loop..."
var frameCount = 0
# UI event loop on worker thread (init already done on main)
while uiRunning and frameCount < 300:
{.cast(gcsafe).}:
handleEvents()
drawFrame()
{.cast(gcsafe).}:
sleep(16)
frameCount.inc
echo " → UI Thread: Complete"
proc audioProcessor() {.gcsafe.} =
echo " → Audio Thread: Started"
var sampleCount = 0
while audioRunning and sampleCount < 5000:
# Simulate audio processing
sampleCount += 1024
#How can I pass a parameter to the audio thread from the UI thread? float parameter based on a UI interaction say gain value.
# Real-time audio work (non-blocking)
{.cast(gcsafe).}:
sleep(8)
echo " → Audio Thread: Complete (processed " & $sampleCount & " samples)"
proc main() =
echo "Simulating VST Plugin...running in daw."
echo ""
echo "Thread Layout:"
echo " → Main Thread: DAW/VST Host (reserved)"
# Initialize uirelays on MAIN thread (requirement)
echo ""
echo " → Initializing plugin on main thread..."
initPlugin()
# Spawn UI thread
echo " → Spawning UI thread..."
createThread(uiThread, uiProcessor)
# Spawn audio thread
echo " → Spawning audio thread..."
createThread(audioThread, audioProcessor)
# Main thread is now available for DAW/VST host
echo ""
echo " Main thread: Available for DAW/VST host callbacks"
sleep(6000) # ~6 seconds
# Signal threads to stop
echo ""
echo " → Signaling worker threads to shutdown..."
uiRunning = false
audioRunning = false
# Wait for both threads to finish
joinThread(uiThread)
joinThread(audioThread)
# Shutdown on main thread
echo " → Shutting down plugin on main thread..."
shutdownPlugin()
echo ""
echo "VST plugin simulation complete."
main()
AI gave me something like this for LV2 : I'll have a play real Lv2 effect.
# Minimal LV2 UI in Nim that sends a float Atom to the DSP
{.passL: "-llv2".}
# --- LV2 C types (simplified) ----------------------------------------------
type
LV2UI_Controller* = pointer
LV2UI_Write_Function* = proc(
controller: LV2UI_Controller,
port_index: uint32,
buffer_size: uint32,
protocol: uint32,
buffer: pointer
): uint32 {.cdecl.}
LV2_Atom* = object
size*: uint32
type*: uint32
LV2_Atom_Float* = object
atom*: LV2_Atom
body*: float32
# --- UI Instance ------------------------------------------------------------
type
UIInstance* = ref object
controller*: LV2UI_Controller
writeFn*: LV2UI_Write_Function
portIndex*: uint32
uridFloat*: uint32
# --- Send a float Atom ------------------------------------------------------
proc sendFloat*(ui: UIInstance; value: float32) =
var atom: LV2_Atom_Float
atom.atom.size = 4
atom.atom.type = ui.uridFloat
atom.body = value
discard ui.writeFn(
ui.controller,
ui.portIndex,
sizeof(atom).uint32,
0, # protocol = 0 for raw atom
addr atom
)
# --- LV2 UI Entry Point -----------------------------------------------------
proc lv2ui_instantiate*(
descriptor: cstring,
plugin_uri: cstring,
bundle_path: cstring,
write_function: LV2UI_Write_Function,
controller: LV2UI_Controller,
widget: ptr pointer,
features: ptr pointer
): pointer {.exportc.} =
# Create UI instance
let ui = UIInstance(
controller: controller,
writeFn: write_function,
portIndex: 0'u32, # your plugin's Atom input port index
uridFloat: 1'u32 # host-mapped URID for atom:Float
)
# No GUI widget (headless UI)
widget[] = nil
# Send a test float immediately
ui.sendFloat(0.75)
return cast[pointer](ui)
# --- Cleanup ----------------------------------------------------------------
proc lv2ui_cleanup*(uiPtr: pointer) {.exportc.} =
discard
# --- Optional: Port event handler ------------------------------------------
proc lv2ui_port_event*(
uiPtr: pointer,
port_index: uint32,
buffer_size: uint32,
format: uint32,
buffer: pointer
) {.exportc.} =
# DSP → UI messages would be handled here
discard