diff options
77 files changed, 4567 insertions, 2716 deletions
diff --git a/.clang-format b/.clang-format index 4692c52..d3ca92c 100644 --- a/.clang-format +++ b/.clang-format @@ -1,7 +1,10 @@ --- AlignConsecutiveAssignments: true AlignConsecutiveDeclarations: true -AlignEscapedNewlinesLeft: true +AlignEscapedNewlines: Left +AttributeMacros: + - ZIX_MALLOC_FUNC + - ZIX_PURE_FUNC BasedOnStyle: Mozilla BraceWrapping: AfterNamespace: false @@ -14,14 +17,19 @@ BraceWrapping: SplitEmptyRecord: false BreakBeforeBraces: Custom Cpp11BracedListStyle: true +FixNamespaceComments: true ForEachMacros: - LILV_FOREACH - - foreach + - LV2_ATOM_OBJECT_BODY_FOREACH + - LV2_ATOM_OBJECT_FOREACH + - LV2_ATOM_SEQUENCE_BODY_FOREACH + - LV2_ATOM_SEQUENCE_FOREACH + - LV2_ATOM_TUPLE_BODY_FOREACH + - LV2_ATOM_TUPLE_FOREACH IndentCaseLabels: false IndentPPDirectives: AfterHash KeepEmptyLinesAtTheStartOfBlocks: false SpacesInContainerLiterals: false StatementMacros: - Q_OBJECT - - _Pragma ... diff --git a/.clang-tidy b/.clang-tidy index 6262c89..dd52ffc 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,39 +1,42 @@ Checks: > *, -*-c-arrays, + -*-macro-to-enum, -*-magic-numbers, -*-named-parameter, -*-narrowing-conversions, -altera-*, + -boost-*, -bugprone-assignment-in-if-condition, - -bugprone-branch-clone, + -bugprone-casting-through-void, -bugprone-easily-swappable-parameters, - -bugprone-suspicious-realloc-usage, + -bugprone-multi-level-implicit-pointer-conversion, -cert-err33-c, -cert-err34-c, + -clang-analyzer-optin.core.EnumCastOutOfRange, -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling, -clang-analyzer-valist.Uninitialized, -concurrency-mt-unsafe, -cppcoreguidelines-avoid-non-const-global-variables, + -cppcoreguidelines-macro-usage, -cppcoreguidelines-owning-memory, -cppcoreguidelines-pro-bounds-pointer-arithmetic, -cppcoreguidelines-pro-type-reinterpret-cast, - -cppcoreguidelines-pro-type-static-cast-downcast, -fuchsia-default-arguments-calls, -google-readability-todo, - -hicpp-multiway-paths-covered, -hicpp-signed-bitwise, -llvm-header-guard, -llvmlibc-*, + -misc-include-cleaner, -misc-no-recursion, - -misc-use-anonymous-namespace, - -modernize-macro-to-enum, -modernize-use-nodiscard, -modernize-use-trailing-return-type, + -modernize-use-using, + -performance-enum-size, -readability-function-cognitive-complexity, -readability-identifier-length, -readability-implicit-bool-conversion, - -readability-non-const-parameter, + -readability-redundant-casting, -readability-static-accessed-through-instance, CheckOptions: - key: hicpp-uppercase-literal-suffix.NewSuffixes diff --git a/.clant.json b/.clant.json index 6f11716..8347642 100644 --- a/.clant.json +++ b/.clant.json @@ -1,4 +1,4 @@ { "version": "1.0.0", - "mapping_files": ["qt5_11.imp"] + "mapping_files": [".includes.imp", "qt5_11.imp"] } @@ -1,14 +1,13 @@ -# Copyright 2019-2022 David Robillard <d@drobilla.net> +# Copyright 2019-2025 David Robillard <d@drobilla.net> # SPDX-License-Identifier: 0BSD OR ISC -build/ -subprojects/lilv/ -subprojects/lv2/ -subprojects/packagecache/ -subprojects/serd/ -subprojects/sord/ -subprojects/sphinxygen-1.0.4/ -subprojects/sratom/ -subprojects/suil/ -subprojects/zix-0.4.0/ -subprojects/zix/ +/build/ +/subprojects/lilv-0.24.26/ +/subprojects/lv2/ +/subprojects/packagecache/ +/subprojects/serd/ +/subprojects/sord-0.16.18/ +/subprojects/sphinxygen-1.0.10/ +/subprojects/sratom-0.6.18/ +/subprojects/suil-0.10.22/ +/subprojects/zix-0.6.2/ diff --git a/.includes.imp b/.includes.imp new file mode 100644 index 0000000..9ec9064 --- /dev/null +++ b/.includes.imp @@ -0,0 +1,7 @@ +[ + { "symbol": [ "QT_VERSION", "private", "<QtGlobal>", "public" ] }, + { "symbol": [ "QT_VERSION_CHECK", "private", "<QtGlobal>", "public" ] }, + { "symbol": [ "Q_OBJECT", "private", "<QObject>", "public" ] }, + { "symbol": [ "Q_SLOT", "private", "<QObject>", "public" ] }, + { "symbol": [ "qMax", "private", "<QtGlobal>", "public" ] } +] diff --git a/.reuse/dep5 b/.reuse/dep5 index 110b66c..5888ff3 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -3,7 +3,7 @@ Upstream-Name: jalv Upstream-Contact: David Robillard <d@drobilla.net> Source: https://gitlab.com/drobilla/jalv -Files: .clang* .clant.json .gitignore AUTHORS NEWS jalv.desktop.in jalv.ttl +Files: .clang* .clant.json .gitignore .includes.imp .suppress.cppcheck AUTHORS NEWS jalv.desktop.in jalv.ttl Copyright: 2010-2022 David Robillard <d@drobilla.net> License: 0BSD OR ISC diff --git a/.suppress.cppcheck b/.suppress.cppcheck new file mode 100644 index 0000000..db432cb --- /dev/null +++ b/.suppress.cppcheck @@ -0,0 +1,3 @@ +cstyleCast +normalCheckLevelMaxBranches +unusedStructMember @@ -1,17 +1,20 @@ Author: David Robillard <d@drobilla.net> -Original GTK2 Generic Plugin UI: +Original GTK generic UI: Nick Lanham <nick@afternight.org> -Various UI improvements: +JACK latency support and other improvements: Robin Gareus <robin@gareus.org> Preset menu support for Qt: Timo Westkämper <timo.westkamper@gmail.com> -Qt4 Generic Plugin UI: +Original Qt generic UI: Amadeus Folego <amadeusfolego@gmail.com> -Gtk Plugin Selector UI: - Alexandros Theodotou <alex@zrythm.org>
\ No newline at end of file +GTK plugin selector UI: + Alexandros Theodotou <alex@zrythm.org> + +Original man pages: + Jaromír Mikes <mira.mikes@seznam.cz> @@ -1,15 +1,30 @@ -jalv (1.6.9) stable; urgency=medium +jalv (1.6.9) unstable; urgency=medium + * Add Qt6 version * Add missing short versions of command line options + * Add option to install tool man pages + * Add support for control outputs with lv2:latency designation * Build Qt UI with -fPIC + * Clean up and strengthen code * Clean up command line help output + * Cleanly separate audio thread from the rest of the application + * Fix Jack latency recomputation when plugin latency changes * Fix clashing command line options + * Fix minor memory leaks + * Make help and version commands exit successfully + * Only send control messages to designated lv2:control ports + * Reduce Jack process callback overhead * Remove Gtk2 interface + * Remove limits on the size of messages sent from plugin to UI + * Remove transport position dumping from Jack process callback * Replace use of deprecated Gtk interfaces + * Rewrite man pages in mdoc * Switch to external zix dependency * Use Gtk switches instead of checkboxes for toggle controls + * Use fewer platform-specific APIs + * Use portable zix filesystem API - -- David Robillard <d@drobilla.net> Sun, 11 Dec 2022 21:51:58 +0000 + -- David Robillard <d@drobilla.net> Fri, 20 Dec 2024 00:45:28 +0000 jalv (1.6.8) stable; urgency=medium @@ -1,84 +1,125 @@ -.TH JALV 1 "18 Feb 2017" - -.SH NAME -.B jalv \- Run an LV2 plugin as a JACK application (console version). - -.SH SYNOPSIS -.B jalv [OPTION]... PLUGIN_URI - -.SH OPTIONS - -.TP -\fB\-b SIZE\fR -Buffer size for plugin <=> UI communication. - -.TP -\fB\-c SYM=VAL\fR -Set control value (e.g. "vol=1.4"). - -.TP -\fB\-d\fR -Dump plugin <=> UI communication. - -.TP -\fB\-U URI\fR -Load the UI with the given URI. - -.TP -\fB\-h\fR -Print the command line options. - -.TP -\fB\-i\fR -Ignore input on stdin (for background use). - -.TP -\fB\-l DIR\fR -Load state from state directory. - -.TP -\fB\-n NAME\fR -Jack client name - -.TP -\fB\-p\fR -Print control output changes to stdout. - -.TP -\fB\-s\fR +.\" # Copyright 2024 David Robillard <d@drobilla.net> +.\" # SPDX-License-Identifier: ISC +.Dd December 20, 2024 +.Dt JALV 1 +.Os +.Sh NAME +.Nm jalv +.Nd run an LV2 plugin with a command-line interface +.Sh SYNOPSIS +.Nm jalv +.Op Fl dhipstx +.Op Fl b Ar size +.Op Fl c Ar symbol=value +.Op Fl U Ar ui_uri +.Op Fl l Ar dir +.Op Fl n Ar name +.Ar plugin_uri +.Sh DESCRIPTION +.Nm +is a simple LV2 host that runs one plugin. +It has several versions, this one has an interactive command-line interface. +.Pp +.Nm +has one positional argument, the URI of an installed LV2 plugin. +.Pp +The options are as follows: +.Bl -tag -width 3n +.It Fl b Ar bytes +Buffer size for communication between plugin and UI. +The default value should be enough, +but if there are overflows, +this option can be used to allocate more space. +.It Fl c Ar symbol=value +Set control value, for example, +.Fl c Ar vol=1.4 +where +.Dq vol +is the symbol of a control port on the plugin. +.It Fl d +Dump communication between plugin and UI to +.Dv stdout . +Note that this may print an extreme amount of text, +piping the output to a pager or file is recommended. +.It Fl h +Print the command line options and exit. +.It Fl i +Ignore input on +.Dv stdin +and run non-interactively. +.It Fl l Ar dir +Load state from the given directory before running the plugin. +.It Fl n Ar name +Use the given JACK client name. +Note that JACK may adjust the name if necessary unless +.Fl x +is also given. +.It Fl p +Print control output changes to +.Dv stdout . +.It Fl s Show plugin UI if possible. - -This option only works when plugins provide a UI that is usable via the non-embeddable showHide interface. For other, embeddable UIs, use jalv.gtk3(1) or jalv.qt5(1). - -.TP -\fB\-t\fR -Print trace messages from plugin - -.TP -\fB\-x\fR -Use only exact Jack client name, and exit if it is taken - -.SH COMMANDS - +This option only works when plugins provide a UI that uses the non-embeddable +.Li showHide +interface. +For embeddable UIs, use +.Xr jalv.gtk3 1 +instead. +.It Fl t +Print trace messages from plugin. +This enables the +.Dq trace +log defined by LV2, which is used by some plugins to print debugging output. +.It Fl U Ar uri +Load the UI with the given URI. +Usually only one suitable UI is available on a given platform, +which is used by default. +If there are several, this option can be used to select which is loaded. +.It Fl V +Print version information and exit. +.It Fl x +Use only the exact JACK client name given by +.Fl n +or exit if it's unavailable. +.El +.Sh COMMANDS The Jalv prompt supports several commands for interactive control: - - \fBhelp\fR Display help message - \fBcontrols\fR Print settable control values - \fBmonitors\fR Print output control values - \fBpresets\fR Print available presets - \fBpreset URI\fR Set preset - \fBset INDEX VALUE\fR Set control value by port index - \fBset SYMBOL VALUE\fR Set control value by symbol - \fBSYMBOL = VALUE\fR Set control value by symbol - -.SH "SEE ALSO" -.BR jalv.gtk3(1), -.BR jalv.qt5(2), -.BR lv2ls(1), -.BR jackd(1) - -.SH AUTHOR -jalv was written by David Robillard <d@drobilla.net> -.PP -This manual page was written by Jaromír Mikes <mira.mikes@seznam.cz> -and David Robillard <d@drobilla.net> +.Pp +.Bl -tag -width 16n -compact +.It Ic help +Display help message. +.It Ic controls +Print settable control values. +.It Ic monitors +Print output control values. +.It Ic presets +Print available presets. +.It Ic preset Ar uri +Set preset. +.It Ic set index value +Set control value by port index. +.It Ic set Ar symbol Ar value +Set control value by symbol. +.It Ar symbol Cm = Ar value +Set control value by symbol. +.El +.Sh ENVIRONMENT +.Bl -tag -width LV2_PATH +.It Ev LV2_PATH +Search path for LV2 bundles, in +.Ev PATH +format. +.El +.Sh SEE ALSO +.Xr jalv.gtk3 1 , +.Xr jalv.qt5 1 , +.Xr lv2ls 1 +.Sh AUTHORS +.Nm +was written by +.An David Robillard +.Aq Mt d@drobilla.net , +with contributions by +Robin Gareus, +Hanspeter Portner, +and others. diff --git a/doc/jalv.gtk3.1 b/doc/jalv.gtk3.1 index 1cb8dc1..953c5b0 100644 --- a/doc/jalv.gtk3.1 +++ b/doc/jalv.gtk3.1 @@ -1,57 +1,101 @@ -.TH JALV.GTK3 1 "27 May 2022" - -.SH NAME -.B jalv.gtk \- Run an LV2 plugin as a JACK application (Gtk3 version). - -.SH SYNOPSIS -.B jalv.gtk [OPTION]... PLUGIN_URI - -.SH OPTIONS - -.TP -\fB\-b SIZE\fR +.\" # Copyright 2024 David Robillard <d@drobilla.net> +.\" # SPDX-License-Identifier: ISC +.Dd December 20, 2024 +.Dt JALV.GTK3 1 +.Os +.Sh NAME +.Nm jalv.gtk3 +.Nd run an LV2 plugin with a GTK3 interface +.Sh SYNOPSIS +.Nm jalv.gtk3 +.Op Fl dghmpstx +.Op Fl b , Fl Fl buffer-size Ns = Ns Ar size +.Op Fl c , Fl Fl control Ns = Ns Ar setting +.Op Fl l , Fl Fl load Ns = Ns Ar dir +.Op Fl n , Fl Fl jack-name Ns = Ns Ar name +.Op Fl P , Fl Fl preset Ns = Ns Ar uri +.Op Fl r , Fl Fl update-frequency Ns = Ns Ar hz +.Op Fl S , Fl Fl scale-factor Ns = Ns Ar scale +.Op Fl U , Fl Fl ui-uri Ns = Ns Ar uri +.Ar plugin_uri +.Sh DESCRIPTION +.Nm +is a simple LV2 host that runs a single plugin. +This version has a GTK3 interface that shows either generic controls or a custom plugin GUI. +.Pp +The options are as follows: +.Bl -tag -width 3n +.It Fl b Ar size Buffer size for plugin <=> UI communication. - -.TP -\fB\-c SYM=VAL\fR -Set control value (e.g. "vol=1.4"). - -.TP -\fB\-d\fR, \fB\-\-dump\fR +.It Fl c Ar symbol=value +Set control value, for example, +.Fl c Ar vol=1.4 +where +.Dq vol +is the symbol of some control port on the plugin. +.It Fl d Dump plugin <=> UI communication. - -.TP -\fB\-U URI\fR -Load the UI with the given URI. - -.TP -\fB\-g\fR, \fB\-\-generic\-ui\fR -Use Jalv generic UI and not the plugin UI. - -.TP -\fB\-h\fR, \fB\-\-help\fR +.It Fl g +Show generic UI instead of custom plugin GUI. +.It Fl h Print the command line options. - -.TP -\fB\-l DIR\fR, \fB\-\-load DIR\fR -Load state from state directory. - -.TP -\fB\-p\fR, \fB\-\-print\-controls\fR -Print control output changes to stdout. - -.TP -\fB\-t\fR, \fB\-\-trace\fR +.It Fl l Ar dir +Load state from the given directory before running the plugin. +.It Fl m +Hide application menu, showing only the plugin interface. +.It Fl n Ar name +Use the given JACK client name. +Note that JACK may adjust the name if necessary unless +.Fl x +is also given. +.It Fl P uri +Load the given preset before running plugin. +.It Fl p +Print control output changes to +.Dv stdout . +.It Fl r Ar hz +Set the UI update frequency. +By default the screen refresh rate is used, typically 30 or 60 Hz. +.It Fl S Ar factor +Override the UI scale factor. +.It Fl s +Show controls that are normally hidden. +.It Fl t Print trace messages from plugin. - -.SH "SEE ALSO" -.BR jalv(1), -.BR jalv.qt5(1), -.BR lv2ls(1), -.BR jackd(1) - -.SH AUTHOR -jalv was written by David Robillard <d@drobilla.net> -.PP -This manual page was written by Jaromír Mikes <mira.mikes@seznam.cz> -and David Robillard <d@drobilla.net> +.It Fl U +URI Load the UI with the given URI. +.It Fl x +Use only the exact JACK client name given by +.Fl n +or exit if it's unavailable. +.El +.Sh COMMANDS +The Jalv prompt supports several commands for interactive control: +.Pp +.Bl -tag -width 16n -compact +.It Ic help +Display help message. +.It Ic controls +Print settable control values. +.It Ic monitors +Print output control values. +.It Ic presets +Print available presets. +.It Ic preset Ar uri +Set preset. +.It Ic set index value +Set control value by port index. +.It Ic set Ar symbol Ar value +Set control value by symbol. +.It Ar SYMBOL Cm = Ar VALUE +Set control value by symbol. +.El +.Sh SEE ALSO +.Xr jalv 1 , +.Xr jalv.qt5 1 +.Sh AUTHORS +jalv was written by +.An David Robillard +.Aq Mt d@drobilla.net +with contributions by others. +Most of the GTK interface was added by Alexandros Theodotou and Nick Lanham. diff --git a/doc/jalv.qt5.1 b/doc/jalv.qt5.1 index 14fa57f..578dab4 100644 --- a/doc/jalv.qt5.1 +++ b/doc/jalv.qt5.1 @@ -1,24 +1,29 @@ -.TH JALV.QT5 1 "19 Apr 2012" - -.SH NAME -.B jalv.qt \- Run an LV2 plugin as a JACK application (Qt version). - -.SH SYNOPSIS -.B jalv.qt PLUGIN_URI - -.SH DESCRIPTION - -This is a version of Jalv with a GUI implemented in Qt. It is mainly for -developer testing purposes, for a production ready program use jalv.gtk. - -.SH "SEE ALSO" -.BR jalv(1), -.BR jalv.gtk3(1), -.BR lv2ls(1), -.BR jackd(1) - -.SH AUTHOR -jalv was written by David Robillard <d@drobilla.net> -.PP -This manual page was written by Jaromír Mikes <mira.mikes@seznam.cz> -and David Robillard <d@drobilla.net> +.\" # Copyright 2024 David Robillard <d@drobilla.net> +.\" # SPDX-License-Identifier: ISC +.Dd December 20, 2024 +.Dt JALV.QT5 1 +.Os +.Sh NAME +.Nm jalv.qt5 +.Nd run an LV2 plugin with a Qt5 interface +.Sh SYNOPSIS +.Nm jalv.qt5 +.Ar plugin_uri +.Sh DESCRIPTION +.Nm +is a simple LV2 host that runs a single plugin. +This version has a Qt5 interface that shows either generic controls or a custom plugin GUI. +.Pp +This version has no options and requires the plugin URI to be given on the command line. +For a fully graphical version with a plugin selector, see +.Xr jalv.gtk3 1 . +.Sh SEE ALSO +.Xr jalv 1 , +.Xr jalv.gtk3 1 , +.Xr lv2ls 1 +.Sh AUTHORS +jalv was written by +.An David Robillard +.Aq Mt d@drobilla.net +with contributions by others. +Most of the Qt interface was added by Amadeus Folego and Timo Westkämper. diff --git a/doc/jalv.qt6.1 b/doc/jalv.qt6.1 new file mode 100644 index 0000000..6b80b2c --- /dev/null +++ b/doc/jalv.qt6.1 @@ -0,0 +1,29 @@ +.\" # Copyright 2024 David Robillard <d@drobilla.net> +.\" # SPDX-License-Identifier: ISC +.Dd December 20, 2024 +.Dt JALV.QT6 1 +.Os +.Sh NAME +.Nm jalv.qt6 +.Nd run an LV2 plugin with a Qt6 interface +.Sh SYNOPSIS +.Nm jalv.qt6 +.Ar plugin_uri +.Sh DESCRIPTION +.Nm +is a simple LV2 host that runs a single plugin. +This version has a Qt6 interface that shows either generic controls or a custom plugin GUI. +.Pp +This version has no options and requires the plugin URI to be given on the command line. +For a fully graphical version with a plugin selector, see +.Xr jalv.gtk3 1 . +.Sh SEE ALSO +.Xr jalv 1 , +.Xr jalv.gtk3 1 , +.Xr lv2ls 1 +.Sh AUTHORS +jalv was written by +.An David Robillard +.Aq Mt d@drobilla.net +with contributions by others. +Most of the Qt interface was added by Amadeus Folego and Timo Westkämper. diff --git a/doc/mandoc.css b/doc/mandoc.css new file mode 100644 index 0000000..aee69e5 --- /dev/null +++ b/doc/mandoc.css @@ -0,0 +1,327 @@ +/* + Copyright 2021-2022 David Robillard <d@drobilla.net> + SPDX-License-Identifier: ISC +*/ + +/* Generic page style */ + +/* + Smaller sizes: 0.236em 0.271em 0.382em 0.438em 0.618em 0.708em + Larger sizes: 1.146em 1.618em 1.854em 2.618em 3em 4.236em +*/ + +html { + margin: 0 1.618em; + background: #fff; + color: #000; +} + +body { + font-style: normal; + line-height: 1.618em; + margin: 0 auto auto; + padding: 0; + max-width: 60em; + font-family: "SF Pro Text", Verdana, "DejaVu Sans", sans-serif; + text-rendering: optimizelegibility; +} + +h1 { + font-family: Helvetica, Arial, "DejaVu Sans Condensed", Verdana, sans-serif; + font-size: 1.854em; + font-weight: 600; + line-height: 114.6%; + margin: 1.146em 0; +} + +a { + text-decoration: none; +} + +h1 a, +h2 a, +h3 a, +h4 a, +h5 a, +h6 a { + color: #222; +} + +a:hover { + text-decoration: underline; +} + +h1 a:link, +h2 a:link, +h3 a:link, +h4 a:link, +h5 a:link, +h6 a:link { + color: #222; +} + +h1 a:visited, +h2 a:visited, +h3 a:visited, +h4 a:visited, +h5 a:visited, +h6 a:visited { + color: #222; +} + +pre, +tt, +code { + overflow: auto; + font-family: "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace, fixed; + hyphens: none; + white-space: nowrap; + + /* stylelint-disable property-no-vendor-prefix */ + -epub-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + /* stylelint-enable property-no-vendor-prefix */ +} + +ul, +ol, +dl { + margin: 0; + padding: 0; +} + +ul { + padding: 0; + hyphens: auto; +} + +dt { + font-weight: 600; + padding: 0.618em 0 0; +} + +dd { + margin: 0 0 0 2.618em; + hyphens: auto; +} + +dd > ul:only-child, +dd > ol:only-child { + padding-left: 0; +} + +li { + margin-left: 2.618em; +} + +dt:empty { + margin: 0; + display: none; +} + +dd:empty { + margin: 0; + display: none; +} + +dt:blank { + margin: 0; + display: none; +} + +dd:blank { + margin: 0; + display: none; +} + +/* Media-specific style */ + +/* Color links on screens */ +@media screen { + a { + color: #546e00; + } +} + +@media print { + body { + color: #000; + } + + a, + h1 a, + h2 a, + h3 a, + h4 a, + h5 a, + h6 a { + color: #000; + } + + a:link { + color: #000; + } + + a:visited { + color: #000; + } +} + +/* Mandoc specific style */ + +/* stylelint-disable selector-class-pattern */ + +table.head { + font-size: 0.708em; + margin: 0.438em 0 1.854em; + width: 100%; +} + +table.foot { + font-size: 0.708em; + margin: 2.618em 0 0.438em; + width: 100%; +} + +td.head-rtitle, +td.foot-os { + text-align: right; +} + +td.head-vol { + text-align: center; +} + +div.Pp { + margin: 1ex 0; +} + +a.permalink { + color: #222; +} + +div.Nd, +div.Bf, +div.Op { + display: inline; +} + +span.Pa, +span.Ad { + font-style: italic; +} + +span.Ms { + font-weight: bold; +} + +dl.Bl-diag > dt { + font-weight: bold; +} + +table.Nm tbody tr { + vertical-align: baseline; +} + +code.Nm, +code.Fl, +code.Cm, +code.Ic, +code.In, +code.Fd, +code.Fn, +code.Cd { + font-weight: bold; + color: #444; +} + +code.Ev { + font-weight: bold; + color: #444; +} + +code.Li { + color: #333; +} + +var.Ar { + font-style: italic; +} + +/* stylelint-enable selector-class-pattern */ + +/* Dark mode */ +@media (prefers-color-scheme: dark) { + html { + background: #222; + color: #ddd; + } + + a { + color: #b4c342; + } + + a.permalink { + color: #ddd; + } + + h1 a, + h2 a, + h3 a, + h4 a, + h5 a, + h6 a { + color: #ddd; + } + + h1 a:link, + h2 a:link, + h3 a:link, + h4 a:link, + h5 a:link, + h6 a:link { + color: #ddd; + } + + h1 a:visited, + h2 a:visited, + h3 a:visited, + h4 a:visited, + h5 a:visited, + h6 a:visited { + color: #ddd; + } + + /* stylelint-disable selector-class-pattern */ + + code.Nm, + code.Fl, + code.Cm, + code.Ic, + code.In, + code.Fd, + code.Fn, + code.Cd { + color: #aaa; + } + + code.Ev { + color: #aaa; + } + + code.Li { + color: #ccc; + } + + /* stylelint-enable selector-class-pattern */ +} + +/* Hard black for dark mode on mobile (since it's likely to be an OLED screen) */ +@media only screen and (hover: none) and (pointer: coarse) and (prefers-color-scheme: dark) { + html { + background: #000; + color: #ccc; + } +} diff --git a/doc/meson.build b/doc/meson.build index be25581..9a00094 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -1,12 +1,60 @@ -# Copyright 2022-2023 David Robillard <d@drobilla.net> +# Copyright 2022-2024 David Robillard <d@drobilla.net> # SPDX-License-Identifier: 0BSD OR ISC -install_man(files('jalv.1')) +docdir = get_option('datadir') / 'doc' -if not get_option('gtk3').disabled() - install_man(files('jalv.gtk3.1')) +if not get_option('man').disabled() + install_man(files('jalv.1')) + + if not get_option('gtk3').disabled() + install_man(files('jalv.gtk3.1')) + endif + + if not get_option('qt5').disabled() + install_man(files('jalv.qt5.1')) + endif + + if not get_option('qt6').disabled() + install_man(files('jalv.qt6.1')) + endif endif -if not get_option('qt5').disabled() - install_man(files('jalv.qt5.1')) +# Build/install HTML man pages if mandoc is present +mandoc = find_program('mandoc', required: get_option('man_html')) +if mandoc.found() + configure_file( + copy: true, + input: files('mandoc.css'), + output: 'mandoc.css', + install_dir: docdir / meson.project_name() / 'man', + ) + + mandoc_html_command = [ + mandoc, + '-Kutf-8', + '-Ostyle=mandoc.css,man=%N.html', + '-Thtml', + '-Wwarning,stop', '@INPUT@', + ] + + html_mandir = docdir / meson.project_name() / 'man' + foreach name : ['jalv', 'jalv.gtk3', 'jalv.qt5', 'jalv.qt6'] + custom_target( + name + '.html', + capture: true, + command: mandoc_html_command, + input: files(name + '.1'), + install: true, + install_dir: html_mandir, + output: name + '.html', + ) + endforeach + + if not meson.is_subproject() + summary( + 'HTML man pages', + get_option('prefix') / html_mandir, + section: 'Directories', + ) + endif endif diff --git a/meson.build b/meson.build index b27f31a..8b6cf66 100644 --- a/meson.build +++ b/meson.build @@ -1,24 +1,28 @@ -# Copyright 2020-2022 David Robillard <d@drobilla.net> +# Copyright 2020-2024 David Robillard <d@drobilla.net> # SPDX-License-Identifier: 0BSD OR ISC -project('jalv', ['c', 'cpp'], - version: '1.6.9', - license: 'ISC', - meson_version: '>= 0.56.0', - default_options: [ - 'b_ndebug=if-release', - 'buildtype=release', - 'c_std=c99', - 'cpp_std=c++14', - ]) +project( + 'jalv', + ['c', 'cpp'], + default_options: [ + 'b_ndebug=if-release', + 'buildtype=release', + 'c_std=c99', + 'cpp_std=c++17', + ], + license: 'ISC', + meson_version: '>= 0.56.0', + version: '1.6.9', +) jalv_src_root = meson.current_source_dir() +jalv_build_root = meson.current_build_dir() major_version = meson.project_version().split('.')[0] version_suffix = '@0@-@1@'.format(meson.project_name(), major_version) -####################### -# Compilers and Flags # -####################### +############# +# Compilers # +############# # Required tools cc = meson.get_compiler('c') @@ -28,11 +32,178 @@ if add_languages(['cpp'], native: false, required: get_option('cxx')) cpp = meson.get_compiler('cpp') endif -# Set global warning suppressions -subdir('meson/suppressions') -add_project_arguments(c_suppressions, language: ['c']) +######################## +# Warning Suppressions # +######################## + +warning_level = get_option('warning_level') + +# C +c_suppressions = [] +if cc.get_id() == 'clang' + if warning_level == 'everything' + c_suppressions += [ + '-Wno-bad-function-cast', + '-Wno-cast-align', + '-Wno-cast-function-type-strict', + '-Wno-cast-qual', + '-Wno-declaration-after-statement', + '-Wno-disabled-macro-expansion', + '-Wno-double-promotion', + '-Wno-float-conversion', + '-Wno-float-equal', + '-Wno-implicit-float-conversion', + '-Wno-missing-noreturn', + '-Wno-padded', + '-Wno-shorten-64-to-32', + '-Wno-sign-conversion', + '-Wno-switch-default', + '-Wno-unsafe-buffer-usage', + ] + + if not meson.is_cross_build() + c_suppressions += [ + '-Wno-poison-system-directories', + ] + endif + + if host_machine.system() == 'darwin' + c_suppressions += [ + '-Wno-documentation-unknown-command', + '-Wno-reserved-id-macro', + ] + endif + endif + + if warning_level in ['everything', '3'] + c_suppressions += [ + '-Wno-nullability-extension', + ] + endif + + if host_machine.system() == 'darwin' + c_suppressions += [ + '-Wno-documentation', # JACK + '-Wno-documentation-deprecated-sync', # JACK + ] + elif host_machine.system() == 'freebsd' + c_suppressions += [ + '-Wno-c11-extensions', # isnan and friends + ] + endif + +elif cc.get_id() == 'gcc' + if warning_level == 'everything' + c_suppressions += [ + '-Wno-bad-function-cast', + '-Wno-c++-compat', + '-Wno-cast-align', + '-Wno-cast-qual', + '-Wno-conversion', + '-Wno-double-promotion', + '-Wno-float-equal', + '-Wno-inline', + '-Wno-padded', + '-Wno-strict-overflow', + '-Wno-suggest-attribute=const', + '-Wno-suggest-attribute=pure', + '-Wno-switch-default', + '-Wno-unsuffixed-float-constants', + '-Wno-unused-const-variable', + ] + endif + +elif cc.get_id() == 'msvc' + if warning_level == 'everything' + c_suppressions += [ + '/wd4062', # enumerator in switch is not handled + '/wd4191', # unsafe function conversion + '/wd4200', # zero-sized array in struct/union + '/wd4242', # possible loss of data from float conversion + '/wd4365', # signed/unsigned mismatch + '/wd4514', # unreferenced inline function has been removed + '/wd4668', # not defined as a preprocessor macro + '/wd4706', # assignment within conditional expression + '/wd4710', # function not inlined + '/wd4711', # function selected for automatic inline expansion + '/wd4800', # implicit conversion from int to bool + '/wd4820', # padding added after construct + '/wd5045', # compiler will insert Spectre mitigation + ] + endif + + if warning_level in ['everything', '3', '2'] + c_suppressions += [ + '/wd4267', # possible loss of data from size conversion + '/wd4996', # POSIX name for this item is deprecated + ] + endif + + if warning_level in ['everything', '3', '2', '1'] + c_suppressions += [ + '/wd4244', # conversion from unsigned long to float + ] + endif +endif + +c_suppressions = cc.get_supported_arguments(c_suppressions) + +# C++ if is_variable('cpp') - add_project_arguments(cpp_suppressions, language: ['cpp']) + cpp_suppressions = [] + + if cpp.get_id() == 'clang' + if warning_level == 'everything' + cpp_suppressions += [ + '-Wno-c++98-compat-pedantic', + '-Wno-cast-align', # MacOS + '-Wno-cast-qual', # MacOS + '-Wno-documentation-unknown-command', # MacOS + '-Wno-double-promotion', + '-Wno-float-conversion', + '-Wno-implicit-float-conversion', + '-Wno-old-style-cast', # MacOS + '-Wno-padded', + '-Wno-redundant-parens', + '-Wno-reserved-id-macro', # MacOS + '-Wno-shorten-64-to-32', + '-Wno-sign-conversion', + '-Wno-unsafe-buffer-usage', + '-Wno-weak-vtables', + '-Wno-zero-as-null-pointer-constant', # MacOS + ] + + if not meson.is_cross_build() + cpp_suppressions += [ + '-Wno-poison-system-directories', + ] + endif + endif + + if warning_level in ['everything', '3'] + cpp_suppressions += [ + '-Wno-nullability-extension', + ] + endif + + elif cpp.get_id() == 'gcc' + if warning_level == 'everything' + cpp_suppressions += [ + '-Wno-cast-align', # LV2 + '-Wno-cast-qual', # LV2 + '-Wno-conversion', + '-Wno-effc++', + '-Wno-inline', + '-Wno-padded', + '-Wno-strict-overflow', + '-Wno-suggest-attribute=const', + '-Wno-suggest-attribute=pure', + '-Wno-unused-const-variable', + ] + endif + endif + + cpp_suppressions = cpp.get_supported_arguments(cpp_suppressions) endif ####################### @@ -52,6 +223,7 @@ zix_dep = dependency( 'tests_cpp=disabled', ], fallback: ['zix', 'zix_dep'], + include_type: 'system', version: '>= 0.4.0', ) @@ -63,7 +235,8 @@ serd_dep = dependency( 'tools=disabled', ], fallback: ['serd', 'serd_dep'], - version: '>= 0.30.0', + include_type: 'system', + version: '>= 0.32.2', ) sord_dep = dependency( @@ -74,7 +247,8 @@ sord_dep = dependency( 'tools=disabled', ], fallback: ['sord', 'sord_dep'], - version: '>= 0.14.0', + include_type: 'system', + version: '>= 0.16.16', ) lv2_dep = dependency( @@ -86,6 +260,7 @@ lv2_dep = dependency( 'tests=disabled', ], fallback: ['lv2', 'lv2_dep'], + include_type: 'system', version: '>= 1.18.0', ) @@ -96,6 +271,7 @@ sratom_dep = dependency( 'tests=disabled', ], fallback: ['sratom', 'sratom_dep'], + include_type: 'system', version: '>= 0.6.4', ) @@ -108,7 +284,8 @@ lilv_dep = dependency( 'tools=disabled', ], fallback: ['lilv', 'lilv_dep'], - version: '>= 0.24.0', + include_type: 'system', + version: '>= 0.24.24', ) suil_dep = dependency( @@ -118,6 +295,7 @@ suil_dep = dependency( 'tests=disabled', ], fallback: ['suil', 'suil_dep'], + include_type: 'system', required: get_option('suil'), version: '>= 0.10.0', ) @@ -176,18 +354,9 @@ no_posix = get_option('posix').disabled() or host_machine.system() == 'windows' if no_posix platform_defines += ['-DJALV_NO_POSIX'] elif host_machine.system() == 'darwin' - platform_defines += [ - '-D_DARWIN_C_SOURCE', - '-D_POSIX_C_SOURCE=200809L', - '-D_XOPEN_SOURCE=600', - ] + platform_defines += ['-D_POSIX_C_SOURCE=200809L'] else - platform_defines += [ - '-D_BSD_SOURCE', - '-D_DEFAULT_SOURCE', - '-D_POSIX_C_SOURCE=200809L', - '-D_XOPEN_SOURCE=600', - ] + platform_defines += ['-D_POSIX_C_SOURCE=200809L'] endif # Build platform-specific configuration arguments @@ -201,7 +370,6 @@ elif get_option('checks').enabled() if no_posix platform_defines += ['-DHAVE_FILENO=0'] platform_defines += ['-DHAVE_ISATTY=0'] - platform_defines += ['-DHAVE_MLOCK=0'] platform_defines += ['-DHAVE_POSIX_MEMALIGN=0'] platform_defines += ['-DHAVE_SIGACTION=0'] else @@ -211,9 +379,6 @@ int main(void) { return fileno(stdin); }''' isatty_code = '''#include <unistd.h> int main(void) { return isatty(0); }''' - mlock_code = '''#include <sys/mman.h> -int main(void) { return mlock(0, 0); }''' - posix_memalign_code = '''#include <stdlib.h> int main(void) { void* mem; posix_memalign(&mem, 8, 8); }''' @@ -221,29 +386,24 @@ int main(void) { void* mem; posix_memalign(&mem, 8, 8); }''' int main(void) { return sigaction(SIGINT, 0, 0); }''' platform_defines += '-DHAVE_FILENO=@0@'.format( - cc.compiles(fileno_code, - args: platform_defines, - name: 'fileno').to_int()) + cc.compiles(fileno_code, args: platform_defines, name: 'fileno').to_int(), + ) platform_defines += '-DHAVE_ISATTY=@0@'.format( - cc.compiles(isatty_code, - args: platform_defines, - name: 'isatty').to_int()) - - platform_defines += '-DHAVE_MLOCK=@0@'.format( - cc.compiles(mlock_code, - args: platform_defines, - name: 'mlock').to_int()) + cc.compiles(isatty_code, args: platform_defines, name: 'isatty').to_int(), + ) platform_defines += '-DHAVE_POSIX_MEMALIGN=@0@'.format( - cc.compiles(posix_memalign_code, - args: platform_defines, - name: 'posix_memalign').to_int()) + cc.compiles( + posix_memalign_code, + args: platform_defines, + name: 'posix_memalign', + ).to_int(), + ) platform_defines += '-DHAVE_SIGACTION=@0@'.format( - cc.compiles(sigaction_code, - args: platform_defines, - name: 'sigaction').to_int()) + cc.compiles(sigaction_code, args: platform_defines, name: 'sigaction').to_int(), + ) endif jack_metadata_code = '''#include <jack/metadata.h> @@ -253,30 +413,49 @@ int main(void) { return !!&jack_set_property; }''' int main(void) { return !!&jack_port_type_get_buffer_size; }''' platform_defines += '-DHAVE_JACK_METADATA=@0@'.format( - cc.compiles(jack_metadata_code, - args: platform_defines, - name: 'jack_metadata').to_int()) + cc.compiles( + jack_metadata_code, + args: platform_defines, + name: 'jack_metadata', + ).to_int(), + ) platform_defines += '-DHAVE_JACK_PORT_TYPE_GET_BUFFER_SIZE=@0@'.format( - cc.compiles(jack_port_type_get_buffer_size_code, - args: platform_defines, - name: 'jack_port_type_get_buffer_size').to_int()) + cc.compiles( + jack_port_type_get_buffer_size_code, + args: platform_defines, + name: 'jack_port_type_get_buffer_size', + ).to_int(), + ) endif ############ # Programs # ############ -sources = backend_sources + files( +common_sources = files( + 'src/comm.c', 'src/control.c', + 'src/dumper.c', + 'src/features.c', 'src/jalv.c', 'src/log.c', 'src/lv2_evbuf.c', + 'src/mapper.c', + 'src/nodes.c', + 'src/process.c', + 'src/process_setup.c', + 'src/query.c', 'src/state.c', + 'src/string_utils.c', 'src/symap.c', + 'src/urids.c', 'src/worker.c', ) +sources = backend_sources + common_sources +program_sources = sources + files('src/main.c') + common_dependencies = [ backend_dep, lilv_dep, @@ -288,14 +467,15 @@ common_dependencies = [ zix_dep, ] +common_c_args = c_suppressions + platform_defines + suil_defines + # Internal JACK client library -if jack_dep.found() +if jack_dep.found() and host_machine.system() != 'windows' shared_library( 'jalv', - sources + files('src/jalv_console.c'), + sources + files('src/jack_internal.c', 'src/jalv_console.c'), c_args: c_suppressions + platform_defines + ['-DHAVE_SUIL=0'], dependencies: common_dependencies, - include_directories: include_directories('src'), install: true, install_dir: get_option('prefix') / get_option('libdir') / 'jack', name_prefix: '', @@ -305,10 +485,9 @@ endif # Console version executable( 'jalv', - sources + files('src/jalv_console.c'), - c_args: c_suppressions + platform_defines + suil_defines, + program_sources + files('src/jalv_console.c'), + c_args: common_c_args, dependencies: common_dependencies + [suil_dep], - include_directories: include_directories('src'), install: true, ) @@ -334,62 +513,122 @@ if not get_option('gtk3').disabled() config.set('APP_HUMAN_NAME', 'Jalv') config.set('BINDIR', get_option('prefix') / get_option('bindir')) - configure_file(configuration: config, - input: files('jalv.desktop.in'), - output: 'jalv.desktop', - install: true, - install_dir: get_option('datadir') / 'applications') + configure_file( + configuration: config, + input: files('jalv.desktop.in'), + install: true, + install_dir: get_option('datadir') / 'applications', + output: 'jalv.desktop', + ) executable( 'jalv.gtk3', - sources + files('src/jalv_gtk.c'), - c_args: c_suppressions + platform_defines + suil_defines, + program_sources + files('src/jalv_gtk.c'), + c_args: common_c_args, dependencies: common_dependencies + [gdk3_dep, gtk3_dep, suil_dep], - include_directories: include_directories('src'), install: true, ) endif endif -# Qt 5 GUI version -if not get_option('qt5').disabled() - qt5_dep = dependency( - 'Qt5Widgets', - include_type: 'system', - required: get_option('qt5'), - version: '>= 5.1.0', - ) +if is_variable('cpp') + common_cpp_args = cpp_suppressions + platform_defines + suil_defines - moc = find_program('moc-qt5', required: false) - if not moc.found() - moc = find_program('moc', required: get_option('qt5')) + qt_args = [] + if cpp.get_id() in ['clang', 'gcc'] + qt_args = ['-fPIC'] endif - if moc.found() - qt_args = [] - if cpp.get_id() in ['clang', 'gcc'] - qt_args = ['-fPIC'] - endif + jalv_qt_cpp = files('src' / 'jalv_qt.cpp') + jalv_qt_hpp = files(jalv_src_root / 'src' / 'jalv_qt.hpp') + + # Qt 5 GUI version + qt5_opt = get_option('qt5') + if not qt5_opt.disabled() + qt5_dep = dependency( + 'Qt5Widgets', + include_type: 'system', + required: qt5_opt, + version: '>= 5.1.0', + ) - jalv_qt_hpp = files(jalv_src_root / 'src' / 'jalv_qt.hpp') + if qt5_dep.found() + if get_option('qt5_moc') != '' + moc_qt5 = find_program(get_option('qt5_moc'), required: qt5_opt) + else + moc_qt5 = find_program('moc-qt5', required: false) + if not moc_qt5.found() + moc_qt5 = find_program( + 'moc', + required: qt5_opt, + version: ['>=5.0.0', '<=6.0.0'], + ) + endif + endif + + if moc_qt5.found() + jalv_qt5_meta_cpp = custom_target( + 'jalv_qt5_meta.cpp', + capture: true, + command: [moc_qt5, '@INPUT@'], + input: jalv_qt_hpp, + output: 'jalv_qt5_meta.cpp', + ) + + executable( + 'jalv.qt5', + program_sources + jalv_qt_cpp + [jalv_qt5_meta_cpp], + c_args: common_c_args, + cpp_args: common_cpp_args + qt_args, + dependencies: common_dependencies + [qt5_dep, suil_dep], + install: true, + ) + endif + endif + endif - jalv_qt5_meta_cpp = custom_target( - 'jalv_qt5_meta.cpp', - capture: true, - command: [moc, '@INPUT@'], - input: jalv_qt_hpp, - output: 'jalv_qt5_meta.cpp', + # Qt 6 GUI version + qt6_opt = get_option('qt6') + if not qt6_opt.disabled() + qt6_dep = dependency( + 'Qt6Widgets', + include_type: 'system', + required: qt6_opt, + version: '>= 6.2.0', ) - executable( - 'jalv.qt5', - sources + files('src/jalv_qt.cpp') + [jalv_qt5_meta_cpp], - c_args: c_suppressions + platform_defines + suil_defines, - cpp_args: cpp_suppressions + platform_defines + suil_defines + qt_args, - dependencies: common_dependencies + [qt5_dep, suil_dep], - include_directories: include_directories('src'), - install: true, - ) + if qt6_dep.found() + moc_qt6_name = get_option('qt6_moc') + if moc_qt6_name == '' + qt6_libexecdir = qt6_dep.get_variable( + default_value: get_option('prefix') / get_option('libexecdir'), + internal: 'libexecdir', + pkgconfig: 'libexecdir', + ) + + moc_qt6_name = qt6_libexecdir / 'moc' + endif + + moc_qt6 = find_program(moc_qt6_name, required: qt6_opt) + if moc_qt6.found() + jalv_qt6_meta_cpp = custom_target( + 'jalv_qt6_meta.cpp', + capture: true, + command: [moc_qt6, '@INPUT@'], + input: jalv_qt_hpp, + output: 'jalv_qt6_meta.cpp', + ) + + executable( + 'jalv.qt6', + program_sources + jalv_qt_cpp + [jalv_qt6_meta_cpp], + c_args: common_c_args, + cpp_args: common_cpp_args + qt_args, + dependencies: common_dependencies + [qt6_dep, suil_dep], + install: true, + ) + endif + endif endif endif @@ -397,7 +636,9 @@ endif # Documentation # ################# -subdir('doc') +if not get_option('man').disabled() + subdir('doc') +endif ######### # Tests # diff --git a/meson/suppressions/meson.build b/meson/suppressions/meson.build deleted file mode 100644 index b338d31..0000000 --- a/meson/suppressions/meson.build +++ /dev/null @@ -1,185 +0,0 @@ -# Copyright 2020-2023 David Robillard <d@drobilla.net> -# SPDX-License-Identifier: 0BSD OR ISC - -# Project-specific warning suppressions - -warning_level = get_option('warning_level') - -##### -# C # -##### - -if is_variable('cc') - c_suppressions = [] - - if cc.get_id() == 'clang' - if warning_level == 'everything' - c_suppressions += [ - '-Wno-bad-function-cast', - '-Wno-cast-align', - '-Wno-cast-function-type-strict', - '-Wno-cast-qual', - '-Wno-declaration-after-statement', - '-Wno-disabled-macro-expansion', - '-Wno-documentation-unknown-command', # MacOS - '-Wno-double-promotion', - '-Wno-float-conversion', - '-Wno-float-equal', - '-Wno-format-nonliteral', - '-Wno-implicit-float-conversion', - '-Wno-missing-noreturn', - '-Wno-padded', - '-Wno-reserved-id-macro', # MacOS - '-Wno-shorten-64-to-32', - '-Wno-sign-conversion', - '-Wno-switch-enum', - '-Wno-unsafe-buffer-usage', - '-Wno-unused-macros', - ] - - if not meson.is_cross_build() - c_suppressions += [ - '-Wno-poison-system-directories', - ] - endif - endif - - if warning_level in ['everything', '3'] - c_suppressions += [ - '-Wno-nullability-extension', - ] - endif - - if warning_level in ['everything', '3', '2'] - c_suppressions += [ - '-Wno-unused-parameter', - ] - endif - - if host_machine.system() == 'darwin' - c_suppressions += [ - '-Wno-documentation', # JACK - '-Wno-documentation-deprecated-sync', # JACK - ] - elif host_machine.system() == 'freebsd' - c_suppressions += [ - '-Wno-c11-extensions', # isnan and friends - ] - endif - - elif cc.get_id() == 'gcc' - if warning_level == 'everything' - c_suppressions += [ - '-Wno-bad-function-cast', - '-Wno-c++-compat', - '-Wno-cast-align', - '-Wno-cast-qual', - '-Wno-conversion', - '-Wno-double-promotion', - '-Wno-float-equal', - '-Wno-format-nonliteral', - '-Wno-inline', - '-Wno-padded', - '-Wno-strict-overflow', - '-Wno-suggest-attribute=const', - '-Wno-suggest-attribute=pure', - '-Wno-switch-default', - '-Wno-switch-enum', - '-Wno-unsuffixed-float-constants', - '-Wno-unused-const-variable', - '-Wno-unused-macros', - ] - endif - - if warning_level in ['everything', '3', '2'] - c_suppressions += [ - '-Wno-unused-parameter', - ] - endif - - elif cc.get_id() == 'msvc' - if warning_level == 'everything' - c_suppressions += [ - '/wd4061', # enumerator in switch is not explicitly handled - '/wd4090', # different const qualifiers - '/wd4100', # unreferenced formal parameter - '/wd4191', # unsafe function conversion - '/wd4200', # zero-sized array in struct/union - '/wd4242', # possible loss of data from float conversion - '/wd4244', # possible loss of data from integer conversion - '/wd4267', # possible loss of data from size conversion - '/wd4365', # signed/unsigned mismatch - '/wd4514', # unreferenced inline function has been removed - '/wd4706', # assignment within conditional expression - '/wd4710', # function not inlined - '/wd4711', # function selected for automatic inline expansion - '/wd4800', # implicit conversion from int to bool - '/wd4820', # padding added after construct - '/wd4996', # POSIX name for this item is deprecated - '/wd5045', # compiler will insert Spectre mitigation - ] - endif - endif - - c_suppressions = cc.get_supported_arguments(c_suppressions) -endif - -####### -# C++ # -####### - -if is_variable('cpp') - cpp_suppressions = [] - - if cpp.get_id() == 'clang' - if warning_level == 'everything' - cpp_suppressions += [ - '-Wno-c++98-compat-pedantic', - '-Wno-cast-align', # MacOS - '-Wno-cast-qual', # MacOS - '-Wno-documentation-unknown-command', # MacOS - '-Wno-double-promotion', - '-Wno-float-conversion', - '-Wno-implicit-float-conversion', - '-Wno-old-style-cast', # MacOS - '-Wno-padded', - '-Wno-redundant-parens', - '-Wno-reserved-id-macro', # MacOS - '-Wno-shorten-64-to-32', - '-Wno-sign-conversion', - '-Wno-unsafe-buffer-usage', - '-Wno-weak-vtables', - '-Wno-zero-as-null-pointer-constant', # MacOS - ] - - if not meson.is_cross_build() - cpp_suppressions += [ - '-Wno-poison-system-directories', - ] - endif - endif - - if warning_level in ['everything', '3'] - cpp_suppressions += [ - '-Wno-nullability-extension', - ] - endif - - elif cpp.get_id() == 'gcc' - if warning_level == 'everything' - cpp_suppressions += [ - '-Wno-cast-align', # LV2 - '-Wno-cast-qual', # LV2 - '-Wno-conversion', - '-Wno-effc++', - '-Wno-padded', - '-Wno-strict-overflow', - '-Wno-suggest-attribute=const', - '-Wno-suggest-attribute=pure', - '-Wno-unused-const-variable', - ] - endif - endif - - cpp_suppressions = cpp.get_supported_arguments(cpp_suppressions) -endif diff --git a/meson_options.txt b/meson_options.txt index 77cc20d..f0577ef 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,35 +1,50 @@ -# Copyright 2020-2023 David Robillard <d@drobilla.net> +# Copyright 2020-2024 David Robillard <d@drobilla.net> # SPDX-License-Identifier: 0BSD OR ISC option('checks', type: 'feature', value: 'enabled', yield: true, description: 'Check for platform-specific features') -option('cxx', type: 'feature', value: 'auto', yield: true, +option('cxx', type: 'feature', yield: true, description: 'Build C++ programs') -option('gtk3', type: 'feature', value: 'auto', yield: true, +option('gtk3', type: 'feature', yield: true, description: 'Build Gtk3 GUI') -option('portaudio', type: 'feature', value: 'auto', yield: true, +option('portaudio', type: 'feature', yield: true, description: 'Build PortAudio driver') -option('jack', type: 'feature', value: 'auto', yield: true, +option('jack', type: 'feature', yield: true, description: 'Build JACK driver') option('lint', type: 'boolean', value: false, yield: true, description: 'Run code quality checks') -option('posix', type: 'feature', value: 'auto', yield: true, +option('man', type: 'feature', value: 'enabled', yield: true, + description: 'Install man pages') + +option('man_html', type: 'feature', yield: true, + description: 'Build HTML man pages') + +option('posix', type: 'feature', yield: true, description: 'Use POSIX system facilities') -option('qt5', type: 'feature', value: 'auto', yield: true, +option('qt5', type: 'feature', yield: true, description: 'Build Qt5 GUI') -option('suil', type: 'feature', value: 'auto', yield: true, +option('qt5_moc', type: 'string', yield: true, + description: 'Path to Qt5 moc executable') + +option('qt6', type: 'feature', yield: true, + description: 'Build Qt6 GUI') + +option('qt6_moc', type: 'string', yield: true, + description: 'Path to Qt6 moc executable') + +option('suil', type: 'feature', yield: true, description: 'Use suil to load plugin UIs') option('title', type: 'string', value: 'Jalv', description: 'Project title') -option('tests', type: 'feature', value: 'auto', yield: true, +option('tests', type: 'feature', yield: true, description: 'Build tests') diff --git a/src/backend.h b/src/backend.h index 2965378..2f34c58 100644 --- a/src/backend.h +++ b/src/backend.h @@ -1,37 +1,63 @@ -// Copyright 2007-2022 David Robillard <d@drobilla.net> +// Copyright 2007-2024 David Robillard <d@drobilla.net> // SPDX-License-Identifier: ISC #ifndef JALV_BACKEND_H #define JALV_BACKEND_H #include "attributes.h" +#include "process.h" +#include "settings.h" #include "types.h" +#include "urids.h" +#include <zix/attributes.h> +#include <zix/sem.h> + +#include <stdbool.h> #include <stdint.h> +// Interface that must be implemented by audio/MIDI backends JALV_BEGIN_DECLS -// Interface that must be implemented by audio/MIDI backends +/// Allocate a new uninitialized backend +ZIX_MALLOC_FUNC JalvBackend* +jalv_backend_allocate(void); + +/// Free a backend allocated with jalv_backend_allocate() +void +jalv_backend_free(JalvBackend* backend); -/// Initialize the audio and MIDI systems -JalvBackend* -jalv_backend_init(Jalv* jalv); +/// Open the audio/MIDI system +int +jalv_backend_open(JalvBackend* backend, + const JalvURIDs* urids, + JalvSettings* settings, + JalvProcess* process, + ZixSem* done, + const char* name, + bool exact_name); + +/// Close the audio/MIDI system +void +jalv_backend_close(JalvBackend* backend); /// Activate the backend and start processing audio void -jalv_backend_activate(Jalv* jalv); +jalv_backend_activate(JalvBackend* backend); /// Deactivate the backend and stop processing audio void -jalv_backend_deactivate(Jalv* jalv); +jalv_backend_deactivate(JalvBackend* backend); -/// Close the backend +/// Expose a port to the system (if applicable) and connect it to its buffer void -jalv_backend_close(Jalv* jalv); +jalv_backend_activate_port(JalvBackend* backend, + JalvProcess* process, + uint32_t port_index); -/// Expose a port to the system (if applicable) and connect it to its buffer +/// Recompute latencies based on plugin port latencies if necessary void -jalv_backend_activate_port(Jalv* jalv, uint32_t port_index); +jalv_backend_recompute_latencies(JalvBackend* backend); JALV_END_DECLS diff --git a/src/comm.c b/src/comm.c new file mode 100644 index 0000000..b3bdfd1 --- /dev/null +++ b/src/comm.c @@ -0,0 +1,64 @@ +// Copyright 2007-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include "comm.h" + +#include <lv2/urid/urid.h> +#include <zix/ring.h> +#include <zix/status.h> + +ZixStatus +jalv_write_split_message(ZixRing* const target, + const void* const header, + const uint32_t header_size, + const void* const body, + const uint32_t body_size) +{ + ZixRingTransaction tx = zix_ring_begin_write(target); + + ZixStatus st = ZIX_STATUS_SUCCESS; + if (!(st = zix_ring_amend_write(target, &tx, header, header_size)) && + !(st = zix_ring_amend_write(target, &tx, body, body_size))) { + st = zix_ring_commit_write(target, &tx); + } + + return st; +} + +ZixStatus +jalv_write_event(ZixRing* const target, + const uint32_t port_index, + const uint32_t size, + const LV2_URID type, + const void* const body) +{ + // TODO: Be more discriminate about what to send + + typedef struct { + JalvMessageHeader message; + JalvEventTransfer event; + } Header; + + const Header header = {{EVENT_TRANSFER, sizeof(JalvEventTransfer) + size}, + {port_index, {size, type}}}; + + return jalv_write_split_message(target, &header, sizeof(header), body, size); +} + +ZixStatus +jalv_write_control(ZixRing* const target, + const uint32_t port_index, + const float value) +{ + typedef struct { + JalvMessageHeader message; + JalvControlChange control; + } Message; + + const Message msg = {{CONTROL_PORT_CHANGE, sizeof(JalvControlChange)}, + {port_index, value}}; + + return zix_ring_write(target, &msg, sizeof(msg)) == sizeof(msg) + ? ZIX_STATUS_SUCCESS + : ZIX_STATUS_ERROR; +} diff --git a/src/comm.h b/src/comm.h new file mode 100644 index 0000000..5106ba3 --- /dev/null +++ b/src/comm.h @@ -0,0 +1,136 @@ +// Copyright 2007-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#ifndef JALV_COMM_H +#define JALV_COMM_H + +#include "attributes.h" +#include "types.h" + +#include <lv2/atom/atom.h> +#include <lv2/urid/urid.h> +#include <zix/ring.h> +#include <zix/status.h> + +#include <stdint.h> + +// Communication between the audio and main threads via rings +JALV_BEGIN_DECLS + +/// Type of an internal message in a communication ring +typedef enum { + NO_MESSAGE, ///< Sentinel type for uninitialized messages + CONTROL_PORT_CHANGE, ///< Value change for a control port (float) + EVENT_TRANSFER, ///< Event transfer for a sequence port (atom) + LATENCY_CHANGE, ///< Change to plugin latency + STATE_REQUEST, ///< Request for a plugin state update (no payload) + RUN_STATE_CHANGE, ///< Request to pause or resume running +} JalvMessageType; + +/** + Message between the audio thread and the main thread. + + This is the general header for any type of message in a communication ring. + The type determines how the message must be handled by the receiver. This + header is followed immediately by `size` bytes of data in the ring. +*/ +typedef struct { + JalvMessageType type; ///< Type of this message + uint32_t size; ///< Size of payload following this header in bytes +} JalvMessageHeader; + +/** + The payload of a CONTROL_PORT_CHANGE message. + + This message has a fixed size, this struct defines the entire payload. +*/ +typedef struct { + uint32_t port_index; ///< Control port index + float value; ///< Control value +} JalvControlChange; + +/** + The start of the payload of an EVENT_TRANSFER message. + + This message has a variable size, the start, described by this struct, is + followed immediately by `atom.size` bytes of data (the atom body). +*/ +typedef struct { + uint32_t port_index; ///< Sequence port index + LV2_Atom atom; ///< Event payload header +} JalvEventTransfer; + +/** + The payload of a LATENCY_CHANGE message. + + This message has a fixed size, this struct defines the entire payload. +*/ +typedef struct { + float value; ///< Latency in frames at the current sample rate +} JalvLatencyChange; + +/** + The payload of a RUN_STATE_CHANGE message. + + This message has a fixed size, this struct defines the entire payload. +*/ +typedef struct { + JalvRunState state; ///< Run state to change to +} JalvRunStateChange; + +/** + Write a message in two parts to a ring. + + This is used to conveniently write a message with a fixed-size header and + possibly variably-sized body in a single call. + + @param target Communication ring (jalv->plugin_to_ui or jalv->ui_to_plugin). + @param header Pointer to start of header data. + @param header_size Size of header in bytes. + @param body Pointer to start of body data. + @param body_size Size of body in bytes. + @return 0 on success, non-zero on failure (overflow). +*/ +ZixStatus +jalv_write_split_message(ZixRing* target, + const void* header, + uint32_t header_size, + const void* body, + uint32_t body_size); + +/** + Write a port event using the atom:eventTransfer protocol. + + This is used to transfer atoms between the plugin and UI via sequence ports. + + @param target Communication ring (jalv->plugin_to_ui or jalv->ui_to_plugin). + @param port_index Index of the port this change is for. + @param size Size of body in bytes. + @param type Atom type URID. + @param body Atom body. + @return 0 on success, non-zero on failure (overflow). +*/ +ZixStatus +jalv_write_event(ZixRing* target, + uint32_t port_index, + uint32_t size, + LV2_URID type, + const void* body); + +/** + Write a control port change using the default (0) protocol. + + This is used to transfer control port value changes between the plugin and + UI. + + @param target Communication ring (jalv->plugin_to_ui or jalv->ui_to_plugin). + @param port_index Index of the port this change is for. + @param value New control port value. + @return 0 on success, non-zero on failure (overflow). +*/ +ZixStatus +jalv_write_control(ZixRing* target, uint32_t port_index, float value); + +JALV_END_DECLS + +#endif // JALV_COMM_H diff --git a/src/control.c b/src/control.c index 7150032..41a2b70 100644 --- a/src/control.c +++ b/src/control.c @@ -4,18 +4,20 @@ #include "control.h" #include "log.h" +#include "nodes.h" +#include "string_utils.h" -#include "lilv/lilv.h" -#include "lv2/atom/atom.h" -#include "lv2/atom/forge.h" -#include "lv2/urid/urid.h" +#include <lilv/lilv.h> +#include <lv2/atom/atom.h> +#include <lv2/atom/forge.h> +#include <lv2/urid/urid.h> #include <stdbool.h> #include <stdint.h> #include <stdlib.h> -#include <string.h> -int +/// Order scale points by value +static int scale_point_cmp(const ScalePoint* a, const ScalePoint* b) { if (a->value < b->value) { @@ -30,22 +32,21 @@ scale_point_cmp(const ScalePoint* a, const ScalePoint* b) } ControlID* -new_port_control(LilvWorld* const world, - const LilvPlugin* const plugin, - const LilvPort* const port, - uint32_t port_index, - const float sample_rate, - const JalvNodes* const nodes, - LV2_Atom_Forge* const forge) +new_port_control(LilvWorld* const world, + const LilvPlugin* const plugin, + const LilvPort* const port, + uint32_t port_index, + const float sample_rate, + const JalvNodes* const nodes, + const LV2_Atom_Forge* const forge) { ControlID* id = (ControlID*)calloc(1, sizeof(ControlID)); id->type = PORT; + id->id.index = port_index; id->node = lilv_node_duplicate(lilv_port_get_node(plugin, port)); id->symbol = lilv_node_duplicate(lilv_port_get_symbol(plugin, port)); id->label = lilv_port_get_name(plugin, port); - id->forge = forge; - id->index = port_index; id->group = lilv_port_get(plugin, port, nodes->pg_group); id->value_type = forge->Float; id->is_writable = lilv_port_is_a(plugin, port, nodes->lv2_InputPort); @@ -88,7 +89,7 @@ new_port_control(LilvWorld* const world, id->points[np].value = lilv_node_as_float(lilv_scale_point_get_value(p)); id->points[np].label = - strdup(lilv_node_as_string(lilv_scale_point_get_label(p))); + jalv_strdup(lilv_node_as_string(lilv_scale_point_get_label(p))); ++np; } // TODO: Non-float scale points? @@ -120,18 +121,17 @@ has_range(LilvWorld* const world, } ControlID* -new_property_control(LilvWorld* const world, - const LilvNode* property, - const JalvNodes* const nodes, - LV2_URID_Map* const map, - LV2_Atom_Forge* const forge) +new_property_control(LilvWorld* const world, + const LilvNode* property, + const JalvNodes* const nodes, + LV2_URID_Map* const map, + const LV2_Atom_Forge* const forge) { - ControlID* id = (ControlID*)calloc(1, sizeof(ControlID)); - id->type = PROPERTY; - id->node = lilv_node_duplicate(property); - id->symbol = lilv_world_get_symbol(world, property); - id->forge = forge; - id->property = map->map(map->handle, lilv_node_as_uri(property)); + ControlID* id = (ControlID*)calloc(1, sizeof(ControlID)); + id->type = PROPERTY; + id->id.property = map->map(map->handle, lilv_node_as_uri(property)); + id->node = lilv_node_duplicate(property); + id->symbol = lilv_world_get_symbol(world, property); id->label = lilv_world_get(world, property, nodes->rdfs_label, NULL); id->min = lilv_world_get(world, property, nodes->lv2_minimum, NULL); @@ -168,19 +168,36 @@ new_property_control(LilvWorld* const world, } void +free_control(ControlID* const control) +{ + lilv_node_free(control->node); + lilv_node_free(control->symbol); + lilv_node_free(control->label); + lilv_node_free(control->group); + lilv_node_free(control->min); + lilv_node_free(control->max); + lilv_node_free(control->def); + free(control); +} + +void add_control(Controls* controls, ControlID* control) { - controls->controls = (ControlID**)realloc( + ControlID** const new_controls = (ControlID**)realloc( controls->controls, (controls->n_controls + 1) * sizeof(ControlID*)); - controls->controls[controls->n_controls++] = control; + if (new_controls) { + controls->controls = new_controls; + controls->controls[controls->n_controls++] = control; + } } ControlID* get_property_control(const Controls* controls, LV2_URID property) { for (size_t i = 0; i < controls->n_controls; ++i) { - if (controls->controls[i]->property == property) { + if (controls->controls[i]->type == PROPERTY && + controls->controls[i]->id.property == property) { return controls->controls[i]; } } diff --git a/src/control.h b/src/control.h index 46ca360..0109e60 100644 --- a/src/control.h +++ b/src/control.h @@ -7,18 +7,17 @@ #include "attributes.h" #include "nodes.h" -#include "lilv/lilv.h" -#include "lv2/atom/forge.h" -#include "lv2/urid/urid.h" +#include <lilv/lilv.h> +#include <lv2/atom/forge.h> +#include <lv2/urid/urid.h> #include <stdbool.h> #include <stddef.h> #include <stdint.h> +// Support for plugin controls (control port or event-based) JALV_BEGIN_DECLS -// Plugin control utilities - /// Type of plugin control typedef enum { PORT, ///< Control port @@ -33,27 +32,28 @@ typedef struct { /// Plugin control typedef struct { - ControlType type; ///< Type of control - LilvNode* node; ///< Port or property - LilvNode* symbol; ///< Symbol - LilvNode* label; ///< Human readable label - LV2_Atom_Forge* forge; ///< Forge (for URIDs) - LV2_URID property; ///< Iff type == PROPERTY - uint32_t index; ///< Iff type == PORT - LilvNode* group; ///< Port/control group, or NULL - void* widget; ///< Control Widget - size_t n_points; ///< Number of scale points - ScalePoint* points; ///< Scale points - LV2_URID value_type; ///< Type of control value - LilvNode* min; ///< Minimum value - LilvNode* max; ///< Maximum value - LilvNode* def; ///< Default value - bool is_toggle; ///< Boolean (0 and 1 only) - bool is_integer; ///< Integer values only - bool is_enumeration; ///< Point values only - bool is_logarithmic; ///< Logarithmic scale - bool is_writable; ///< Writable (input) - bool is_readable; ///< Readable (output) + ControlType type; ///< Type of control + union { + LV2_URID property; ///< Iff type == PROPERTY + uint32_t index; ///< Iff type == PORT + } id; + LilvNode* node; ///< Port or property + LilvNode* symbol; ///< Symbol + LilvNode* label; ///< Human readable label + LilvNode* group; ///< Port/control group, or NULL + void* widget; ///< Control Widget + size_t n_points; ///< Number of scale points + ScalePoint* points; ///< Scale points + LV2_URID value_type; ///< Type of control value + LilvNode* min; ///< Minimum value + LilvNode* max; ///< Maximum value + LilvNode* def; ///< Default value + bool is_toggle; ///< Boolean (0 and 1 only) + bool is_integer; ///< Integer values only + bool is_enumeration; ///< Point values only + bool is_logarithmic; ///< Logarithmic scale + bool is_writable; ///< Writable (input) + bool is_readable; ///< Readable (output) } ControlID; /// Set of plugin controls @@ -62,39 +62,33 @@ typedef struct { ControlID** controls; } Controls; -/// Control change event, sent through ring buffers for UI updates -typedef struct { - uint32_t index; - uint32_t protocol; - uint32_t size; - // Followed immediately by size bytes of data -} ControlChange; - -/// Order scale points by value -int -scale_point_cmp(const ScalePoint* a, const ScalePoint* b); - /// Create a new ID for a control port ControlID* -new_port_control(LilvWorld* world, - const LilvPlugin* plugin, - const LilvPort* port, - uint32_t port_index, - float sample_rate, - const JalvNodes* nodes, - LV2_Atom_Forge* forge); +new_port_control(LilvWorld* world, + const LilvPlugin* plugin, + const LilvPort* port, + uint32_t port_index, + float sample_rate, + const JalvNodes* nodes, + const LV2_Atom_Forge* forge); /// Create a new ID for a property-based parameter ControlID* -new_property_control(LilvWorld* world, - const LilvNode* property, - const JalvNodes* nodes, - LV2_URID_Map* map, - LV2_Atom_Forge* forge); +new_property_control(LilvWorld* world, + const LilvNode* property, + const JalvNodes* nodes, + LV2_URID_Map* map, + const LV2_Atom_Forge* forge); + +/// Free a control allocated with new_port_control() or new_property_control() +void +free_control(ControlID* control); +/// Add a control to the given controls set, reallocating as necessary void add_control(Controls* controls, ControlID* control); +/// Return a pointer to the control for the given property, or null ControlID* get_property_control(const Controls* controls, LV2_URID property); diff --git a/src/dumper.c b/src/dumper.c new file mode 100644 index 0000000..06722d8 --- /dev/null +++ b/src/dumper.c @@ -0,0 +1,81 @@ +// Copyright 2012-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include "dumper.h" + +#include "log.h" + +#include <lilv/lilv.h> +#include <lv2/atom/atom.h> +#include <lv2/patch/patch.h> +#include <lv2/time/time.h> +#include <lv2/urid/urid.h> +#include <serd/serd.h> +#include <sratom/sratom.h> + +#include <stdint.h> +#include <stdlib.h> + +struct JalvDumperImpl { + LV2_URID_Unmap* unmap; + SerdEnv* env; + Sratom* sratom; +}; + +JalvDumper* +jalv_dumper_new(LV2_URID_Map* const map, LV2_URID_Unmap* const unmap) +{ + JalvDumper* const dumper = (JalvDumper*)calloc(1, sizeof(JalvDumper)); + SerdEnv* const env = serd_env_new(NULL); + Sratom* const sratom = sratom_new(map); + if (!dumper || !env || !sratom) { + jalv_dumper_free(dumper); + return NULL; + } + + serd_env_set_prefix_from_strings( + env, (const uint8_t*)"patch", (const uint8_t*)LV2_PATCH_PREFIX); + serd_env_set_prefix_from_strings( + env, (const uint8_t*)"time", (const uint8_t*)LV2_TIME_PREFIX); + serd_env_set_prefix_from_strings( + env, (const uint8_t*)"xsd", (const uint8_t*)LILV_NS_XSD); + + dumper->env = env; + dumper->sratom = sratom; + dumper->unmap = unmap; + return dumper; +} + +void +jalv_dumper_free(JalvDumper* const dumper) +{ + if (dumper) { + sratom_free(dumper->sratom); + serd_env_free(dumper->env); + free(dumper); + } +} + +void +jalv_dump_atom(JalvDumper* const dumper, + FILE* const stream, + const char* const label, + const LV2_Atom* const atom, + const int color) +{ + if (dumper) { + char* const str = sratom_to_turtle(dumper->sratom, + dumper->unmap, + "jalv:", + NULL, + NULL, + atom->type, + atom->size, + LV2_ATOM_BODY_CONST(atom)); + + jalv_ansi_start(stream, color); + fprintf(stream, "\n# %s (%u bytes):\n%s\n", label, atom->size, str); + jalv_ansi_reset(stream); + free(str); + } +} diff --git a/src/dumper.h b/src/dumper.h new file mode 100644 index 0000000..60dcfe1 --- /dev/null +++ b/src/dumper.h @@ -0,0 +1,38 @@ +// Copyright 2012-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#ifndef JALV_DUMPER_H +#define JALV_DUMPER_H + +#include "attributes.h" + +#include <lv2/atom/atom.h> +#include <lv2/urid/urid.h> + +#include <stdio.h> + +// LV2 atom dumper +JALV_BEGIN_DECLS + +/// Dumper for writing atoms as Turtle for debugging +typedef struct JalvDumperImpl JalvDumper; + +/// Allocate, configure, and return a new atom dumper +JalvDumper* +jalv_dumper_new(LV2_URID_Map* map, LV2_URID_Unmap* unmap); + +/// Free memory allocated by jalv_init_dumper() +void +jalv_dumper_free(JalvDumper* dumper); + +/// Dump an atom to stdout +void +jalv_dump_atom(JalvDumper* dumper, + FILE* stream, + const char* label, + const LV2_Atom* atom, + int color); + +JALV_END_DECLS + +#endif // JALV_DUMPER_H diff --git a/src/features.c b/src/features.c new file mode 100644 index 0000000..82d8c27 --- /dev/null +++ b/src/features.c @@ -0,0 +1,62 @@ +// Copyright 2018-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include "features.h" + +#include "macros.h" +#include "settings.h" +#include "urids.h" + +#include <lv2/options/options.h> + +#include <stdint.h> +#include <string.h> + +void +jalv_init_lv2_options(JalvFeatures* const features, + const JalvURIDs* const urids, + const JalvSettings* const settings) +{ + const LV2_Options_Option options[ARRAY_SIZE(features->options)] = { + {LV2_OPTIONS_INSTANCE, + 0, + urids->param_sampleRate, + sizeof(float), + urids->atom_Float, + &settings->sample_rate}, + {LV2_OPTIONS_INSTANCE, + 0, + urids->bufsz_minBlockLength, + sizeof(int32_t), + urids->atom_Int, + &settings->block_length}, + {LV2_OPTIONS_INSTANCE, + 0, + urids->bufsz_maxBlockLength, + sizeof(int32_t), + urids->atom_Int, + &settings->block_length}, + {LV2_OPTIONS_INSTANCE, + 0, + urids->bufsz_sequenceSize, + sizeof(int32_t), + urids->atom_Int, + &settings->midi_buf_size}, + {LV2_OPTIONS_INSTANCE, + 0, + urids->ui_updateRate, + sizeof(float), + urids->atom_Float, + &settings->ui_update_hz}, + {LV2_OPTIONS_INSTANCE, + 0, + urids->ui_scaleFactor, + sizeof(float), + urids->atom_Float, + &settings->ui_scale_factor}, + {LV2_OPTIONS_INSTANCE, 0, 0, 0, 0, NULL}}; + + memcpy(features->options, options, sizeof(features->options)); + features->options_feature.URI = LV2_OPTIONS__options; + features->options_feature.data = (void*)features->options; +} diff --git a/src/features.h b/src/features.h new file mode 100644 index 0000000..54742d7 --- /dev/null +++ b/src/features.h @@ -0,0 +1,50 @@ +// Copyright 2018-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#ifndef JALV_FEATURES_H +#define JALV_FEATURES_H + +#include "attributes.h" +#include "settings.h" +#include "urids.h" + +#include <lv2/core/lv2.h> +#include <lv2/data-access/data-access.h> +#include <lv2/log/log.h> +#include <lv2/options/options.h> +#include <lv2/state/state.h> +#include <lv2/ui/ui.h> +#include <lv2/worker/worker.h> + +// LV2 feature support +JALV_BEGIN_DECLS + +/// LV2 features and associated data to be passed to plugins +typedef struct { + LV2_Feature map_feature; + LV2_Feature unmap_feature; + LV2_State_Make_Path make_path; + LV2_Feature make_path_feature; + LV2_Worker_Schedule sched; + LV2_Feature sched_feature; + LV2_Worker_Schedule ssched; + LV2_Feature state_sched_feature; + LV2_Log_Log llog; + LV2_Feature log_feature; + LV2_Options_Option options[7]; + LV2_Feature options_feature; + LV2_Feature safe_restore_feature; + LV2UI_Request_Value request_value; + LV2_Feature request_value_feature; + LV2_Extension_Data_Feature ext_data; +} JalvFeatures; + +/// Set LV2 options feature for passing to plugin after settings are determined +void +jalv_init_lv2_options(JalvFeatures* features, + const JalvURIDs* urids, + const JalvSettings* settings); + +JALV_END_DECLS + +#endif // JALV_FEATURES_H diff --git a/src/frontend.h b/src/frontend.h index 774796d..e95fa6d 100644 --- a/src/frontend.h +++ b/src/frontend.h @@ -8,17 +8,26 @@ #include "options.h" #include "types.h" -#include "lilv/lilv.h" +#include <lilv/lilv.h> #include <stdbool.h> +#include <stdint.h> +// Interface that must be implemented by UIs JALV_BEGIN_DECLS -// Interface that must be implemented by UIs +/// Arbitrary return code for successful early exit (for --help and so on) +#define JALV_EARLY_EXIT_STATUS (-431) -/// Read command-line arguments and set `opts` accordingly +/// Command-line arguments passed to an executable +typedef struct { + int* argc; ///< Pointer to `argc` like in `main` + char*** argv; ///< Pointer to `argv` like in `main` +} JalvFrontendArgs; + +/// Consume command-line arguments and set `opts` accordingly int -jalv_frontend_init(int* argc, char*** argv, JalvOptions* opts); +jalv_frontend_init(JalvFrontendArgs* args, JalvOptions* opts); /// Return the URI of the "native" LV2 UI type const char* @@ -26,15 +35,15 @@ jalv_frontend_ui_type(void); /// Return true if an interactive frontend is available bool -jalv_frontend_discover(Jalv* jalv); +jalv_frontend_discover(const Jalv* jalv); /// Return the ideal refresh rate of the frontend in Hz float -jalv_frontend_refresh_rate(Jalv* jalv); +jalv_frontend_refresh_rate(const Jalv* jalv); /// Return the scale factor of the frontend (for example 2.0 for double sized) float -jalv_frontend_scale_factor(Jalv* jalv); +jalv_frontend_scale_factor(const Jalv* jalv); /// Attempt to get a plugin URI selection from the user LilvNode* @@ -48,6 +57,14 @@ jalv_frontend_open(Jalv* jalv); int jalv_frontend_close(Jalv* jalv); +/// Called when a port event (control change or other message) is sent to the UI +void +jalv_frontend_port_event(Jalv* jalv, + uint32_t port_index, + uint32_t buffer_size, + uint32_t protocol, + const void* buffer); + JALV_END_DECLS #endif // JALV_UI_H @@ -1,21 +1,25 @@ -// Copyright 2007-2022 David Robillard <d@drobilla.net> +// Copyright 2007-2024 David Robillard <d@drobilla.net> // SPDX-License-Identifier: ISC #include "backend.h" -#include "frontend.h" +#include "comm.h" +#include "jack_impl.h" #include "jalv_config.h" -#include "jalv_internal.h" #include "log.h" #include "lv2_evbuf.h" -#include "port.h" +#include "process.h" +#include "process_setup.h" +#include "settings.h" +#include "string_utils.h" #include "types.h" +#include "urids.h" -#include "lilv/lilv.h" -#include "lv2/atom/atom.h" -#include "lv2/atom/forge.h" -#include "lv2/urid/urid.h" -#include "zix/sem.h" +#include <lilv/lilv.h> +#include <lv2/atom/atom.h> +#include <lv2/atom/forge.h> +#include <lv2/urid/urid.h> +#include <zix/sem.h> #include <jack/jack.h> #include <jack/midiport.h> @@ -26,7 +30,6 @@ # include <jack/metadata.h> #endif -#include <ctype.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> @@ -39,147 +42,145 @@ # define REALTIME #endif -struct JalvBackendImpl { - jack_client_t* client; ///< Jack client - bool is_internal_client; ///< Running inside jackd -}; - -/// Internal Jack client initialization entry point -int -jack_initialize(jack_client_t* client, const char* load_init); - -/// Internal Jack client finalization entry point -void -jack_finish(void* arg); +/// Maximum supported latency in frames (at most 2^24 so all integers work) +static const float max_latency = 16777216.0f; /// Jack buffer size callback static int -jack_buffer_size_cb(jack_nframes_t nframes, void* data) +buffer_size_cb(jack_nframes_t nframes, void* data) { - Jalv* const jalv = (Jalv*)data; - jalv->block_length = nframes; - jalv->buf_size_set = true; + JalvBackend* const backend = (JalvBackend*)data; + JalvSettings* const settings = backend->settings; + JalvProcess* const proc = backend->process; + + settings->block_length = nframes; #if USE_JACK_PORT_TYPE_GET_BUFFER_SIZE - jalv->midi_buf_size = jack_port_type_get_buffer_size(jalv->backend->client, - JACK_DEFAULT_MIDI_TYPE); + settings->midi_buf_size = + jack_port_type_get_buffer_size(backend->client, JACK_DEFAULT_MIDI_TYPE); #endif - jalv_allocate_port_buffers(jalv); + if (proc->run_state == JALV_RUNNING) { + jalv_process_activate(proc, backend->urids, proc->instance, settings); + } return 0; } /// Jack shutdown callback static void -jack_shutdown_cb(void* data) +shutdown_cb(void* data) { - Jalv* const jalv = (Jalv*)data; - jalv_frontend_close(jalv); - zix_sem_post(&jalv->done); + JalvBackend* const backend = (JalvBackend*)data; + zix_sem_post(backend->done); } -/// Jack process callback -static REALTIME int -jack_process_cb(jack_nframes_t nframes, void* data) +static void +forge_position(LV2_Atom_Forge* const forge, + const JalvURIDs* const urids, + const jack_transport_state_t state, + const jack_position_t pos) { - Jalv* const jalv = (Jalv*)data; - jack_client_t* client = jalv->backend->client; + LV2_Atom_Forge_Frame frame; + lv2_atom_forge_object(forge, &frame, 0, urids->time_Position); + lv2_atom_forge_key(forge, urids->time_frame); + lv2_atom_forge_long(forge, pos.frame); + lv2_atom_forge_key(forge, urids->time_speed); + lv2_atom_forge_float(forge, (state == JackTransportRolling) ? 1.0 : 0.0); + if ((pos.valid & JackPositionBBT)) { + lv2_atom_forge_key(forge, urids->time_barBeat); + lv2_atom_forge_float(forge, pos.beat - 1 + (pos.tick / pos.ticks_per_beat)); + lv2_atom_forge_key(forge, urids->time_bar); + lv2_atom_forge_long(forge, pos.bar - 1); + lv2_atom_forge_key(forge, urids->time_beatUnit); + lv2_atom_forge_int(forge, pos.beat_type); + lv2_atom_forge_key(forge, urids->time_beatsPerBar); + lv2_atom_forge_float(forge, pos.beats_per_bar); + lv2_atom_forge_key(forge, urids->time_beatsPerMinute); + lv2_atom_forge_float(forge, pos.beats_per_minute); + } +} - // Get Jack transport position - jack_position_t pos; - const bool rolling = - (jack_transport_query(client, &pos) == JackTransportRolling); +static int +process_silent(JalvProcess* const proc, const jack_nframes_t nframes) +{ + for (uint32_t p = 0U; p < proc->num_ports; ++p) { + const JalvProcessPort* const port = &proc->ports[p]; + jack_port_t* const jport = (jack_port_t*)proc->ports[p].sys_port; + if (jport && port->flow == FLOW_OUTPUT) { + void* const buf = jack_port_get_buffer(jport, nframes); + if (port->type == TYPE_EVENT) { + jack_midi_clear_buffer(buf); + } else { + memset(buf, '\0', nframes * sizeof(float)); + } + } + } + return jalv_bypass(proc, nframes); +} + +static bool +process_transport(JalvProcess* const proc, + const jack_transport_state_t state, + const jack_position_t pos, + const jack_nframes_t nframes) +{ // If transport state is not as expected, then something has changed + const bool rolling = state == JackTransportRolling; const bool has_bbt = (pos.valid & JackPositionBBT); const bool xport_changed = - (rolling != jalv->rolling || pos.frame != jalv->position || - (has_bbt && pos.beats_per_minute != jalv->bpm)); + (rolling != proc->rolling || pos.frame != proc->position || + (has_bbt && pos.beats_per_minute != proc->bpm)); - uint8_t pos_buf[256]; - LV2_Atom* lv2_pos = (LV2_Atom*)pos_buf; - if (xport_changed) { - // Build an LV2 position object to report change to plugin - lv2_atom_forge_set_buffer(&jalv->forge, pos_buf, sizeof(pos_buf)); - LV2_Atom_Forge* forge = &jalv->forge; - LV2_Atom_Forge_Frame frame; - lv2_atom_forge_object(forge, &frame, 0, jalv->urids.time_Position); - lv2_atom_forge_key(forge, jalv->urids.time_frame); - lv2_atom_forge_long(forge, pos.frame); - lv2_atom_forge_key(forge, jalv->urids.time_speed); - lv2_atom_forge_float(forge, rolling ? 1.0 : 0.0); - if (has_bbt) { - lv2_atom_forge_key(forge, jalv->urids.time_barBeat); - lv2_atom_forge_float(forge, - pos.beat - 1 + (pos.tick / pos.ticks_per_beat)); - lv2_atom_forge_key(forge, jalv->urids.time_bar); - lv2_atom_forge_long(forge, pos.bar - 1); - lv2_atom_forge_key(forge, jalv->urids.time_beatUnit); - lv2_atom_forge_int(forge, pos.beat_type); - lv2_atom_forge_key(forge, jalv->urids.time_beatsPerBar); - lv2_atom_forge_float(forge, pos.beats_per_bar); - lv2_atom_forge_key(forge, jalv->urids.time_beatsPerMinute); - lv2_atom_forge_float(forge, pos.beats_per_minute); - } + // Update transport state to expected values for next cycle + proc->position = rolling ? pos.frame + nframes : pos.frame; + proc->bpm = has_bbt ? pos.beats_per_minute : proc->bpm; + proc->rolling = rolling; + + return xport_changed; +} - jalv_dump_atom(jalv, stdout, "Position", lv2_pos, 32); +/// Jack process callback +static REALTIME int +process_cb(jack_nframes_t nframes, void* data) +{ + JalvBackend* const backend = (JalvBackend*)data; + const JalvURIDs* const urids = backend->urids; + JalvProcess* const proc = backend->process; + jack_client_t* const client = backend->client; + uint64_t pos_buf[64] = {0U}; + LV2_Atom* const lv2_pos = (LV2_Atom*)pos_buf; + + // If execution is paused, emit silence and return + if (proc->run_state == JALV_PAUSED) { + return process_silent(proc, nframes); } - // Update transport state to expected values for next cycle - jalv->position = rolling ? pos.frame + nframes : pos.frame; - jalv->bpm = has_bbt ? pos.beats_per_minute : jalv->bpm; - jalv->rolling = rolling; - - switch (jalv->play_state) { - case JALV_PAUSE_REQUESTED: - jalv->play_state = JALV_PAUSED; - zix_sem_post(&jalv->paused); - break; - case JALV_PAUSED: - for (uint32_t p = 0; p < jalv->num_ports; ++p) { - jack_port_t* jport = jalv->ports[p].sys_port; - if (jport && jalv->ports[p].flow == FLOW_OUTPUT) { - void* buf = jack_port_get_buffer(jport, nframes); - if (jalv->ports[p].type == TYPE_EVENT) { - jack_midi_clear_buffer(buf); - } else { - memset(buf, '\0', nframes * sizeof(float)); - } - } - } - return 0; - default: - break; + // Get transport state and position + jack_position_t pos = {0U}; + const jack_transport_state_t state = jack_transport_query(client, &pos); + + // Check if transport is discontinuous from last time and update state + const bool xport_changed = process_transport(proc, state, pos, nframes); + if (xport_changed) { + // Build an LV2 position object to report change to plugin + lv2_atom_forge_set_buffer(&proc->forge, (uint8_t*)pos_buf, sizeof(pos_buf)); + forge_position(&proc->forge, urids, state, pos); } // Prepare port buffers - for (uint32_t p = 0; p < jalv->num_ports; ++p) { - struct Port* port = &jalv->ports[p]; - if (port->type == TYPE_AUDIO && port->sys_port) { - // Connect plugin port directly to Jack port buffer - lilv_instance_connect_port( - jalv->instance, p, jack_port_get_buffer(port->sys_port, nframes)); -#if USE_JACK_METADATA - } else if (port->type == TYPE_CV && port->sys_port) { + for (uint32_t p = 0; p < proc->num_ports; ++p) { + JalvProcessPort* const port = &proc->ports[p]; + if (port->sys_port && (port->type == TYPE_AUDIO || port->type == TYPE_CV)) { // Connect plugin port directly to Jack port buffer lilv_instance_connect_port( - jalv->instance, p, jack_port_get_buffer(port->sys_port, nframes)); -#endif + proc->instance, p, jack_port_get_buffer(port->sys_port, nframes)); } else if (port->type == TYPE_EVENT && port->flow == FLOW_INPUT) { lv2_evbuf_reset(port->evbuf, true); - - // Write transport change event if applicable LV2_Evbuf_Iterator iter = lv2_evbuf_begin(port->evbuf); - if (xport_changed) { - lv2_evbuf_write( - &iter, 0, 0, lv2_pos->type, lv2_pos->size, LV2_ATOM_BODY(lv2_pos)); - } - if (jalv->request_update) { - // Plugin state has changed, request an update - const LV2_Atom_Object get = { - {sizeof(LV2_Atom_Object_Body), jalv->urids.atom_Object}, - {0, jalv->urids.patch_Get}}; + if (port->is_primary && xport_changed) { + // Write new transport position lv2_evbuf_write( - &iter, 0, 0, get.atom.type, get.atom.size, LV2_ATOM_BODY_CONST(&get)); + &iter, 0, 0, lv2_pos->type, lv2_pos->size, LV2_ATOM_BODY(lv2_pos)); } if (port->sys_port) { @@ -189,7 +190,7 @@ jack_process_cb(jack_nframes_t nframes, void* data) jack_midi_event_t ev; jack_midi_event_get(&ev, buf, i); lv2_evbuf_write( - &iter, ev.time, 0, jalv->urids.midi_MidiEvent, ev.size, ev.buffer); + &iter, ev.time, 0, urids->midi_MidiEvent, ev.size, ev.buffer); } } } else if (port->type == TYPE_EVENT) { @@ -197,20 +198,28 @@ jack_process_cb(jack_nframes_t nframes, void* data) lv2_evbuf_reset(port->evbuf, false); } } - jalv->request_update = false; // Run plugin for this cycle - const bool send_ui_updates = jalv_run(jalv, nframes); + const bool send_ui_updates = jalv_run(proc, nframes); // Deliver MIDI output and UI events - for (uint32_t p = 0; p < jalv->num_ports; ++p) { - struct Port* const port = &jalv->ports[p]; + for (uint32_t p = 0; p < proc->num_ports; ++p) { + JalvProcessPort* const port = &proc->ports[p]; if (port->flow == FLOW_OUTPUT && port->type == TYPE_CONTROL && - lilv_port_has_property( - jalv->plugin, port->lilv_port, jalv->nodes.lv2_reportsLatency)) { - if (jalv->plugin_latency != port->control) { - jalv->plugin_latency = port->control; - jack_recompute_total_latencies(client); + port->reports_latency) { + // Get the latency in frames from the control output truncated to integer + const float value = proc->controls_buf[p]; + const uint32_t frames = + (value >= 0.0f && value <= max_latency) ? (uint32_t)value : 0U; + + if (proc->plugin_latency != frames) { + // Update the cached value and notify the UI if the latency changed + proc->plugin_latency = frames; + + const JalvLatencyChange body = {frames}; + const JalvMessageHeader header = {LATENCY_CHANGE, sizeof(body)}; + jalv_write_split_message( + proc->plugin_to_ui, &header, sizeof(header), &body, sizeof(body)); } } else if (port->flow == FLOW_OUTPUT && port->type == TYPE_EVENT) { void* buf = NULL; @@ -230,38 +239,41 @@ jack_process_cb(jack_nframes_t nframes, void* data) void* body = NULL; lv2_evbuf_get(i, &frames, &subframes, &type, &size, &body); - if (buf && type == jalv->urids.midi_MidiEvent) { + if (buf && type == urids->midi_MidiEvent) { // Write MIDI event to Jack output jack_midi_event_write(buf, frames, body, size); } - if (jalv->has_ui) { + if (proc->has_ui) { // Forward event to UI - jalv_write_event(jalv, jalv->plugin_to_ui, p, size, type, body); + jalv_write_event(proc->plugin_to_ui, p, size, type, body); } } } else if (send_ui_updates && port->flow == FLOW_OUTPUT && port->type == TYPE_CONTROL) { - jalv_write_control(jalv, jalv->plugin_to_ui, p, port->control); + jalv_write_control(proc->plugin_to_ui, p, proc->controls_buf[p]); } } return 0; } -/// Calculate latency assuming all ports depend on each other +/// Jack latency callback static void -jack_latency_cb(jack_latency_callback_mode_t mode, void* data) +latency_cb(const jack_latency_callback_mode_t mode, void* const data) { - Jalv* const jalv = (Jalv*)data; - const enum PortFlow flow = + // Calculate latency assuming all ports depend on each other + + const JalvBackend* const backend = (JalvBackend*)data; + const JalvProcess* const proc = backend->process; + const PortFlow flow = ((mode == JackCaptureLatency) ? FLOW_INPUT : FLOW_OUTPUT); // First calculate the min/max latency of all feeding ports uint32_t ports_found = 0; jack_latency_range_t range = {UINT32_MAX, 0}; - for (uint32_t p = 0; p < jalv->num_ports; ++p) { - struct Port* port = &jalv->ports[p]; + for (uint32_t p = 0; p < proc->num_ports; ++p) { + JalvProcessPort* const port = &proc->ports[p]; if (port->sys_port && port->flow == flow) { jack_latency_range_t r; jack_port_get_latency_range(port->sys_port, mode, &r); @@ -280,12 +292,12 @@ jack_latency_cb(jack_latency_callback_mode_t mode, void* data) } // Add the plugin's own latency - range.min += jalv->plugin_latency; - range.max += jalv->plugin_latency; + range.min += proc->plugin_latency; + range.max += proc->plugin_latency; // Tell Jack about it - for (uint32_t p = 0; p < jalv->num_ports; ++p) { - struct Port* port = &jalv->ports[p]; + for (uint32_t p = 0; p < proc->num_ports; ++p) { + const JalvProcessPort* const port = &proc->ports[p]; if (port->sys_port && port->flow == flow) { jack_port_set_latency_range(port->sys_port, mode, &range); } @@ -293,21 +305,9 @@ jack_latency_cb(jack_latency_callback_mode_t mode, void* data) } static jack_client_t* -jack_create_client(Jalv* jalv) +create_client(const char* const name, const bool exact_name) { - jack_client_t* client = NULL; - - // Determine the name of the JACK client - char* jack_name = NULL; - if (jalv->opts.name) { - // Name given on command line - jack_name = jalv_strdup(jalv->opts.name); - } else { - // Use plugin name - LilvNode* name = lilv_plugin_get_name(jalv->plugin); - jack_name = jalv_strdup(lilv_node_as_string(name)); - lilv_node_free(name); - } + char* const jack_name = jalv_strdup(name); // Truncate client name to suit JACK if necessary if (strlen(jack_name) >= (unsigned)jack_client_name_size() - 1) { @@ -315,12 +315,8 @@ jack_create_client(Jalv* jalv) } // Connect to JACK - if (!client) { - client = jack_client_open( - jack_name, - (jalv->opts.name_exact ? JackUseExactName : JackNullOption), - NULL); - } + jack_client_t* const client = jack_client_open( + jack_name, (exact_name ? JackUseExactName : JackNullOption), NULL); free(jack_name); @@ -328,84 +324,93 @@ jack_create_client(Jalv* jalv) } JalvBackend* -jalv_backend_init(Jalv* jalv) +jalv_backend_allocate(void) +{ + return (JalvBackend*)calloc(1, sizeof(JalvBackend)); +} + +void +jalv_backend_free(JalvBackend* const backend) +{ + free(backend); +} + +int +jalv_backend_open(JalvBackend* const backend, + const JalvURIDs* const urids, + JalvSettings* const settings, + JalvProcess* const process, + ZixSem* const done, + const char* const name, + const bool exact_name) { jack_client_t* const client = - jalv->backend ? jalv->backend->client : jack_create_client(jalv); + backend->client ? backend->client : create_client(name, exact_name); if (!client) { - return NULL; + return 1; } - jalv_log(JALV_LOG_INFO, "JACK Name: %s\n", jack_get_client_name(client)); + jalv_log(JALV_LOG_INFO, "JACK name: %s\n", jack_get_client_name(client)); // Set audio engine properties - jalv->sample_rate = (float)jack_get_sample_rate(client); - jalv->block_length = jack_get_buffer_size(client); - jalv->midi_buf_size = 4096; + settings->sample_rate = (float)jack_get_sample_rate(client); + settings->block_length = jack_get_buffer_size(client); + settings->midi_buf_size = 4096; #if USE_JACK_PORT_TYPE_GET_BUFFER_SIZE - jalv->midi_buf_size = + settings->midi_buf_size = jack_port_type_get_buffer_size(client, JACK_DEFAULT_MIDI_TYPE); #endif // Set JACK callbacks - void* const arg = (void*)jalv; - jack_set_process_callback(client, &jack_process_cb, arg); - jack_set_buffer_size_callback(client, &jack_buffer_size_cb, arg); - jack_on_shutdown(client, &jack_shutdown_cb, arg); - jack_set_latency_callback(client, &jack_latency_cb, arg); - - if (jalv->backend) { - /* Internal JACK client, jalv->backend->is_internal_client was already set - in jack_initialize() when allocating the backend. */ - return jalv->backend; - } - - // External JACK client, allocate and return opaque backend - JalvBackend* backend = (JalvBackend*)calloc(1, sizeof(JalvBackend)); + void* const arg = (void*)backend; + jack_set_process_callback(client, &process_cb, arg); + jack_set_buffer_size_callback(client, &buffer_size_cb, arg); + jack_on_shutdown(client, &shutdown_cb, arg); + jack_set_latency_callback(client, &latency_cb, arg); + + backend->urids = urids; + backend->settings = settings; + backend->process = process; + backend->done = done; backend->client = client; backend->is_internal_client = false; - return backend; + return 0; } void -jalv_backend_close(Jalv* jalv) +jalv_backend_close(JalvBackend* const backend) { - if (jalv->backend) { - if (!jalv->backend->is_internal_client) { - jack_client_close(jalv->backend->client); - } - - free(jalv->backend); - jalv->backend = NULL; + if (backend && backend->client && !backend->is_internal_client) { + jack_client_close(backend->client); } } void -jalv_backend_activate(Jalv* jalv) +jalv_backend_activate(JalvBackend* const backend) { - jack_activate(jalv->backend->client); + jack_activate(backend->client); } void -jalv_backend_deactivate(Jalv* jalv) +jalv_backend_deactivate(JalvBackend* const backend) { - if (jalv->backend && !jalv->backend->is_internal_client) { - jack_deactivate(jalv->backend->client); + if (!backend->is_internal_client && backend->client) { + jack_deactivate(backend->client); } } void -jalv_backend_activate_port(Jalv* jalv, uint32_t port_index) +jalv_backend_activate_port(JalvBackend* const backend, + JalvProcess* const proc, + const uint32_t port_index) { - jack_client_t* client = jalv->backend->client; - struct Port* const port = &jalv->ports[port_index]; - - const LilvNode* sym = lilv_port_get_symbol(jalv->plugin, port->lilv_port); + jack_client_t* const client = backend->client; + JalvProcessPort* const port = &proc->ports[port_index]; // Connect unsupported ports to NULL (known to be optional by this point) if (port->flow == FLOW_UNKNOWN || port->type == TYPE_UNKNOWN) { - lilv_instance_connect_port(jalv->instance, port_index, NULL); + lilv_instance_connect_port(proc->instance, port_index, NULL); return; } @@ -415,17 +420,20 @@ jalv_backend_activate_port(Jalv* jalv, uint32_t port_index) // Connect the port based on its type switch (port->type) { + case TYPE_UNKNOWN: + break; case TYPE_CONTROL: - lilv_instance_connect_port(jalv->instance, port_index, &port->control); + lilv_instance_connect_port( + proc->instance, port_index, &proc->controls_buf[port_index]); break; case TYPE_AUDIO: port->sys_port = jack_port_register( - client, lilv_node_as_string(sym), JACK_DEFAULT_AUDIO_TYPE, jack_flags, 0); + client, port->symbol, JACK_DEFAULT_AUDIO_TYPE, jack_flags, 0); break; #if USE_JACK_METADATA case TYPE_CV: port->sys_port = jack_port_register( - client, lilv_node_as_string(sym), JACK_DEFAULT_AUDIO_TYPE, jack_flags, 0); + client, port->symbol, JACK_DEFAULT_AUDIO_TYPE, jack_flags, 0); if (port->sys_port) { jack_set_property(client, jack_port_uuid(port->sys_port), @@ -436,17 +444,11 @@ jalv_backend_activate_port(Jalv* jalv, uint32_t port_index) break; #endif case TYPE_EVENT: - if (lilv_port_supports_event( - jalv->plugin, port->lilv_port, jalv->nodes.midi_MidiEvent)) { - port->sys_port = jack_port_register(client, - lilv_node_as_string(sym), - JACK_DEFAULT_MIDI_TYPE, - jack_flags, - 0); + if (port->supports_midi) { + port->sys_port = jack_port_register( + client, port->symbol, JACK_DEFAULT_MIDI_TYPE, jack_flags, 0); } break; - default: - break; } #if USE_JACK_METADATA @@ -461,78 +463,19 @@ jalv_backend_activate_port(Jalv* jalv, uint32_t port_index) "http://www.w3.org/2001/XMLSchema#integer"); // Set port pretty name to label - LilvNode* name = lilv_port_get_name(jalv->plugin, port->lilv_port); - jack_set_property(client, - jack_port_uuid(port->sys_port), - JACK_METADATA_PRETTY_NAME, - lilv_node_as_string(name), - "text/plain"); - lilv_node_free(name); - } -#endif -} - -int -jack_initialize(jack_client_t* const client, const char* const load_init) -{ - const size_t args_len = strlen(load_init); - if (args_len > JACK_LOAD_INIT_LIMIT) { - jalv_log(JALV_LOG_ERR, "Too many arguments given\n"); - return -1; - } - - Jalv* const jalv = (Jalv*)calloc(1, sizeof(Jalv)); - if (!jalv) { - return -1; - } - - if (!(jalv->backend = (JalvBackend*)calloc(1, sizeof(JalvBackend)))) { - free(jalv); - return -1; - } - - jalv->backend->client = client; - jalv->backend->is_internal_client = true; - - // Build full command line with "program" name for building argv - const size_t cmd_len = strlen("jalv ") + args_len; - char* const cmd = (char*)calloc(cmd_len + 1, 1); - memcpy(cmd, "jalv ", strlen("jalv ") + 1); - memcpy(cmd + 5, load_init, args_len + 1); - - // Build argv - int argc = 0; - char** argv = NULL; - char* tok = cmd; - for (size_t i = 0; i <= cmd_len; ++i) { - if (isspace(cmd[i]) || !cmd[i]) { - argv = (char**)realloc(argv, sizeof(char*) * ++argc); - cmd[i] = '\0'; - argv[argc - 1] = tok; - tok = cmd + i + 1; + if (port->label) { + jack_set_property(client, + jack_port_uuid(port->sys_port), + JACK_METADATA_PRETTY_NAME, + port->label, + "text/plain"); } } - - const int err = jalv_open(jalv, &argc, &argv); - if (err) { - jalv_backend_close(jalv); - free(jalv); - } - - free(argv); - free(cmd); - return err; +#endif } void -jack_finish(void* const arg) +jalv_backend_recompute_latencies(JalvBackend* const backend) { - Jalv* const jalv = (Jalv*)arg; - if (jalv) { - if (jalv_close(jalv)) { - jalv_log(JALV_LOG_ERR, "Failed to close Jalv\n"); - } - - free(jalv); - } + jack_recompute_total_latencies(backend->client); } diff --git a/src/jack_impl.h b/src/jack_impl.h new file mode 100644 index 0000000..241ea85 --- /dev/null +++ b/src/jack_impl.h @@ -0,0 +1,31 @@ +// Copyright 2007-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#ifndef JALV_JACK_IMPL_H +#define JALV_JACK_IMPL_H + +#include "attributes.h" +#include "process.h" +#include "settings.h" +#include "urids.h" + +#include <jack/types.h> +#include <zix/sem.h> + +#include <stdbool.h> + +// Definition of Jack backend structure (private to implementation) +JALV_BEGIN_DECLS + +struct JalvBackendImpl { + const JalvURIDs* urids; ///< Application vocabulary + JalvSettings* settings; ///< Run settings + JalvProcess* process; ///< Process thread state + ZixSem* done; ///< Shutdown semaphore + jack_client_t* client; ///< Jack client + bool is_internal_client; ///< Running inside jackd +}; + +JALV_END_DECLS + +#endif // JALV_JACK_IMPL_H diff --git a/src/jack_internal.c b/src/jack_internal.c new file mode 100644 index 0000000..5157e43 --- /dev/null +++ b/src/jack_internal.c @@ -0,0 +1,109 @@ +// Copyright 2007-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include "backend.h" +#include "jack_impl.h" +#include "jalv.h" +#include "log.h" +#include "types.h" + +#include <jack/types.h> + +#include <ctype.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +/// Internal Jack client initialization entry point +int +jack_initialize(jack_client_t* client, const char* load_init); + +/// Internal Jack client finalization entry point +void +jack_finish(void* arg); + +int +jack_initialize(jack_client_t* const client, const char* const load_init) +{ +#ifndef E2BIG +# define E2BIG 7 +#endif +#ifndef ENOMEM +# define ENOMEM 12 +#endif + + const size_t args_len = strlen(load_init); + if (args_len > JACK_LOAD_INIT_LIMIT) { + jalv_log(JALV_LOG_ERR, "Too many arguments given\n"); + return E2BIG; + } + + Jalv* const jalv = (Jalv*)calloc(1, sizeof(Jalv)); + if (!jalv) { + return ENOMEM; + } + + if (!(jalv->backend = jalv_backend_allocate())) { + free(jalv); + return ENOMEM; + } + + jalv->backend->client = client; + jalv->backend->is_internal_client = true; + + // Build full command line with "program" name for building argv + const size_t cmd_len = strlen("jalv ") + args_len; + char* const cmd = (char*)calloc(cmd_len + 1, 1); + memcpy(cmd, "jalv ", strlen("jalv ") + 1); + memcpy(cmd + 5, load_init, args_len + 1); + + // Build argv + int argc = 0; + char** argv = NULL; + char* tok = cmd; + int err = 0; + for (size_t i = 0; !err && i <= cmd_len; ++i) { + if (isspace(cmd[i]) || !cmd[i]) { + char** const new_argv = (char**)realloc(argv, sizeof(char*) * ++argc); + if (!new_argv) { + err = ENOMEM; + break; + } + + argv = new_argv; + cmd[i] = '\0'; + argv[argc - 1] = tok; + tok = cmd + i + 1; + } + } + + if (err || (err = jalv_open(jalv, &argc, &argv))) { + jalv_close(jalv); + free(jalv); + } else { + jalv_activate(jalv); + } + + free(argv); + free(cmd); + return err; + +#undef ENOMEM +#undef E2BIG +} + +void +jack_finish(void* const arg) +{ + Jalv* const jalv = (Jalv*)arg; + if (jalv) { + jalv_deactivate(jalv); + if (jalv_close(jalv)) { + jalv_log(JALV_LOG_ERR, "Failed to close Jalv\n"); + } + + jalv_backend_free(jalv->backend); + free(jalv); + } +} @@ -1,88 +1,60 @@ -// Copyright 2007-2022 David Robillard <d@drobilla.net> +// Copyright 2007-2024 David Robillard <d@drobilla.net> // SPDX-License-Identifier: ISC +#include "jalv.h" + #include "backend.h" +#include "comm.h" #include "control.h" +#include "dumper.h" +#include "features.h" #include "frontend.h" #include "jalv_config.h" -#include "jalv_internal.h" #include "log.h" -#include "lv2_evbuf.h" +#include "macros.h" +#include "mapper.h" #include "nodes.h" +#include "options.h" #include "port.h" +#include "process.h" +#include "process_setup.h" +#include "settings.h" #include "state.h" +#include "string_utils.h" #include "types.h" #include "urids.h" #include "worker.h" -#include "lilv/lilv.h" -#include "lv2/atom/atom.h" -#include "lv2/atom/forge.h" -#include "lv2/atom/util.h" -#include "lv2/buf-size/buf-size.h" -#include "lv2/core/lv2.h" -#include "lv2/data-access/data-access.h" -#include "lv2/instance-access/instance-access.h" -#include "lv2/log/log.h" -#include "lv2/midi/midi.h" -#include "lv2/options/options.h" -#include "lv2/parameters/parameters.h" -#include "lv2/patch/patch.h" -#include "lv2/port-groups/port-groups.h" -#include "lv2/port-props/port-props.h" -#include "lv2/presets/presets.h" -#include "lv2/resize-port/resize-port.h" -#include "lv2/state/state.h" -#include "lv2/time/time.h" -#include "lv2/ui/ui.h" -#include "lv2/urid/urid.h" -#include "lv2/worker/worker.h" -#include "serd/serd.h" -#include "sratom/sratom.h" -#include "symap.h" -#include "zix/attributes.h" -#include "zix/ring.h" -#include "zix/sem.h" +#include <lilv/lilv.h> +#include <lv2/atom/atom.h> +#include <lv2/atom/forge.h> +#include <lv2/atom/util.h> +#include <lv2/buf-size/buf-size.h> +#include <lv2/core/lv2.h> +#include <lv2/data-access/data-access.h> +#include <lv2/instance-access/instance-access.h> +#include <lv2/log/log.h> +#include <lv2/patch/patch.h> +#include <lv2/state/state.h> +#include <lv2/ui/ui.h> +#include <lv2/urid/urid.h> +#include <lv2/worker/worker.h> +#include <zix/allocator.h> +#include <zix/filesystem.h> +#include <zix/path.h> +#include <zix/ring.h> +#include <zix/sem.h> +#include <zix/status.h> #if USE_SUIL -# include "suil/suil.h" -#endif - -#if defined(_WIN32) -# include <io.h> // for _mktemp -# define snprintf _snprintf -#elif defined(__APPLE__) -# include <unistd.h> // for mkdtemp on Darwin +# include <suil/suil.h> #endif -#include <assert.h> -#include <math.h> -#include <signal.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> -#include <sys/stat.h> - -#define NS_RDF "http://www.w3.org/1999/02/22-rdf-syntax-ns#" -#define NS_XSD "http://www.w3.org/2001/XMLSchema#" - -#ifndef MIN -# define MIN(a, b) (((a) < (b)) ? (a) : (b)) -#endif - -#ifndef MAX -# define MAX(a, b) (((a) > (b)) ? (a) : (b)) -#endif - -#ifndef ARRAY_SIZE -# define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) -#endif - -#ifndef MSG_BUFFER_SIZE -# define MSG_BUFFER_SIZE 1024 -#endif /** Size factor for UI ring buffers. @@ -94,30 +66,6 @@ */ #define N_BUFFER_CYCLES 16 -static ZixSem* exit_sem = NULL; ///< Exit semaphore used by signal handler - -static LV2_URID -map_uri(LV2_URID_Map_Handle handle, const char* uri) -{ - Jalv* jalv = (Jalv*)handle; - zix_sem_wait(&jalv->symap_lock); - const LV2_URID id = symap_map(jalv->symap, uri); - zix_sem_post(&jalv->symap_lock); - return id; -} - -static const char* -unmap_uri(LV2_URID_Unmap_Handle handle, LV2_URID urid) -{ - Jalv* jalv = (Jalv*)handle; - zix_sem_wait(&jalv->symap_lock); - const char* uri = symap_unmap(jalv->symap, urid); - zix_sem_post(&jalv->symap_lock); - return uri; -} - -#define NS_EXT "http://lv2plug.in/ns/ext/" - /// These features have no data static const LV2_Feature static_features[] = { {LV2_STATE__loadDefaultState, NULL}, @@ -127,7 +75,7 @@ static const LV2_Feature static_features[] = { /// Return true iff Jalv supports the given feature static bool -feature_is_supported(Jalv* jalv, const char* uri) +feature_is_supported(const Jalv* jalv, const char* uri) { if (!strcmp(uri, "http://lv2plug.in/ns/lv2core#isLive") || !strcmp(uri, "http://lv2plug.in/ns/lv2core#inPlaceBroken")) { @@ -142,147 +90,91 @@ feature_is_supported(Jalv* jalv, const char* uri) return false; } -/// Abort and exit on error -static void -die(const char* msg) -{ - jalv_log(JALV_LOG_ERR, "%s\n", msg); - exit(EXIT_FAILURE); -} - /** Create a port structure from data description. This is called before plugin and Jack instantiation. The remaining instance-specific setup (e.g. buffers) is done later in activate_port(). */ -static void -create_port(Jalv* jalv, uint32_t port_index, float default_value) +static int +create_port(Jalv* jalv, uint32_t port_index) { - struct Port* const port = &jalv->ports[port_index]; + JalvPort* const port = &jalv->ports[port_index]; port->lilv_port = lilv_plugin_get_port_by_index(jalv->plugin, port_index); - port->sys_port = NULL; - port->evbuf = NULL; - port->buf_size = 0; port->index = port_index; - port->control = 0.0f; port->flow = FLOW_UNKNOWN; - const bool optional = lilv_port_has_property( - jalv->plugin, port->lilv_port, jalv->nodes.lv2_connectionOptional); - - // Set the port flow (input or output) - if (lilv_port_is_a( - jalv->plugin, port->lilv_port, jalv->nodes.lv2_InputPort)) { - port->flow = FLOW_INPUT; - } else if (lilv_port_is_a( - jalv->plugin, port->lilv_port, jalv->nodes.lv2_OutputPort)) { - port->flow = FLOW_OUTPUT; - } else if (!optional) { - die("Mandatory port has unknown type (neither input nor output)"); + JalvProcessPort* const pport = &jalv->process.ports[port_index]; + if (jalv_process_port_init(&jalv->process.ports[port_index], + &jalv->nodes, + jalv->plugin, + port->lilv_port)) { + return 1; } - const bool hidden = !jalv->opts.show_hidden && - lilv_port_has_property(jalv->plugin, - port->lilv_port, - jalv->nodes.pprops_notOnGUI); + port->type = pport->type; + port->flow = pport->flow; - // Set control values if (lilv_port_is_a( jalv->plugin, port->lilv_port, jalv->nodes.lv2_ControlPort)) { - port->type = TYPE_CONTROL; - port->control = isnan(default_value) ? 0.0f : default_value; + const bool hidden = !jalv->opts.show_hidden && + lilv_port_has_property(jalv->plugin, + port->lilv_port, + jalv->nodes.pprops_notOnGUI); + if (!hidden) { add_control(&jalv->controls, new_port_control(jalv->world, jalv->plugin, port->lilv_port, port->index, - jalv->sample_rate, + jalv->settings.sample_rate, &jalv->nodes, &jalv->forge)); } - } else if (lilv_port_is_a( - jalv->plugin, port->lilv_port, jalv->nodes.lv2_AudioPort)) { - port->type = TYPE_AUDIO; -#if USE_JACK_METADATA - } else if (lilv_port_is_a( - jalv->plugin, port->lilv_port, jalv->nodes.lv2_CVPort)) { - port->type = TYPE_CV; -#endif - } else if (lilv_port_is_a( - jalv->plugin, port->lilv_port, jalv->nodes.atom_AtomPort)) { - port->type = TYPE_EVENT; - } else if (!optional) { - die("Mandatory port has unknown data type"); - } - - LilvNode* min_size = - lilv_port_get(jalv->plugin, port->lilv_port, jalv->nodes.rsz_minimumSize); - if (min_size && lilv_node_is_int(min_size)) { - port->buf_size = lilv_node_as_int(min_size); - jalv->opts.buffer_size = - MAX(jalv->opts.buffer_size, port->buf_size * N_BUFFER_CYCLES); } - lilv_node_free(min_size); -} - -/// Create port structures from data (via create_port()) for all ports -void -jalv_create_ports(Jalv* jalv) -{ - jalv->num_ports = lilv_plugin_get_num_ports(jalv->plugin); - jalv->ports = (struct Port*)calloc(jalv->num_ports, sizeof(struct Port)); - float* default_values = - (float*)calloc(lilv_plugin_get_num_ports(jalv->plugin), sizeof(float)); - lilv_plugin_get_port_ranges_float(jalv->plugin, NULL, NULL, default_values); - for (uint32_t i = 0; i < jalv->num_ports; ++i) { - create_port(jalv, i, default_values[i]); + // Store index if this is the designated control input port + if (jalv->process.control_in == UINT32_MAX && pport->is_primary && + port->flow == FLOW_INPUT && port->type == TYPE_EVENT) { + jalv->process.control_in = port_index; } - const LilvPort* control_input = lilv_plugin_get_port_by_designation( - jalv->plugin, jalv->nodes.lv2_InputPort, jalv->nodes.lv2_control); - if (control_input) { - const uint32_t index = lilv_port_get_index(jalv->plugin, control_input); - if (jalv->ports[index].type == TYPE_EVENT) { - jalv->control_in = index; - } else { - jalv_log(JALV_LOG_WARNING, - "Non-event port %u has lv2:control designation, ignored\n", - index); - } + // Update maximum buffer sizes + const uint32_t buf_size = pport->buf_size; + jalv->opts.ring_size = MAX(jalv->opts.ring_size, buf_size * N_BUFFER_CYCLES); + if (port->flow == FLOW_OUTPUT) { + jalv->ui_msg_size = MAX(jalv->ui_msg_size, buf_size); } - free(default_values); + return 0; } -/// Allocate port buffers (only necessary for MIDI) -void -jalv_allocate_port_buffers(Jalv* jalv) +/// Create port structures from data (via create_port()) for all ports +static int +jalv_create_ports(Jalv* jalv) { - const LV2_URID atom_Chunk = jalv->map.map( - jalv->map.handle, lilv_node_as_string(jalv->nodes.atom_Chunk)); - - const LV2_URID atom_Sequence = jalv->map.map( - jalv->map.handle, lilv_node_as_string(jalv->nodes.atom_Sequence)); - - for (uint32_t i = 0; i < jalv->num_ports; ++i) { - struct Port* const port = &jalv->ports[i]; - if (port->type == TYPE_EVENT) { - lv2_evbuf_free(port->evbuf); + const uint32_t n_ports = lilv_plugin_get_num_ports(jalv->plugin); - const size_t size = port->buf_size ? port->buf_size : jalv->midi_buf_size; + jalv->num_ports = n_ports; + jalv->ports = (JalvPort*)calloc(n_ports, sizeof(JalvPort)); + jalv->process.num_ports = n_ports; + jalv->process.ports = + (JalvProcessPort*)calloc(n_ports, sizeof(JalvProcessPort)); - port->evbuf = lv2_evbuf_new(size, atom_Chunk, atom_Sequence); + // Allocate control port buffers array and set to default values + jalv->process.controls_buf = (float*)calloc(n_ports, sizeof(float)); + lilv_plugin_get_port_ranges_float( + jalv->plugin, NULL, NULL, jalv->process.controls_buf); - lilv_instance_connect_port( - jalv->instance, i, lv2_evbuf_get_buffer(port->evbuf)); - - lv2_evbuf_reset(port->evbuf, port->flow == FLOW_INPUT); + for (uint32_t i = 0; i < jalv->num_ports; ++i) { + if (create_port(jalv, i)) { + return 1; } } + + return 0; } /** @@ -291,12 +183,12 @@ jalv_allocate_port_buffers(Jalv* jalv) TODO: Build an index to make this faster, currently O(n) which may be a problem when restoring the state of plugins with many ports. */ -struct Port* +JalvPort* jalv_port_by_symbol(Jalv* jalv, const char* sym) { for (uint32_t i = 0; i < jalv->num_ports; ++i) { - struct Port* const port = &jalv->ports[i]; - const LilvNode* port_sym = + JalvPort* const port = &jalv->ports[i]; + const LilvNode* port_sym = lilv_port_get_symbol(jalv->plugin, port->lilv_port); if (!strcmp(lilv_node_as_string(port_sym), sym)) { @@ -307,7 +199,7 @@ jalv_port_by_symbol(Jalv* jalv, const char* sym) return NULL; } -ControlID* +static ControlID* jalv_control_by_symbol(Jalv* jalv, const char* sym) { for (size_t i = 0; i < jalv->controls.n_controls; ++i) { @@ -318,7 +210,7 @@ jalv_control_by_symbol(Jalv* jalv, const char* sym) return NULL; } -void +static void jalv_create_controls(Jalv* jalv, bool writable) { const LilvPlugin* plugin = jalv->plugin; @@ -352,8 +244,11 @@ jalv_create_controls(Jalv* jalv, bool writable) } } - record = new_property_control( - jalv->world, property, &jalv->nodes, &jalv->map, &jalv->forge); + record = new_property_control(jalv->world, + property, + &jalv->nodes, + jalv_mapper_urid_map(jalv->mapper), + &jalv->forge); if (writable) { record->is_writable = true; @@ -376,6 +271,53 @@ jalv_create_controls(Jalv* jalv, bool writable) lilv_node_free(patch_writable); } +static void +jalv_send_to_plugin(void* const jalv_handle, + const uint32_t port_index, + const uint32_t buffer_size, + const uint32_t protocol, + const void* const buffer) +{ + Jalv* const jalv = (Jalv*)jalv_handle; + JalvProcess* const proc = &jalv->process; + ZixStatus st = ZIX_STATUS_SUCCESS; + + if (port_index >= jalv->num_ports) { + jalv_log(JALV_LOG_ERR, "UI wrote to invalid port index %u\n", port_index); + + } else if (protocol == 0U) { + if (buffer_size != sizeof(float)) { + st = ZIX_STATUS_BAD_ARG; + } else { + const float value = *(const float*)buffer; + st = jalv_write_control(proc->ui_to_plugin, port_index, value); + } + + } else if (protocol == jalv->urids.atom_eventTransfer) { + const LV2_Atom* const atom = (const LV2_Atom*)buffer; + if (buffer_size < sizeof(LV2_Atom) || + (sizeof(LV2_Atom) + atom->size != buffer_size)) { + st = ZIX_STATUS_BAD_ARG; + } else { + jalv_dump_atom(jalv->dumper, stdout, "UI => Plugin", atom, 36); + st = jalv_write_event( + proc->ui_to_plugin, port_index, atom->size, atom->type, atom + 1U); + } + + } else { + jalv_log(JALV_LOG_ERR, + "UI wrote with unsupported protocol %u (%s)\n", + protocol, + jalv_mapper_unmap_uri(jalv->mapper, protocol)); + } + + if (st) { + jalv_log(JALV_LOG_ERR, + "Failed to write to plugin from UI (%s)\n", + zix_strerror(st)); + } +} + void jalv_set_control(Jalv* jalv, const ControlID* control, @@ -384,25 +326,23 @@ jalv_set_control(Jalv* jalv, const void* body) { if (control->type == PORT && type == jalv->forge.Float) { - struct Port* port = &jalv->ports[control->index]; - port->control = *(const float*)body; - } else if (control->type == PROPERTY) { - // Copy forge since it is used by process thread - LV2_Atom_Forge forge = jalv->forge; + const float value = *(const float*)body; + jalv_write_control(jalv->process.ui_to_plugin, control->id.index, value); + } else if (control->type == PROPERTY && + jalv->process.control_in != UINT32_MAX) { LV2_Atom_Forge_Frame frame; - uint8_t buf[MSG_BUFFER_SIZE]; - lv2_atom_forge_set_buffer(&forge, buf, sizeof(buf)); + lv2_atom_forge_set_buffer(&jalv->forge, jalv->ui_msg, jalv->ui_msg_size); - lv2_atom_forge_object(&forge, &frame, 0, jalv->urids.patch_Set); - lv2_atom_forge_key(&forge, jalv->urids.patch_property); - lv2_atom_forge_urid(&forge, control->property); - lv2_atom_forge_key(&forge, jalv->urids.patch_value); - lv2_atom_forge_atom(&forge, size, type); - lv2_atom_forge_write(&forge, body, size); + lv2_atom_forge_object(&jalv->forge, &frame, 0, jalv->urids.patch_Set); + lv2_atom_forge_key(&jalv->forge, jalv->urids.patch_property); + lv2_atom_forge_urid(&jalv->forge, control->id.property); + lv2_atom_forge_key(&jalv->forge, jalv->urids.patch_value); + lv2_atom_forge_atom(&jalv->forge, size, type); + lv2_atom_forge_write(&jalv->forge, body, size); - const LV2_Atom* atom = lv2_atom_forge_deref(&forge, frame.ref); + const LV2_Atom* atom = lv2_atom_forge_deref(&jalv->forge, frame.ref); jalv_send_to_plugin(jalv, - jalv->control_in, + jalv->process.control_in, lv2_atom_total_size(atom), jalv->urids.atom_eventTransfer, atom); @@ -413,8 +353,8 @@ jalv_set_control(Jalv* jalv, static uint32_t jalv_ui_port_index(void* const controller, const char* symbol) { - Jalv* const jalv = (Jalv*)controller; - struct Port* port = jalv_port_by_symbol(jalv, symbol); + Jalv* const jalv = (Jalv*)controller; + JalvPort* const port = jalv_port_by_symbol(jalv, symbol); return port ? port->index : LV2UI_INVALID_PORT_INDEX; } @@ -424,13 +364,15 @@ void jalv_ui_instantiate(Jalv* jalv, const char* native_ui_type, void* parent) { #if USE_SUIL + const LilvInstance* const instance = jalv->process.instance; + jalv->ui_host = suil_host_new(jalv_send_to_plugin, jalv_ui_port_index, NULL, NULL); const LV2_Feature parent_feature = {LV2_UI__parent, parent}; - const LV2_Feature instance_feature = { - LV2_INSTANCE_ACCESS_URI, lilv_instance_get_handle(jalv->instance)}; + const LV2_Feature instance_feature = {LV2_INSTANCE_ACCESS_URI, + lilv_instance_get_handle(instance)}; const LV2_Feature data_feature = {LV2_DATA_ACCESS_URI, &jalv->features.ext_data}; @@ -466,273 +408,46 @@ jalv_ui_instantiate(Jalv* jalv, const char* native_ui_type, void* parent) lilv_free(binary_path); lilv_free(bundle_path); +#else + (void)jalv; + (void)native_ui_type; + (void)parent; #endif } -bool -jalv_ui_is_resizable(Jalv* jalv) -{ - if (!jalv->ui) { - return false; - } - - const LilvNode* s = lilv_ui_get_uri(jalv->ui); - LilvNode* p = lilv_new_uri(jalv->world, LV2_CORE__optionalFeature); - LilvNode* fs = lilv_new_uri(jalv->world, LV2_UI__fixedSize); - LilvNode* nrs = lilv_new_uri(jalv->world, LV2_UI__noUserResize); - - LilvNodes* fs_matches = lilv_world_find_nodes(jalv->world, s, p, fs); - LilvNodes* nrs_matches = lilv_world_find_nodes(jalv->world, s, p, nrs); - - lilv_nodes_free(nrs_matches); - lilv_nodes_free(fs_matches); - lilv_node_free(nrs); - lilv_node_free(fs); - lilv_node_free(p); - - return !fs_matches && !nrs_matches; -} - -static void -jalv_send_control_to_plugin(Jalv* const jalv, - uint32_t port_index, - uint32_t buffer_size, - const void* buffer) -{ - if (buffer_size != sizeof(float)) { - jalv_log(JALV_LOG_ERR, "UI wrote invalid control size %u\n", buffer_size); - - } else { - jalv_write_control( - jalv, jalv->ui_to_plugin, port_index, *(const float*)buffer); - } -} - -static void -jalv_send_event_to_plugin(Jalv* const jalv, - uint32_t port_index, - uint32_t buffer_size, - const void* buffer) -{ - const LV2_Atom* const atom = (const LV2_Atom*)buffer; - - if (buffer_size < sizeof(LV2_Atom)) { - jalv_log(JALV_LOG_ERR, "UI wrote impossible atom size\n"); - - } else if (sizeof(LV2_Atom) + atom->size != buffer_size) { - jalv_log(JALV_LOG_ERR, "UI wrote corrupt atom size\n"); - - } else { - jalv_dump_atom(jalv, stdout, "UI => Plugin", atom, 36); - jalv_write_event( - jalv, jalv->ui_to_plugin, port_index, atom->size, atom->type, atom + 1U); - } -} - -void -jalv_send_to_plugin(void* const jalv_handle, - uint32_t port_index, - uint32_t buffer_size, - uint32_t protocol, - const void* buffer) -{ - Jalv* const jalv = (Jalv*)jalv_handle; - - if (port_index >= jalv->num_ports) { - jalv_log(JALV_LOG_ERR, "UI wrote to invalid port index %u\n", port_index); - - } else if (protocol == 0U) { - jalv_send_control_to_plugin(jalv, port_index, buffer_size, buffer); - - } else if (protocol == jalv->urids.atom_eventTransfer) { - jalv_send_event_to_plugin(jalv, port_index, buffer_size, buffer); - - } else { - jalv_log(JALV_LOG_ERR, - "UI wrote with unsupported protocol %u (%s)\n", - protocol, - unmap_uri(jalv, protocol)); - } -} - -void -jalv_apply_ui_events(Jalv* jalv, uint32_t nframes) -{ - if (!jalv->has_ui) { - return; - } - - ControlChange ev = {0U, 0U, 0U}; - const size_t space = zix_ring_read_space(jalv->ui_to_plugin); - for (size_t i = 0; i < space; i += sizeof(ev) + ev.size) { - if (zix_ring_read(jalv->ui_to_plugin, &ev, sizeof(ev)) != sizeof(ev)) { - jalv_log(JALV_LOG_ERR, "Failed to read header from UI ring buffer\n"); - break; - } - - struct { - union { - LV2_Atom atom; - float control; - } head; - uint8_t body[MSG_BUFFER_SIZE]; - } buffer; - - if (zix_ring_read(jalv->ui_to_plugin, &buffer, ev.size) != ev.size) { - jalv_log(JALV_LOG_ERR, "Failed to read from UI ring buffer\n"); - break; - } - - assert(ev.index < jalv->num_ports); - struct Port* const port = &jalv->ports[ev.index]; - if (ev.protocol == 0) { - assert(ev.size == sizeof(float)); - port->control = buffer.head.control; - } else if (ev.protocol == jalv->urids.atom_eventTransfer) { - LV2_Evbuf_Iterator e = lv2_evbuf_end(port->evbuf); - const LV2_Atom* const atom = &buffer.head.atom; - lv2_evbuf_write( - &e, nframes, 0, atom->type, atom->size, LV2_ATOM_BODY_CONST(atom)); - } else { - jalv_log( - JALV_LOG_ERR, "Unknown control change protocol %u\n", ev.protocol); - } - } -} - void jalv_init_ui(Jalv* jalv) { // Set initial control port values for (uint32_t i = 0; i < jalv->num_ports; ++i) { if (jalv->ports[i].type == TYPE_CONTROL) { - jalv_ui_port_event(jalv, i, sizeof(float), 0, &jalv->ports[i].control); + jalv_frontend_port_event( + jalv, i, sizeof(float), 0, &jalv->process.controls_buf[i]); } } - if (jalv->control_in != (uint32_t)-1) { + if (jalv->process.control_in != UINT32_MAX) { // Send patch:Get message for initial parameters/etc - LV2_Atom_Forge forge = jalv->forge; LV2_Atom_Forge_Frame frame; - uint8_t buf[MSG_BUFFER_SIZE]; - lv2_atom_forge_set_buffer(&forge, buf, sizeof(buf)); - lv2_atom_forge_object(&forge, &frame, 0, jalv->urids.patch_Get); + uint64_t buf[4U] = {0U, 0U, 0U, 0U}; + lv2_atom_forge_set_buffer(&jalv->forge, (uint8_t*)buf, sizeof(buf)); + lv2_atom_forge_object(&jalv->forge, &frame, 0, jalv->urids.patch_Get); - const LV2_Atom* atom = lv2_atom_forge_deref(&forge, frame.ref); + const LV2_Atom* atom = lv2_atom_forge_deref(&jalv->forge, frame.ref); jalv_send_to_plugin(jalv, - jalv->control_in, + jalv->process.control_in, lv2_atom_total_size(atom), jalv->urids.atom_eventTransfer, atom); - lv2_atom_forge_pop(&forge, &frame); + lv2_atom_forge_pop(&jalv->forge, &frame); } } static int -jalv_write_control_change(Jalv* const jalv, - ZixRing* const target, - const void* const header, - const uint32_t header_size, - const void* const body, - const uint32_t body_size) +ring_error(const char* const message) { - ZixRingTransaction tx = zix_ring_begin_write(target); - if (zix_ring_amend_write(target, &tx, header, header_size) || - zix_ring_amend_write(target, &tx, body, body_size)) { - jalv_log(JALV_LOG_ERR, - target == jalv->plugin_to_ui ? "Plugin => UI buffer overflow" - : "UI => Plugin buffer overflow"); - return -1; - } - - zix_ring_commit_write(target, &tx); - return 0; -} - -int -jalv_write_event(Jalv* const jalv, - ZixRing* const target, - const uint32_t port_index, - const uint32_t size, - const LV2_URID type, - const void* const body) -{ - // TODO: Be more discriminate about what to send - - typedef struct { - ControlChange change; - LV2_Atom atom; - } Header; - - const Header header = { - {port_index, jalv->urids.atom_eventTransfer, sizeof(LV2_Atom) + size}, - {size, type}}; - - return jalv_write_control_change( - jalv, target, &header, sizeof(header), body, size); -} - -int -jalv_write_control(Jalv* const jalv, - ZixRing* const target, - const uint32_t port_index, - const float value) -{ - const ControlChange header = {port_index, 0, sizeof(value)}; - - return jalv_write_control_change( - jalv, target, &header, sizeof(header), &value, sizeof(value)); -} - -void -jalv_dump_atom(Jalv* const jalv, - FILE* const stream, - const char* const label, - const LV2_Atom* const atom, - const int color) -{ - if (jalv->opts.dump) { - char* const str = sratom_to_turtle(jalv->sratom, - &jalv->unmap, - "jalv:", - NULL, - NULL, - atom->type, - atom->size, - LV2_ATOM_BODY_CONST(atom)); - - jalv_ansi_start(stream, color); - fprintf(stream, "\n# %s (%u bytes):\n%s\n", label, atom->size, str); - jalv_ansi_reset(stream); - free(str); - } -} - -bool -jalv_run(Jalv* jalv, uint32_t nframes) -{ - // Read and apply control change events from UI - jalv_apply_ui_events(jalv, nframes); - - // Run plugin for this cycle - lilv_instance_run(jalv->instance, nframes); - - // Process any worker replies and end the cycle - LV2_Handle handle = lilv_instance_get_handle(jalv->instance); - jalv_worker_emit_responses(jalv->state_worker, handle); - jalv_worker_emit_responses(jalv->worker, handle); - jalv_worker_end_run(jalv->worker); - - // Check if it's time to send updates to the UI - jalv->event_delta_t += nframes; - bool send_ui_updates = false; - uint32_t update_frames = (uint32_t)(jalv->sample_rate / jalv->ui_update_hz); - if (jalv->has_ui && (jalv->event_delta_t > update_frames)) { - send_ui_updates = true; - jalv->event_delta_t = 0; - } - - return send_ui_updates; + jalv_log(JALV_LOG_ERR, "%s", message); + return 1; } int @@ -745,27 +460,36 @@ jalv_update(Jalv* jalv) } // Emit UI events - ControlChange ev; - const size_t space = zix_ring_read_space(jalv->plugin_to_ui); - for (size_t i = 0; i + sizeof(ev) < space; i += sizeof(ev) + ev.size) { - // Read event header to get the size - zix_ring_read(jalv->plugin_to_ui, &ev, sizeof(ev)); - - // Resize read buffer if necessary - jalv->ui_event_buf = realloc(jalv->ui_event_buf, ev.size); - void* const buf = jalv->ui_event_buf; - - // Read event body - zix_ring_read(jalv->plugin_to_ui, buf, ev.size); - - if (ev.protocol == jalv->urids.atom_eventTransfer) { - jalv_dump_atom(jalv, stdout, "Plugin => UI", (const LV2_Atom*)buf, 35); + ZixRing* const ring = jalv->process.plugin_to_ui; + JalvMessageHeader header = {NO_MESSAGE, 0U}; + const size_t space = zix_ring_read_space(ring); + for (size_t i = 0; i < space; i += sizeof(header) + header.size) { + // Read message header (which includes the body size) + if (zix_ring_read(ring, &header, sizeof(header)) != sizeof(header)) { + return ring_error("Failed to read header from process ring\n"); } - jalv_ui_port_event(jalv, ev.index, ev.size, ev.protocol, buf); + // Read message body + void* const body = jalv->ui_msg; + if (zix_ring_read(ring, body, header.size) != header.size) { + return ring_error("Failed to read message from process ring\n"); + } - if (ev.protocol == 0 && jalv->opts.print_controls) { - jalv_print_control(jalv, &jalv->ports[ev.index], *(float*)buf); + if (header.type == CONTROL_PORT_CHANGE) { + const JalvControlChange* const msg = (const JalvControlChange*)body; + jalv_frontend_port_event(jalv, msg->port_index, sizeof(float), 0, body); + } else if (header.type == EVENT_TRANSFER) { + const JalvEventTransfer* const msg = (const JalvEventTransfer*)body; + jalv_dump_atom(jalv->dumper, stdout, "Plugin => UI", &msg->atom, 35); + jalv_frontend_port_event(jalv, + msg->port_index, + sizeof(LV2_Atom) + msg->atom.size, + jalv->urids.atom_eventTransfer, + &msg->atom); + } else if (header.type == LATENCY_CHANGE) { + jalv_backend_recompute_latencies(jalv->backend); + } else { + return ring_error("Unknown message type received from process ring\n"); } } @@ -775,14 +499,14 @@ jalv_update(Jalv* jalv) static bool jalv_apply_control_arg(Jalv* jalv, const char* s) { - char sym[256]; - float val = 0.0f; - if (sscanf(s, "%[^=]=%f", sym, &val) != 2) { + char sym[256] = {'\0'}; + float val = 0.0f; + if (sscanf(s, "%240[^=]=%f", sym, &val) != 2) { jalv_log(JALV_LOG_WARNING, "Ignoring invalid value `%s'\n", s); return false; } - ControlID* control = jalv_control_by_symbol(jalv, sym); + const ControlID* control = jalv_control_by_symbol(jalv, sym); if (!control) { jalv_log( JALV_LOG_WARNING, "Ignoring value for unknown control `%s'\n", sym); @@ -796,37 +520,12 @@ jalv_apply_control_arg(Jalv* jalv, const char* s) } static void -signal_handler(int ZIX_UNUSED(sig)) -{ - zix_sem_post(exit_sem); -} - -static void init_feature(LV2_Feature* const dest, const char* const URI, void* data) { dest->URI = URI; dest->data = data; } -static void -setup_signals(Jalv* const jalv) -{ - exit_sem = &jalv->done; - -#if !defined(_WIN32) && USE_SIGACTION - struct sigaction action; - sigemptyset(&action.sa_mask); - action.sa_flags = 0; - action.sa_handler = signal_handler; - sigaction(SIGINT, &action, NULL); - sigaction(SIGTERM, &action, NULL); -#else - // May not work in combination with fgets in the console interface - signal(SIGINT, signal_handler); - signal(SIGTERM, signal_handler); -#endif -} - static const LilvUI* jalv_select_custom_ui(const Jalv* const jalv) { @@ -887,114 +586,17 @@ jalv_select_custom_ui(const Jalv* const jalv) } static void -jalv_init_urids(Symap* const symap, JalvURIDs* const urids) -{ -#define MAP_URI(uri) symap_map(symap, (uri)) - - urids->atom_Float = MAP_URI(LV2_ATOM__Float); - urids->atom_Int = MAP_URI(LV2_ATOM__Int); - urids->atom_Object = MAP_URI(LV2_ATOM__Object); - urids->atom_Path = MAP_URI(LV2_ATOM__Path); - urids->atom_String = MAP_URI(LV2_ATOM__String); - urids->atom_eventTransfer = MAP_URI(LV2_ATOM__eventTransfer); - urids->bufsz_maxBlockLength = MAP_URI(LV2_BUF_SIZE__maxBlockLength); - urids->bufsz_minBlockLength = MAP_URI(LV2_BUF_SIZE__minBlockLength); - urids->bufsz_sequenceSize = MAP_URI(LV2_BUF_SIZE__sequenceSize); - urids->log_Error = MAP_URI(LV2_LOG__Error); - urids->log_Trace = MAP_URI(LV2_LOG__Trace); - urids->log_Warning = MAP_URI(LV2_LOG__Warning); - urids->midi_MidiEvent = MAP_URI(LV2_MIDI__MidiEvent); - urids->param_sampleRate = MAP_URI(LV2_PARAMETERS__sampleRate); - urids->patch_Get = MAP_URI(LV2_PATCH__Get); - urids->patch_Put = MAP_URI(LV2_PATCH__Put); - urids->patch_Set = MAP_URI(LV2_PATCH__Set); - urids->patch_body = MAP_URI(LV2_PATCH__body); - urids->patch_property = MAP_URI(LV2_PATCH__property); - urids->patch_value = MAP_URI(LV2_PATCH__value); - urids->time_Position = MAP_URI(LV2_TIME__Position); - urids->time_bar = MAP_URI(LV2_TIME__bar); - urids->time_barBeat = MAP_URI(LV2_TIME__barBeat); - urids->time_beatUnit = MAP_URI(LV2_TIME__beatUnit); - urids->time_beatsPerBar = MAP_URI(LV2_TIME__beatsPerBar); - urids->time_beatsPerMinute = MAP_URI(LV2_TIME__beatsPerMinute); - urids->time_frame = MAP_URI(LV2_TIME__frame); - urids->time_speed = MAP_URI(LV2_TIME__speed); - urids->ui_scaleFactor = MAP_URI(LV2_UI__scaleFactor); - urids->ui_updateRate = MAP_URI(LV2_UI__updateRate); - -#undef MAP_URI -} - -static void -jalv_init_nodes(LilvWorld* const world, JalvNodes* const nodes) -{ -#define MAP_NODE(uri) lilv_new_uri(world, (uri)) - - nodes->atom_AtomPort = MAP_NODE(LV2_ATOM__AtomPort); - nodes->atom_Chunk = MAP_NODE(LV2_ATOM__Chunk); - nodes->atom_Float = MAP_NODE(LV2_ATOM__Float); - nodes->atom_Path = MAP_NODE(LV2_ATOM__Path); - nodes->atom_Sequence = MAP_NODE(LV2_ATOM__Sequence); - nodes->lv2_AudioPort = MAP_NODE(LV2_CORE__AudioPort); - nodes->lv2_CVPort = MAP_NODE(LV2_CORE__CVPort); - nodes->lv2_ControlPort = MAP_NODE(LV2_CORE__ControlPort); - nodes->lv2_InputPort = MAP_NODE(LV2_CORE__InputPort); - nodes->lv2_OutputPort = MAP_NODE(LV2_CORE__OutputPort); - nodes->lv2_connectionOptional = MAP_NODE(LV2_CORE__connectionOptional); - nodes->lv2_control = MAP_NODE(LV2_CORE__control); - nodes->lv2_default = MAP_NODE(LV2_CORE__default); - nodes->lv2_enumeration = MAP_NODE(LV2_CORE__enumeration); - nodes->lv2_extensionData = MAP_NODE(LV2_CORE__extensionData); - nodes->lv2_integer = MAP_NODE(LV2_CORE__integer); - nodes->lv2_maximum = MAP_NODE(LV2_CORE__maximum); - nodes->lv2_minimum = MAP_NODE(LV2_CORE__minimum); - nodes->lv2_name = MAP_NODE(LV2_CORE__name); - nodes->lv2_reportsLatency = MAP_NODE(LV2_CORE__reportsLatency); - nodes->lv2_sampleRate = MAP_NODE(LV2_CORE__sampleRate); - nodes->lv2_symbol = MAP_NODE(LV2_CORE__symbol); - nodes->lv2_toggled = MAP_NODE(LV2_CORE__toggled); - nodes->midi_MidiEvent = MAP_NODE(LV2_MIDI__MidiEvent); - nodes->pg_group = MAP_NODE(LV2_PORT_GROUPS__group); - nodes->pprops_logarithmic = MAP_NODE(LV2_PORT_PROPS__logarithmic); - nodes->pprops_notOnGUI = MAP_NODE(LV2_PORT_PROPS__notOnGUI); - nodes->pprops_rangeSteps = MAP_NODE(LV2_PORT_PROPS__rangeSteps); - nodes->pset_Preset = MAP_NODE(LV2_PRESETS__Preset); - nodes->pset_bank = MAP_NODE(LV2_PRESETS__bank); - nodes->rdfs_comment = MAP_NODE(LILV_NS_RDFS "comment"); - nodes->rdfs_label = MAP_NODE(LILV_NS_RDFS "label"); - nodes->rdfs_range = MAP_NODE(LILV_NS_RDFS "range"); - nodes->rsz_minimumSize = MAP_NODE(LV2_RESIZE_PORT__minimumSize); - nodes->ui_showInterface = MAP_NODE(LV2_UI__showInterface); - nodes->work_interface = MAP_NODE(LV2_WORKER__interface); - nodes->work_schedule = MAP_NODE(LV2_WORKER__schedule); - nodes->end = NULL; - -#undef MAP_NODE -} - -static void -jalv_init_env(SerdEnv* const env) -{ - serd_env_set_prefix_from_strings( - env, (const uint8_t*)"patch", (const uint8_t*)LV2_PATCH_PREFIX); - serd_env_set_prefix_from_strings( - env, (const uint8_t*)"time", (const uint8_t*)LV2_TIME_PREFIX); - serd_env_set_prefix_from_strings( - env, (const uint8_t*)"xsd", (const uint8_t*)NS_XSD); -} - -static void jalv_init_features(Jalv* const jalv) { // urid:map - jalv->map.handle = jalv; - jalv->map.map = map_uri; - init_feature(&jalv->features.map_feature, LV2_URID__map, &jalv->map); + init_feature(&jalv->features.map_feature, + LV2_URID__map, + jalv_mapper_urid_map(jalv->mapper)); // urid:unmap - jalv->unmap.handle = jalv; - jalv->unmap.unmap = unmap_uri; - init_feature(&jalv->features.unmap_feature, LV2_URID__unmap, &jalv->unmap); + init_feature(&jalv->features.unmap_feature, + LV2_URID__unmap, + jalv_mapper_urid_unmap(jalv->mapper)); // state:makePath jalv->features.make_path.handle = jalv; @@ -1034,80 +636,52 @@ jalv_init_features(Jalv* const jalv) } static void -jalv_init_options(Jalv* const jalv) +jalv_init_ui_settings(Jalv* const jalv) { - const LV2_Options_Option options[ARRAY_SIZE(jalv->features.options)] = { - {LV2_OPTIONS_INSTANCE, - 0, - jalv->urids.param_sampleRate, - sizeof(float), - jalv->urids.atom_Float, - &jalv->sample_rate}, - {LV2_OPTIONS_INSTANCE, - 0, - jalv->urids.bufsz_minBlockLength, - sizeof(int32_t), - jalv->urids.atom_Int, - &jalv->block_length}, - {LV2_OPTIONS_INSTANCE, - 0, - jalv->urids.bufsz_maxBlockLength, - sizeof(int32_t), - jalv->urids.atom_Int, - &jalv->block_length}, - {LV2_OPTIONS_INSTANCE, - 0, - jalv->urids.bufsz_sequenceSize, - sizeof(int32_t), - jalv->urids.atom_Int, - &jalv->midi_buf_size}, - {LV2_OPTIONS_INSTANCE, - 0, - jalv->urids.ui_updateRate, - sizeof(float), - jalv->urids.atom_Float, - &jalv->ui_update_hz}, - {LV2_OPTIONS_INSTANCE, - 0, - jalv->urids.ui_scaleFactor, - sizeof(float), - jalv->urids.atom_Float, - &jalv->ui_scale_factor}, - {LV2_OPTIONS_INSTANCE, 0, 0, 0, 0, NULL}}; - - memcpy(jalv->features.options, options, sizeof(jalv->features.options)); - - init_feature(&jalv->features.options_feature, - LV2_OPTIONS__options, - (void*)jalv->features.options); -} + const JalvOptions* const opts = &jalv->opts; + JalvSettings* const settings = &jalv->settings; -static void -jalv_init_display(Jalv* const jalv) -{ - if (!jalv->opts.update_rate) { + if (!settings->ring_size) { + /* The UI ring is fed by plugin output ports (usually one), and the UI + updates roughly once per cycle. The ring size is a few times the size + of the MIDI output to give the UI a chance to keep up. */ + settings->ring_size = settings->midi_buf_size * N_BUFFER_CYCLES; + } + + if (opts->update_rate <= 0.0f) { // Calculate a reasonable UI update frequency - jalv->ui_update_hz = jalv_frontend_refresh_rate(jalv); - } else { - // Use user-specified UI update rate - jalv->ui_update_hz = jalv->opts.update_rate; - jalv->ui_update_hz = MAX(1.0f, jalv->ui_update_hz); + settings->ui_update_hz = jalv_frontend_refresh_rate(jalv); } - if (!jalv->opts.scale_factor) { + if (opts->scale_factor <= 0.0f) { // Calculate the monitor's scale factor - jalv->ui_scale_factor = jalv_frontend_scale_factor(jalv); - } else { - // Use user-specified UI scale factor - jalv->ui_scale_factor = jalv->opts.scale_factor; + settings->ui_scale_factor = jalv_frontend_scale_factor(jalv); } // The UI can only go so fast, clamp to reasonable limits - jalv->ui_update_hz = MIN(60, jalv->ui_update_hz); - jalv->opts.buffer_size = MAX(4096, jalv->opts.buffer_size); - jalv_log(JALV_LOG_INFO, "Comm buffers: %u bytes\n", jalv->opts.buffer_size); - jalv_log(JALV_LOG_INFO, "Update rate: %.01f Hz\n", jalv->ui_update_hz); - jalv_log(JALV_LOG_INFO, "Scale factor: %.01f\n", jalv->ui_scale_factor); + settings->ui_update_hz = MAX(1.0f, MIN(60.0f, settings->ui_update_hz)); + settings->ring_size = MAX(4096, settings->ring_size); + jalv_log(JALV_LOG_INFO, "Comm buffers: %u bytes\n", settings->ring_size); + jalv_log(JALV_LOG_INFO, "Update rate: %.01f Hz\n", settings->ui_update_hz); + jalv_log(JALV_LOG_INFO, "Scale factor: %.01f\n", settings->ui_scale_factor); +} + +static LilvState* +initial_state(LilvWorld* const world, + LV2_URID_Map* const urid_map, + const char* const state_path) +{ + LilvState* state = NULL; + if (state_path) { + if (zix_file_type(state_path) == ZIX_FILE_TYPE_DIRECTORY) { + char* const path = zix_path_join(NULL, state_path, "state.ttl"); + state = lilv_state_new_from_file(world, urid_map, NULL, path); + free(path); + } else { + state = lilv_state_new_from_file(world, urid_map, NULL, state_path); + } + } + return state; } int @@ -1118,86 +692,70 @@ jalv_open(Jalv* const jalv, int* argc, char*** argv) #endif // Parse command-line arguments - int ret = 0; - if ((ret = jalv_frontend_init(argc, argv, &jalv->opts))) { - jalv_close(jalv); + JalvFrontendArgs args = {argc, argv}; + const int ret = jalv_frontend_init(&args, &jalv->opts); + if (ret) { return ret; } + JalvSettings* const settings = &jalv->settings; + + settings->block_length = 4096U; + settings->midi_buf_size = 1024U; + settings->ring_size = jalv->opts.ring_size; + settings->ui_update_hz = jalv->opts.update_rate; + settings->ui_scale_factor = jalv->opts.scale_factor; + // Load the LV2 world LilvWorld* const world = lilv_world_new(); lilv_world_load_all(world); - jalv->world = world; - jalv->env = serd_env_new(NULL); - jalv->symap = symap_new(); - jalv->block_length = 4096U; - jalv->midi_buf_size = 1024U; - jalv->play_state = JALV_PAUSED; - jalv->bpm = 120.0f; - jalv->control_in = (uint32_t)-1; - jalv->log.urids = &jalv->urids; - jalv->log.tracing = jalv->opts.trace; - - zix_sem_init(&jalv->symap_lock, 1); + jalv->world = world; + jalv->mapper = jalv_mapper_new(); + jalv->log.urids = &jalv->urids; + jalv->log.tracing = jalv->opts.trace; + + // Set up atom dumping for debugging if enabled + LV2_URID_Map* const urid_map = jalv_mapper_urid_map(jalv->mapper); + LV2_URID_Unmap* const urid_unmap = jalv_mapper_urid_unmap(jalv->mapper); + if (jalv->opts.dump) { + jalv->dumper = jalv_dumper_new(urid_map, urid_unmap); + } + zix_sem_init(&jalv->work_lock, 1); zix_sem_init(&jalv->done, 0); - zix_sem_init(&jalv->paused, 0); - - jalv_init_env(jalv->env); - jalv_init_urids(jalv->symap, &jalv->urids); + jalv_init_urids(jalv->mapper, &jalv->urids); jalv_init_nodes(world, &jalv->nodes); jalv_init_features(jalv); - lv2_atom_forge_init(&jalv->forge, &jalv->map); - - // Set up atom reading and writing environment - jalv->sratom = sratom_new(&jalv->map); - sratom_set_env(jalv->sratom, jalv->env); - jalv->ui_sratom = sratom_new(&jalv->map); - sratom_set_env(jalv->ui_sratom, jalv->env); + lv2_atom_forge_init(&jalv->forge, urid_map); // Create temporary directory for plugin state -#ifdef _WIN32 - jalv->temp_dir = jalv_strdup("jalvXXXXXX"); - _mktemp(jalv->temp_dir); -#else - char* templ = jalv_strdup("/tmp/jalv-XXXXXX"); - jalv->temp_dir = jalv_strjoin(mkdtemp(templ), "/"); - free(templ); -#endif - - // Get plugin URI from loaded state or command line - LilvState* state = NULL; - LilvNode* plugin_uri = NULL; - if (jalv->opts.load) { - struct stat info; - stat(jalv->opts.load, &info); - if ((info.st_mode & S_IFMT) == S_IFDIR) { - char* path = jalv_strjoin(jalv->opts.load, "/state.ttl"); - state = lilv_state_new_from_file(jalv->world, &jalv->map, NULL, path); - free(path); - } else { - state = lilv_state_new_from_file( - jalv->world, &jalv->map, NULL, jalv->opts.load); - } - if (!state) { - jalv_log(JALV_LOG_ERR, "Failed to load state from %s\n", jalv->opts.load); - jalv_close(jalv); - return -2; - } - plugin_uri = lilv_node_duplicate(lilv_state_get_plugin_uri(state)); - } else if (*argc > 1) { - plugin_uri = lilv_new_uri(world, (*argv)[*argc - 1]); + jalv->temp_dir = zix_create_temporary_directory(NULL, "jalvXXXXXX"); + if (!jalv->temp_dir) { + jalv_log(JALV_LOG_WARNING, "Failed to create temporary state directory\n"); } - if (!plugin_uri) { - plugin_uri = jalv_frontend_select_plugin(jalv); + // Load initial state given in options if any + LilvState* state = initial_state(world, urid_map, jalv->opts.load); + if (jalv->opts.load && !state) { + jalv_log(JALV_LOG_ERR, "Failed to load state from %s\n", jalv->opts.load); + return -2; } - if (!plugin_uri) { - jalv_log(JALV_LOG_ERR, "Missing plugin URI, try lv2ls to list plugins\n"); - jalv_close(jalv); - return -3; + // Get plugin URI from loaded state or command line + LilvNode* plugin_uri = NULL; + if (state) { + plugin_uri = lilv_node_duplicate(lilv_state_get_plugin_uri(state)); + } else if (*args.argc == 0) { + if (!(plugin_uri = jalv_frontend_select_plugin(jalv))) { + jalv_log(JALV_LOG_ERR, "Missing plugin URI, try lv2ls to list plugins\n"); + return -3; + } + } else if (*args.argc == 1) { + plugin_uri = lilv_new_uri(world, (*args.argv)[0]); + } else { + jalv_log(JALV_LOG_ERR, "Unexpected trailing arguments\n"); + return -1; } // Find plugin @@ -1208,18 +766,22 @@ jalv_open(Jalv* const jalv, int* argc, char*** argv) lilv_node_free(plugin_uri); if (!jalv->plugin) { jalv_log(JALV_LOG_ERR, "Failed to find plugin\n"); - jalv_close(jalv); return -4; } + if (!jalv->opts.name) { + jalv->opts.name = + jalv_strdup(lilv_node_as_string(lilv_plugin_get_name(jalv->plugin))); + } + // Create workers if necessary if (lilv_plugin_has_extension_data(jalv->plugin, jalv->nodes.work_interface)) { - jalv->worker = jalv_worker_new(&jalv->work_lock, true); - jalv->features.sched.handle = jalv->worker; + jalv->process.worker = jalv_worker_new(&jalv->work_lock, true); + jalv->features.sched.handle = jalv->process.worker; if (jalv->safe_restore) { - jalv->state_worker = jalv_worker_new(&jalv->work_lock, false); - jalv->features.ssched.handle = jalv->state_worker; + jalv->process.state_worker = jalv_worker_new(&jalv->work_lock, false); + jalv->features.ssched.handle = jalv->process.state_worker; } } @@ -1228,28 +790,23 @@ jalv_open(Jalv* const jalv, int* argc, char*** argv) LilvNode* preset = lilv_new_uri(jalv->world, jalv->opts.preset); jalv_load_presets(jalv, NULL, NULL); - state = lilv_state_new_from_world(jalv->world, &jalv->map, preset); + state = lilv_state_new_from_world(jalv->world, urid_map, preset); jalv->preset = state; lilv_node_free(preset); if (!state) { jalv_log(JALV_LOG_ERR, "Failed to find preset <%s>\n", jalv->opts.preset); - jalv_close(jalv); return -5; } } // Check for thread-safe state restore() method - LilvNode* state_threadSafeRestore = - lilv_new_uri(jalv->world, LV2_STATE__threadSafeRestore); - if (lilv_plugin_has_feature(jalv->plugin, state_threadSafeRestore)) { - jalv->safe_restore = true; - } - lilv_node_free(state_threadSafeRestore); + jalv->safe_restore = + lilv_plugin_has_feature(jalv->plugin, jalv->nodes.state_threadSafeRestore); if (!state) { // Not restoring state, load the plugin as a preset to get default state = lilv_state_new_from_world( - jalv->world, &jalv->map, lilv_plugin_get_uri(jalv->plugin)); + jalv->world, urid_map, lilv_plugin_get_uri(jalv->plugin)); } // Get a plugin UI @@ -1270,46 +827,47 @@ jalv_open(Jalv* const jalv, int* argc, char*** argv) } #endif - if (jalv->ui) { - jalv_log(JALV_LOG_INFO, - "UI: %s\n", - lilv_node_as_uri(lilv_ui_get_uri(jalv->ui))); - } + jalv_log(JALV_LOG_INFO, + "UI: %s\n", + lilv_node_as_uri(lilv_ui_get_uri(jalv->ui))); } } - // Create port and control structures - jalv_create_ports(jalv); + // Initialize process thread + const uint32_t update_frames = + (uint32_t)(settings->sample_rate / settings->ui_update_hz); + jalv_process_init(&jalv->process, &jalv->urids, jalv->mapper, update_frames); + + // Create port structures + if (jalv_create_ports(jalv)) { + return -10; + } + + // Create input and output control structures jalv_create_controls(jalv, true); jalv_create_controls(jalv, false); - if (!(jalv->backend = jalv_backend_init(jalv))) { + if (jalv_backend_open(jalv->backend, + &jalv->urids, + &jalv->settings, + &jalv->process, + &jalv->done, + jalv->opts.name, + jalv->opts.name_exact)) { jalv_log(JALV_LOG_ERR, "Failed to connect to audio system\n"); - jalv_close(jalv); return -6; } - jalv_log(JALV_LOG_INFO, "Sample rate: %u Hz\n", (uint32_t)jalv->sample_rate); - jalv_log(JALV_LOG_INFO, "Block length: %u frames\n", jalv->block_length); - jalv_log(JALV_LOG_INFO, "MIDI buffers: %zu bytes\n", jalv->midi_buf_size); - - if (jalv->opts.buffer_size == 0) { - /* The UI ring is fed by plugin output ports (usually one), and the UI - updates roughly once per cycle. The ring size is a few times the size - of the MIDI output to give the UI a chance to keep up. The UI should be - able to keep up with 4 cycles, and tests show this works for me, but - this value might need increasing to avoid overflows. */ - jalv->opts.buffer_size = jalv->midi_buf_size * N_BUFFER_CYCLES; - } + jalv_log( + JALV_LOG_INFO, "Sample rate: %u Hz\n", (uint32_t)settings->sample_rate); + jalv_log(JALV_LOG_INFO, "Block length: %u frames\n", settings->block_length); + jalv_log(JALV_LOG_INFO, "MIDI buffers: %zu bytes\n", settings->midi_buf_size); - jalv_init_display(jalv); - jalv_init_options(jalv); + jalv_init_ui_settings(jalv); + jalv_init_lv2_options(&jalv->features, &jalv->urids, settings); - // Create Plugin <=> UI communication buffers - jalv->ui_to_plugin = zix_ring_new(NULL, jalv->opts.buffer_size); - jalv->plugin_to_ui = zix_ring_new(NULL, jalv->opts.buffer_size); - zix_ring_mlock(jalv->ui_to_plugin); - zix_ring_mlock(jalv->plugin_to_ui); + // Create Plugin => UI communication buffers + jalv->ui_msg = zix_aligned_alloc(NULL, 8U, jalv->ui_msg_size); // Build feature list for passing to plugins const LV2_Feature* const features[] = {&jalv->features.map_feature, @@ -1326,7 +884,6 @@ jalv_open(Jalv* const jalv, int* argc, char*** argv) jalv->feature_list = (const LV2_Feature**)calloc(1, sizeof(features)); if (!jalv->feature_list) { jalv_log(JALV_LOG_ERR, "Failed to allocate feature list\n"); - jalv_close(jalv); return -7; } memcpy(jalv->feature_list, features, sizeof(features)); @@ -1337,38 +894,37 @@ jalv_open(Jalv* const jalv, int* argc, char*** argv) const char* uri = lilv_node_as_uri(lilv_nodes_get(req_feats, f)); if (!feature_is_supported(jalv, uri)) { jalv_log(JALV_LOG_ERR, "Feature %s is not supported\n", uri); - jalv_close(jalv); return -8; } } lilv_nodes_free(req_feats); // Instantiate the plugin - jalv->instance = lilv_plugin_instantiate( - jalv->plugin, jalv->sample_rate, jalv->feature_list); - if (!jalv->instance) { + LilvInstance* const instance = lilv_plugin_instantiate( + jalv->plugin, settings->sample_rate, jalv->feature_list); + if (!instance) { jalv_log(JALV_LOG_ERR, "Failed to instantiate plugin\n"); - jalv_close(jalv); return -9; } // Point things to the instance that require it + // jalv->process.instance = instance; jalv->features.ext_data.data_access = - lilv_instance_get_descriptor(jalv->instance)->extension_data; + lilv_instance_get_descriptor(instance)->extension_data; const LV2_Worker_Interface* worker_iface = (const LV2_Worker_Interface*)lilv_instance_get_extension_data( - jalv->instance, LV2_WORKER__interface); - - jalv_worker_start(jalv->worker, worker_iface, jalv->instance->lv2_handle); - jalv_worker_start( - jalv->state_worker, worker_iface, jalv->instance->lv2_handle); + instance, LV2_WORKER__interface); + jalv_worker_attach(jalv->process.worker, worker_iface, instance->lv2_handle); + jalv_worker_attach( + jalv->process.state_worker, worker_iface, instance->lv2_handle); jalv_log(JALV_LOG_INFO, "\n"); - if (!jalv->buf_size_set) { - jalv_allocate_port_buffers(jalv); - } + + // Allocate port buffers + jalv_process_activate( + &jalv->process, &jalv->urids, instance, &jalv->settings); // Apply loaded state to plugin instance if necessary if (state) { @@ -1385,100 +941,100 @@ jalv_open(Jalv* const jalv, int* argc, char*** argv) // Create Jack ports and connect plugin ports to buffers for (uint32_t i = 0; i < jalv->num_ports; ++i) { - jalv_backend_activate_port(jalv, i); + jalv_backend_activate_port(jalv->backend, &jalv->process, i); } - // Print initial control values - for (size_t i = 0; i < jalv->controls.n_controls; ++i) { - ControlID* control = jalv->controls.controls[i]; - if (control->type == PORT && control->is_writable) { - struct Port* port = &jalv->ports[control->index]; - jalv_print_control(jalv, port, port->control); + // Discover UI + jalv->process.has_ui = jalv_frontend_discover(jalv); + return 0; +} + +int +jalv_activate(Jalv* const jalv) +{ + jalv->process.run_state = JALV_RUNNING; + + if (jalv->backend) { + if (jalv->process.worker) { + jalv_worker_launch(jalv->process.worker); } + lilv_instance_activate(jalv->process.instance); + jalv_backend_activate(jalv->backend); } - // Activate plugin - lilv_instance_activate(jalv->instance); - - // Discover UI - jalv->has_ui = jalv_frontend_discover(jalv); + return 0; +} - // Activate audio backend - jalv_backend_activate(jalv); - jalv->play_state = JALV_RUNNING; +int +jalv_deactivate(Jalv* const jalv) +{ + if (jalv->backend) { + jalv_backend_deactivate(jalv->backend); + } + if (jalv->process.instance) { + lilv_instance_deactivate(jalv->process.instance); + } + if (jalv->process.worker) { + jalv_worker_exit(jalv->process.worker); + } + jalv->process.run_state = JALV_PAUSED; return 0; } int jalv_close(Jalv* const jalv) { - // Terminate the worker - jalv_worker_exit(jalv->worker); - - // Deactivate audio + // Stop audio processing, free event port buffers, and close backend + jalv_deactivate(jalv); + jalv_process_deactivate(&jalv->process); if (jalv->backend) { - jalv_backend_deactivate(jalv); - jalv_backend_close(jalv); + jalv_backend_close(jalv->backend); } - // Free event port buffers - for (uint32_t i = 0; i < jalv->num_ports; ++i) { - if (jalv->ports[i].evbuf) { - lv2_evbuf_free(jalv->ports[i].evbuf); - } - } - - // Destroy the worker - jalv_worker_free(jalv->worker); - jalv_worker_free(jalv->state_worker); - - // Deactivate plugin + // Free UI and plugin instances #if USE_SUIL suil_instance_free(jalv->ui_instance); #endif - if (jalv->instance) { - lilv_instance_deactivate(jalv->instance); - lilv_instance_free(jalv->instance); + if (jalv->process.instance) { + lilv_instance_free(jalv->process.instance); } // Clean up + lilv_state_free(jalv->preset); free(jalv->ports); - zix_ring_free(jalv->ui_to_plugin); - zix_ring_free(jalv->plugin_to_ui); - for (LilvNode** n = (LilvNode**)&jalv->nodes; *n; ++n) { - lilv_node_free(*n); - } - symap_free(jalv->symap); - zix_sem_destroy(&jalv->symap_lock); + jalv_process_cleanup(&jalv->process); + zix_aligned_free(NULL, jalv->ui_msg); + free(jalv->process.controls_buf); + jalv_free_nodes(&jalv->nodes); #if USE_SUIL suil_host_free(jalv->ui_host); #endif for (unsigned i = 0; i < jalv->controls.n_controls; ++i) { - ControlID* const control = jalv->controls.controls[i]; - lilv_node_free(control->node); - lilv_node_free(control->symbol); - lilv_node_free(control->label); - lilv_node_free(control->group); - lilv_node_free(control->min); - lilv_node_free(control->max); - lilv_node_free(control->def); - free(control); + free_control(jalv->controls.controls[i]); } free(jalv->controls.controls); - sratom_free(jalv->sratom); - sratom_free(jalv->ui_sratom); - serd_env_free(jalv->env); + jalv_dumper_free(jalv->dumper); lilv_uis_free(jalv->uis); + jalv_mapper_free(jalv->mapper); lilv_world_free(jalv->world); zix_sem_destroy(&jalv->done); - remove(jalv->temp_dir); - free(jalv->temp_dir); - free(jalv->ui_event_buf); + if (jalv->temp_dir) { + // Remove temporary state directory + const ZixStatus zst = zix_remove(jalv->temp_dir); + if (zst) { + jalv_log(JALV_LOG_WARNING, + "Failed to remove temporary directory %s (%s)\n", + jalv->temp_dir, + zix_strerror(zst)); + } + } + + zix_free(NULL, jalv->temp_dir); free(jalv->feature_list); free(jalv->opts.name); @@ -1487,25 +1043,3 @@ jalv_close(Jalv* const jalv) return 0; } - -int -main(int argc, char** argv) -{ - Jalv jalv; - memset(&jalv, '\0', sizeof(Jalv)); - - if (jalv_open(&jalv, &argc, &argv)) { - return EXIT_FAILURE; - } - - // Set up signal handlers - setup_signals(&jalv); - - // Run UI (or prompt at console) - jalv_frontend_open(&jalv); - - // Wait for finish signal from UI or signal handler - zix_sem_wait(&jalv.done); - - return jalv_close(&jalv); -} diff --git a/src/jalv.h b/src/jalv.h new file mode 100644 index 0000000..518f94d --- /dev/null +++ b/src/jalv.h @@ -0,0 +1,118 @@ +// Copyright 2007-2022 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#ifndef JALV_JALV_H +#define JALV_JALV_H + +#include "attributes.h" +#include "control.h" +#include "dumper.h" +#include "features.h" +#include "jalv_config.h" +#include "log.h" +#include "mapper.h" +#include "nodes.h" +#include "options.h" +#include "port.h" +#include "process.h" +#include "settings.h" +#include "types.h" +#include "urids.h" + +#if USE_SUIL +# include <suil/suil.h> +#endif + +#include <lilv/lilv.h> +#include <lv2/atom/forge.h> +#include <lv2/core/lv2.h> +#include <lv2/urid/urid.h> +#include <zix/sem.h> + +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> + +// "Shared" internal application declarations +JALV_BEGIN_DECLS + +/// Internal application state +struct JalvImpl { + JalvOptions opts; ///< Command-line options + LilvWorld* world; ///< Lilv World + JalvMapper* mapper; ///< URI mapper/unmapper + JalvURIDs urids; ///< URIDs + JalvNodes nodes; ///< Nodes + JalvLog log; ///< Log for error/warning/debug messages + LV2_Atom_Forge forge; ///< Atom forge + JalvDumper* dumper; ///< Atom dumper (console debug output) + JalvBackend* backend; ///< Audio system backend + JalvSettings settings; ///< Processing settings + void* ui_msg; ///< Buffer for messages in the UI thread + ZixSem work_lock; ///< Lock for plugin work() method + ZixSem done; ///< Exit semaphore + char* temp_dir; ///< Temporary plugin state directory + char* save_dir; ///< Plugin save directory + const LilvPlugin* plugin; ///< Plugin class (RDF data) + LilvState* preset; ///< Current preset + LilvUIs* uis; ///< All plugin UIs (RDF data) + const LilvUI* ui; ///< Plugin UI (RDF data) + const LilvNode* ui_type; ///< Plugin UI type (unwrapped) + JalvProcess process; ///< Process thread state +#if USE_SUIL + SuilHost* ui_host; ///< Plugin UI host support + SuilInstance* ui_instance; ///< Plugin UI instance (shared library) +#endif + void* window; ///< Window (if applicable) + JalvPort* ports; ///< Port array of size num_ports + Controls controls; ///< Available plugin controls + size_t ui_msg_size; ///< Maximum size of a single message + uint32_t num_ports; ///< Total number of ports on the plugin + bool safe_restore; ///< Plugin restore() is thread-safe + JalvFeatures features; + const LV2_Feature** feature_list; +}; + +/// Load the plugin and set up the application +int +jalv_open(Jalv* jalv, int* argc, char*** argv); + +/// Shut down the application (counterpart to jalv_open) +int +jalv_close(Jalv* jalv); + +/// Activate audio processing +int +jalv_activate(Jalv* jalv); + +/// Deactivate audio processing +int +jalv_deactivate(Jalv* jalv); + +/// Find a port by symbol +JalvPort* +jalv_port_by_symbol(Jalv* jalv, const char* sym); + +/// Set a control to the given value +void +jalv_set_control(Jalv* jalv, + const ControlID* control, + uint32_t size, + LV2_URID type, + const void* body); + +/// Request and/or set initial control values to initialize the UI +void +jalv_init_ui(Jalv* jalv); + +/// Instantiate the UI instance using suil if available +void +jalv_ui_instantiate(Jalv* jalv, const char* native_ui_type, void* parent); + +/// Periodically update user interface +int +jalv_update(Jalv* jalv); + +JALV_END_DECLS + +#endif // JALV_JALV_H diff --git a/src/jalv_config.h b/src/jalv_config.h index 3d0133e..4bcda8c 100644 --- a/src/jalv_config.h +++ b/src/jalv_config.h @@ -60,15 +60,6 @@ # endif # endif -// POSIX.1-2001: mlock() -# ifndef HAVE_MLOCK -# if defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112L -# define HAVE_MLOCK 1 -# else -# define HAVE_MLOCK 0 -# endif -# endif - // POSIX.1-2001: posix_memalign() # ifndef HAVE_POSIX_MEMALIGN # if defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112L @@ -142,12 +133,6 @@ # define USE_ISATTY 0 #endif -#if HAVE_MLOCK -# define USE_MLOCK 1 -#else -# define USE_MLOCK 0 -#endif - #if HAVE_POSIX_MEMALIGN # define USE_POSIX_MEMALIGN 1 #else diff --git a/src/jalv_console.c b/src/jalv_console.c index 635b668..fe92cd7 100644 --- a/src/jalv_console.c +++ b/src/jalv_console.c @@ -1,31 +1,34 @@ -// Copyright 2007-2022 David Robillard <d@drobilla.net> +// Copyright 2007-2024 David Robillard <d@drobilla.net> // SPDX-License-Identifier: ISC +#include "comm.h" #include "control.h" #include "frontend.h" +#include "jalv.h" #include "jalv_config.h" -#include "jalv_internal.h" #include "log.h" #include "options.h" #include "port.h" #include "state.h" +#include "string_utils.h" #include "types.h" -#include "lilv/lilv.h" -#include "lv2/ui/ui.h" -#include "zix/attributes.h" -#include "zix/sem.h" +#include <lilv/lilv.h> +#include <lv2/ui/ui.h> +#include <zix/attributes.h> +#include <zix/sem.h> #if USE_SUIL -# include "suil/suil.h" +# include <suil/suil.h> #endif #ifdef _WIN32 # include <synchapi.h> #else -# include <unistd.h> +# include <time.h> #endif +#include <assert.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> @@ -39,39 +42,55 @@ print_usage(const char* name, bool error) fprintf(os, "Usage: %s [OPTION...] PLUGIN_URI\n", name); fprintf(os, "Run an LV2 plugin as a Jack application.\n" - " -b SIZE Buffer size for plugin <=> UI communication\n" - " -c SYM=VAL Set control value (e.g. \"vol=1.4\")\n" - " -d Dump plugin <=> UI communication\n" - " -h Display this help and exit\n" - " -i Ignore keyboard input, run non-interactively\n" - " -l DIR Load state from save directory\n" - " -n NAME JACK client name\n" - " -p Print control output changes to stdout\n" - " -s Show plugin UI if possible\n" - " -t Print trace messages from plugin\n" - " -U URI Load the UI with the given URI\n" - " -V Display version information and exit\n" - " -x Exit if the requested JACK client name is taken.\n"); - return error ? 1 : 0; + " -b SIZE Buffer size for plugin <=> UI communication\n" + " -c SYM=VAL Set control value (like \"vol=1.4\")\n" + " -d Dump plugin <=> UI communication\n" + " -h Display this help and exit\n" + " -i Ignore keyboard input, run non-interactively\n" + " -l DIR Load state from save directory\n" + " -n NAME JACK client name\n" + " -p Print control output changes to stdout\n" + " -s Show plugin UI if possible\n" + " -t Print trace messages from plugin\n" + " -U URI Load the UI with the given URI\n" + " -V Display version information and exit\n" + " -x Exit if the requested JACK client name is taken\n"); + return error ? 1 : JALV_EARLY_EXIT_STATUS; } static int print_version(void) { printf("jalv " JALV_VERSION " <http://drobilla.net/software/jalv>\n"); - printf("Copyright 2011-2022 David Robillard <d@drobilla.net>.\n" + printf("Copyright 2011-2024 David Robillard <d@drobilla.net>\n" "License ISC: <https://spdx.org/licenses/ISC>.\n" "This is free software; you are free to change and redistribute it." "\nThere is NO WARRANTY, to the extent permitted by law.\n"); + return JALV_EARLY_EXIT_STATUS; +} + +static int +print_arg_error(const char* const command, const char* const msg) +{ + fprintf(stderr, "%s: %s\n", command, msg); return 1; } +static void +print_control_port(const Jalv* const jalv, + const JalvPort* const port, + const float value) +{ + const LilvNode* sym = lilv_port_get_symbol(jalv->plugin, port->lilv_port); + jalv_log(JALV_LOG_INFO, "%s = %f\n", lilv_node_as_string(sym), value); +} + void -jalv_ui_port_event(Jalv* jalv, - uint32_t port_index, - uint32_t buffer_size, - uint32_t protocol, - const void* buffer) +jalv_frontend_port_event(Jalv* jalv, + uint32_t port_index, + uint32_t buffer_size, + uint32_t protocol, + const void* buffer) { #if USE_SUIL if (jalv->ui_instance) { @@ -79,80 +98,90 @@ jalv_ui_port_event(Jalv* jalv, jalv->ui_instance, port_index, buffer_size, protocol, buffer); } #else - (void)jalv; - (void)port_index; (void)buffer_size; - (void)protocol; - (void)buffer; #endif + + if (!protocol && jalv->opts.print_controls) { + assert(buffer_size == sizeof(float)); + print_control_port(jalv, &jalv->ports[port_index], *(float*)buffer); + } } int -jalv_frontend_init(int* argc, char*** argv, JalvOptions* opts) +jalv_frontend_init(JalvFrontendArgs* const args, JalvOptions* const opts) { + const int argc = *args->argc; + char** argv = *args->argv; + + const char* const cmd = argv[0]; + int n_controls = 0; int a = 1; - for (; a < *argc && (*argv)[a][0] == '-'; ++a) { - if ((*argv)[a][1] == 'h') { - return print_usage((*argv)[0], true); + for (; a < argc && argv[a][0] == '-'; ++a) { + if (argv[a][1] == 'h' || !strcmp(argv[a], "--help")) { + return print_usage(cmd, false); } - if ((*argv)[a][1] == 'V') { + if (argv[a][1] == 'V' || !strcmp(argv[a], "--version")) { return print_version(); } - if ((*argv)[a][1] == 's') { + if (argv[a][1] == 's') { opts->show_ui = true; - } else if ((*argv)[a][1] == 'p') { + } else if (argv[a][1] == 'p') { opts->print_controls = true; - } else if ((*argv)[a][1] == 'U') { - if (++a == *argc) { - fprintf(stderr, "Missing argument for -U\n"); - return 1; + } else if (argv[a][1] == 'U') { + if (++a == argc) { + return print_arg_error(cmd, "option requires an argument -- 'U'"); + } + opts->ui_uri = jalv_strdup(argv[a]); + } else if (argv[a][1] == 'l') { + if (++a == argc) { + return print_arg_error(cmd, "option requires an argument -- 'l'"); } - opts->ui_uri = jalv_strdup((*argv)[a]); - } else if ((*argv)[a][1] == 'l') { - if (++a == *argc) { - fprintf(stderr, "Missing argument for -l\n"); - return 1; + opts->load = jalv_strdup(argv[a]); + } else if (argv[a][1] == 'b') { + if (++a == argc) { + return print_arg_error(cmd, "option requires an argument -- 'b'"); } - opts->load = jalv_strdup((*argv)[a]); - } else if ((*argv)[a][1] == 'b') { - if (++a == *argc) { - fprintf(stderr, "Missing argument for -b\n"); - return 1; + opts->ring_size = atoi(argv[a]); + } else if (argv[a][1] == 'c') { + if (++a == argc) { + return print_arg_error(cmd, "option requires an argument -- 'c'"); } - opts->buffer_size = atoi((*argv)[a]); - } else if ((*argv)[a][1] == 'c') { - if (++a == *argc) { - fprintf(stderr, "Missing argument for -c\n"); - return 1; + + char** new_controls = + (char**)realloc(opts->controls, (n_controls + 2) * sizeof(char*)); + if (!new_controls) { + fprintf(stderr, "Out of memory\n"); + return 12; } - opts->controls = - (char**)realloc(opts->controls, (++n_controls + 1) * sizeof(char*)); - opts->controls[n_controls - 1] = (*argv)[a]; - opts->controls[n_controls] = NULL; - } else if ((*argv)[a][1] == 'i') { + + opts->controls = new_controls; + opts->controls[n_controls++] = argv[a]; + opts->controls[n_controls] = NULL; + } else if (argv[a][1] == 'i') { opts->non_interactive = true; - } else if ((*argv)[a][1] == 'd') { + } else if (argv[a][1] == 'd') { opts->dump = true; - } else if ((*argv)[a][1] == 't') { + } else if (argv[a][1] == 't') { opts->trace = true; - } else if ((*argv)[a][1] == 'n') { - if (++a == *argc) { - fprintf(stderr, "Missing argument for -n\n"); - return 1; + } else if (argv[a][1] == 'n') { + if (++a == argc) { + return print_arg_error(cmd, "option requires an argument -- 'n'"); } free(opts->name); - opts->name = jalv_strdup((*argv)[a]); - } else if ((*argv)[a][1] == 'x') { + opts->name = jalv_strdup(argv[a]); + } else if (argv[a][1] == 'x') { opts->name_exact = 1; } else { - fprintf(stderr, "Unknown option %s\n", (*argv)[a]); - return print_usage((*argv)[0], true); + fprintf(stderr, "%s: unknown option -- '%c'\n", cmd, argv[a][1]); + return print_usage(argv[0], true); } } + *args->argc = *args->argc - a; + *args->argv = *args->argv + a; return 0; } @@ -163,17 +192,16 @@ jalv_frontend_ui_type(void) } static void -jalv_print_controls(Jalv* jalv, bool writable, bool readable) +print_controls(const Jalv* const jalv, const bool writable, const bool readable) { for (size_t i = 0; i < jalv->controls.n_controls; ++i) { ControlID* const control = jalv->controls.controls[i]; - if ((control->is_writable && writable) || - (control->is_readable && readable)) { - struct Port* const port = &jalv->ports[control->index]; + if (control->type == PORT && ((control->is_writable && writable) || + (control->is_readable && readable))) { jalv_log(JALV_LOG_INFO, "%s = %f\n", lilv_node_as_string(control->symbol), - port->control); + jalv->process.controls_buf[control->id.index]); } } @@ -215,32 +243,24 @@ jalv_process_command(Jalv* jalv, const char* cmd) lilv_world_load_resource(jalv->world, preset); jalv_apply_preset(jalv, preset); lilv_node_free(preset); - jalv_print_controls(jalv, true, false); + print_controls(jalv, true, false); } else if (strcmp(cmd, "controls\n") == 0) { - jalv_print_controls(jalv, true, false); + print_controls(jalv, true, false); } else if (strcmp(cmd, "monitors\n") == 0) { - jalv_print_controls(jalv, false, true); + print_controls(jalv, false, true); } else if (sscanf(cmd, "set %u %f", &index, &value) == 2) { if (index < jalv->num_ports) { - jalv->ports[index].control = value; - jalv_print_control(jalv, &jalv->ports[index], value); + jalv_write_control(jalv->process.ui_to_plugin, index, value); + print_control_port(jalv, &jalv->ports[index], value); } else { fprintf(stderr, "error: port index out of range\n"); } } else if (sscanf(cmd, "set %1023[a-zA-Z0-9_] %f", sym, &value) == 2 || sscanf(cmd, "%1023[a-zA-Z0-9_] = %f", sym, &value) == 2) { - struct Port* port = NULL; - for (uint32_t i = 0; i < jalv->num_ports; ++i) { - struct Port* p = &jalv->ports[i]; - const LilvNode* s = lilv_port_get_symbol(jalv->plugin, p->lilv_port); - if (!strcmp(lilv_node_as_string(s), sym)) { - port = p; - break; - } - } + const JalvPort* const port = jalv_port_by_symbol(jalv, sym); if (port) { - port->control = value; - jalv_print_control(jalv, port, value); + jalv->process.controls_buf[port->index] = value; + print_control_port(jalv, port, value); } else { fprintf(stderr, "error: no control named `%s'\n", sym); } @@ -250,7 +270,7 @@ jalv_process_command(Jalv* jalv, const char* cmd) } bool -jalv_frontend_discover(Jalv* jalv) +jalv_frontend_discover(const Jalv* jalv) { return jalv->opts.show_ui; } @@ -282,7 +302,8 @@ jalv_run_custom_ui(Jalv* jalv) # ifdef _WIN32 Sleep(33); # else - usleep(33333); + const struct timespec delay = {0, 33333000}; + nanosleep(&delay, NULL); # endif } @@ -297,13 +318,13 @@ jalv_run_custom_ui(Jalv* jalv) } float -jalv_frontend_refresh_rate(Jalv* ZIX_UNUSED(jalv)) +jalv_frontend_refresh_rate(const Jalv* ZIX_UNUSED(jalv)) { return 30.0f; } float -jalv_frontend_scale_factor(Jalv* ZIX_UNUSED(jalv)) +jalv_frontend_scale_factor(const Jalv* ZIX_UNUSED(jalv)) { return 1.0f; } @@ -318,6 +339,16 @@ jalv_frontend_select_plugin(Jalv* jalv) int jalv_frontend_open(Jalv* jalv) { + // Print initial control values + for (size_t i = 0; i < jalv->controls.n_controls; ++i) { + ControlID* control = jalv->controls.controls[i]; + if (control->type == PORT && control->is_writable) { + const JalvPort* const port = &jalv->ports[control->id.index]; + print_control_port( + jalv, port, jalv->process.controls_buf[control->id.index]); + } + } + if (!jalv_run_custom_ui(jalv) && !jalv->opts.non_interactive) { // Primitive command prompt for setting control values while (zix_sem_try_wait(&jalv->done)) { diff --git a/src/jalv_gtk.c b/src/jalv_gtk.c index 5b7240b..16cff34 100644 --- a/src/jalv_gtk.c +++ b/src/jalv_gtk.c @@ -3,23 +3,23 @@ #include "control.h" #include "frontend.h" -#include "jalv_internal.h" +#include "jalv.h" #include "log.h" #include "options.h" -#include "port.h" +#include "query.h" #include "state.h" #include "types.h" -#include "lilv/lilv.h" -#include "lv2/atom/atom.h" -#include "lv2/atom/forge.h" -#include "lv2/atom/util.h" -#include "lv2/core/lv2.h" -#include "lv2/ui/ui.h" -#include "lv2/urid/urid.h" -#include "suil/suil.h" -#include "zix/attributes.h" -#include "zix/sem.h" +#include <lilv/lilv.h> +#include <lv2/atom/atom.h> +#include <lv2/atom/forge.h> +#include <lv2/atom/util.h> +#include <lv2/core/lv2.h> +#include <lv2/ui/ui.h> +#include <lv2/urid/urid.h> +#include <suil/suil.h> +#include <zix/attributes.h> +#include <zix/sem.h> #include <gdk/gdk.h> #include <glib-object.h> @@ -48,10 +48,10 @@ static Jalv* s_jalv = NULL; static GtkCheckMenuItem* active_preset_item = NULL; static bool updating = false; -/// Widget for a control +/// Widget(s) for a control port or parameter typedef struct { - GtkSpinButton* spin; - GtkWidget* control; + GtkSpinButton* spin; ///< Spinner for numbers, or null + GtkWidget* control; ///< Primary value control } Controller; static float @@ -63,13 +63,15 @@ get_float(const LilvNode* node, float fallback) } static void -on_window_destroy(GtkWidget* ZIX_UNUSED(widget), gpointer ZIX_UNUSED(data)) +on_window_destroy(GtkWidget* widget, gpointer data) { + (void)widget; + (void)data; gtk_main_quit(); } int -jalv_frontend_init(int* argc, char*** argv, JalvOptions* opts) +jalv_frontend_init(JalvFrontendArgs* const args, JalvOptions* const opts) { const GOptionEntry entries[] = { {"preset", @@ -97,7 +99,7 @@ jalv_frontend_init(int* argc, char*** argv, JalvOptions* opts) 'b', 0, G_OPTION_ARG_INT, - &opts->buffer_size, + &opts->ring_size, "Buffer size for plugin <=> UI communication", "SIZE"}, {"control", @@ -181,8 +183,8 @@ jalv_frontend_init(int* argc, char*** argv, JalvOptions* opts) GError* error = NULL; const int err = - gtk_init_with_args(argc, - argv, + gtk_init_with_args(args->argc, + args->argv, "PLUGIN_URI - Run an LV2 plugin as a Jack application", entries, NULL, @@ -192,6 +194,8 @@ jalv_frontend_init(int* argc, char*** argv, JalvOptions* opts) fprintf(stderr, "%s\n", error->message); } + --*args->argc; + ++*args->argv; return !err; } @@ -305,10 +309,10 @@ typedef struct { } PresetMenu; static PresetMenu* -pset_menu_new(const char* label) +pset_menu_new(char* const label) { PresetMenu* menu = (PresetMenu*)malloc(sizeof(PresetMenu)); - menu->label = g_strdup(label); + menu->label = label; menu->item = GTK_MENU_ITEM(gtk_menu_item_new_with_label(menu->label)); menu->menu = GTK_MENU(gtk_menu_new()); menu->banks = NULL; @@ -338,22 +342,35 @@ menu_cmp(gconstpointer a, gconstpointer b, gpointer ZIX_UNUSED(data)) return strcmp(((const PresetMenu*)a)->label, ((const PresetMenu*)b)->label); } +static char* +get_label_string(Jalv* const jalv, const LilvNode* const node) +{ + LilvNode* const label_node = + lilv_world_get(jalv->world, node, jalv->nodes.rdfs_label, NULL); + + if (!label_node) { + return g_strdup(lilv_node_as_string(node)); + } + + char* const label = g_strdup(lilv_node_as_string(label_node)); + lilv_node_free(label_node); + return label; +} + static PresetMenu* get_bank_menu(Jalv* jalv, PresetMenu* menu, const LilvNode* bank) { - LilvNode* label = - lilv_world_get(jalv->world, bank, jalv->nodes.rdfs_label, NULL); - - const char* uri = lilv_node_as_string(bank); - const char* str = label ? lilv_node_as_string(label) : uri; - PresetMenu key = {NULL, (char*)str, NULL, NULL}; - GSequenceIter* i = g_sequence_lookup(menu->banks, &key, menu_cmp, NULL); + char* const label = get_label_string(jalv, bank); + PresetMenu key = {NULL, label, NULL, NULL}; + GSequenceIter* i = g_sequence_lookup(menu->banks, &key, menu_cmp, NULL); if (!i) { - PresetMenu* bank_menu = pset_menu_new(str); + PresetMenu* const bank_menu = pset_menu_new(label); gtk_menu_item_set_submenu(bank_menu->item, GTK_WIDGET(bank_menu->menu)); g_sequence_insert_sorted(menu->banks, bank_menu, menu_cmp, NULL); return bank_menu; } + + g_free(label); return (PresetMenu*)g_sequence_get(i); } @@ -373,7 +390,7 @@ add_preset_to_menu(Jalv* jalv, active_preset_item = GTK_CHECK_MENU_ITEM(item); } - LilvNode* bank = + const LilvNode* bank = lilv_world_get(jalv->world, node, jalv->nodes.pset_bank, NULL); if (bank) { @@ -569,20 +586,21 @@ differ_enough(float a, float b) static void set_float_control(const ControlID* control, float value) { - if (control->value_type == control->forge->Int) { + const LV2_Atom_Forge* const forge = &s_jalv->forge; + if (control->value_type == forge->Int) { const int32_t ival = lrintf(value); - set_control(control, sizeof(ival), control->forge->Int, &ival); - } else if (control->value_type == control->forge->Long) { + set_control(control, sizeof(ival), forge->Int, &ival); + } else if (control->value_type == forge->Long) { const int64_t lval = lrintf(value); - set_control(control, sizeof(lval), control->forge->Long, &lval); - } else if (control->value_type == control->forge->Float) { - set_control(control, sizeof(value), control->forge->Float, &value); - } else if (control->value_type == control->forge->Double) { + set_control(control, sizeof(lval), forge->Long, &lval); + } else if (control->value_type == forge->Float) { + set_control(control, sizeof(value), forge->Float, &value); + } else if (control->value_type == forge->Double) { const double dval = value; - set_control(control, sizeof(dval), control->forge->Double, &dval); - } else if (control->value_type == control->forge->Bool) { + set_control(control, sizeof(dval), forge->Double, &dval); + } else if (control->value_type == forge->Bool) { const int32_t ival = value; - set_control(control, sizeof(ival), control->forge->Bool, &ival); + set_control(control, sizeof(ival), forge->Bool, &ival); } Controller* controller = (Controller*)control->widget; @@ -593,7 +611,7 @@ set_float_control(const ControlID* control, float value) } static double -get_atom_double(Jalv* jalv, +get_atom_double(const Jalv* jalv, uint32_t ZIX_UNUSED(size), LV2_URID type, const void* body) @@ -618,7 +636,7 @@ get_atom_double(Jalv* jalv, } static void -control_changed(Jalv* jalv, +control_changed(const Jalv* jalv, Controller* controller, uint32_t size, LV2_URID type, @@ -716,8 +734,8 @@ on_request_value(LV2UI_Feature_Handle handle, const LV2_URID ZIX_UNUSED(type), const LV2_Feature* const* ZIX_UNUSED(features)) { - Jalv* jalv = (Jalv*)handle; - ControlID* control = get_property_control(&jalv->controls, key); + Jalv* const jalv = (Jalv*)handle; + const ControlID* control = get_property_control(&jalv->controls, key); if (!control) { return LV2UI_REQUEST_VALUE_ERR_UNKNOWN; @@ -760,11 +778,11 @@ property_changed(Jalv* jalv, LV2_URID key, const LV2_Atom* value) } void -jalv_ui_port_event(Jalv* jalv, - uint32_t port_index, - uint32_t buffer_size, - uint32_t protocol, - const void* buffer) +jalv_frontend_port_event(Jalv* jalv, + uint32_t port_index, + uint32_t buffer_size, + uint32_t protocol, + const void* buffer) { if (jalv->ui_instance) { suil_instance_port_event( @@ -772,17 +790,13 @@ jalv_ui_port_event(Jalv* jalv, return; } - if (protocol == 0 && (Controller*)jalv->ports[port_index].widget) { - control_changed(jalv, - (Controller*)jalv->ports[port_index].widget, - buffer_size, - jalv->forge.Float, - buffer); - return; - } - if (protocol == 0) { - return; // No widget (probably notOnGUI) + Controller* const controller = (Controller*)jalv->ports[port_index].widget; + if (controller) { + control_changed(jalv, controller, buffer_size, jalv->forge.Float, buffer); + } + + return; } if (protocol != jalv->urids.atom_eventTransfer) { @@ -803,8 +817,7 @@ jalv_ui_port_event(Jalv* jalv, } else if (obj->body.otype == jalv->urids.patch_Put) { const LV2_Atom_Object* body = NULL; if (!patch_put_get(jalv, obj, &body)) { - LV2_ATOM_OBJECT_FOREACH(body, prop) - { + LV2_ATOM_OBJECT_FOREACH (body, prop) { property_changed(jalv, prop->key, &prop->value); } } @@ -886,17 +899,17 @@ switch_changed(GtkSwitch* toggle_switch, gboolean state, gpointer data) static void string_changed(GtkEntry* widget, gpointer data) { - ControlID* control = (ControlID*)data; - const char* string = gtk_entry_get_text(widget); + const ControlID* control = (const ControlID*)data; + const char* string = gtk_entry_get_text(widget); - set_control(control, strlen(string) + 1, control->forge->String, string); + set_control(control, strlen(string) + 1, s_jalv->forge.String, string); } static void file_changed(GtkFileChooserButton* widget, gpointer data) { - ControlID* control = (ControlID*)data; - const char* filename = + const ControlID* control = (const ControlID*)data; + const char* filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget)); set_control(control, strlen(filename) + 1, s_jalv->forge.Path, filename); @@ -1095,9 +1108,11 @@ make_controller(ControlID* control, float value) static GtkWidget* new_label(const char* text, bool title, GtkAlign xalign, GtkAlign yalign) { - GtkWidget* label = gtk_label_new(NULL); - const char* fmt = title ? "<span font_weight=\"bold\">%s</span>" : "%s:"; - gchar* str = g_markup_printf_escaped(fmt, text); + GtkWidget* const label = gtk_label_new(NULL); + gchar* const str = + title + ? g_markup_printf_escaped("<span font_weight=\"bold\">%s</span>", text) + : g_markup_printf_escaped("%s:", text); gtk_widget_set_halign(label, xalign); gtk_widget_set_valign(label, yalign); @@ -1153,8 +1168,8 @@ build_control_widget(Jalv* jalv, GtkWidget* window) g_array_sort_with_data(controls, control_group_cmp, jalv); // Add controls in group order - LilvNode* last_group = NULL; - int n_rows = 0; + const LilvNode* last_group = NULL; + int n_rows = 0; for (size_t i = 0; i < controls->len; ++i) { ControlID* record = g_array_index(controls, ControlID*, i); Controller* controller = NULL; @@ -1175,6 +1190,7 @@ build_control_widget(Jalv* jalv, GtkWidget* window) GTK_ALIGN_START, GTK_ALIGN_BASELINE); + lilv_node_free(group_name); gtk_grid_attach(GTK_GRID(port_grid), group_label, 0, n_rows, 3, 1); ++n_rows; } @@ -1192,7 +1208,7 @@ build_control_widget(Jalv* jalv, GtkWidget* window) record->widget = controller; if (record->type == PORT) { - jalv->ports[record->index].widget = controller; + jalv->ports[record->id.index].widget = controller; } if (controller) { // Add row to table for this controller @@ -1289,13 +1305,13 @@ build_menu(Jalv* jalv, GtkWidget* window, GtkWidget* vbox) } bool -jalv_frontend_discover(Jalv* ZIX_UNUSED(jalv)) +jalv_frontend_discover(const Jalv* ZIX_UNUSED(jalv)) { return TRUE; } float -jalv_frontend_refresh_rate(Jalv* ZIX_UNUSED(jalv)) +jalv_frontend_refresh_rate(const Jalv* ZIX_UNUSED(jalv)) { GdkDisplay* const display = gdk_display_get_default(); GdkMonitor* const monitor = gdk_display_get_primary_monitor(display); @@ -1306,7 +1322,7 @@ jalv_frontend_refresh_rate(Jalv* ZIX_UNUSED(jalv)) } float -jalv_frontend_scale_factor(Jalv* ZIX_UNUSED(jalv)) +jalv_frontend_scale_factor(const Jalv* ZIX_UNUSED(jalv)) { GdkDisplay* const display = gdk_display_get_default(); GdkMonitor* const monitor = gdk_display_get_primary_monitor(display); @@ -1318,7 +1334,7 @@ static void on_row_activated(GtkTreeView* const tree_view, GtkTreePath* const path, GtkTreeViewColumn* const column, - const void* const user_data) + void* const user_data) { (void)tree_view; (void)path; @@ -1473,7 +1489,8 @@ jalv_frontend_open(Jalv* jalv) GtkWidget* widget = (GtkWidget*)suil_instance_get_widget(jalv->ui_instance); gtk_container_add(GTK_CONTAINER(ui_box), widget); - gtk_window_set_resizable(GTK_WINDOW(window), jalv_ui_is_resizable(jalv)); + gtk_window_set_resizable(GTK_WINDOW(window), + jalv_ui_is_resizable(jalv->world, jalv->ui)); gtk_widget_show_all(vbox); gtk_widget_grab_focus(widget); } else { @@ -1501,20 +1518,27 @@ jalv_frontend_open(Jalv* jalv) jalv_init_ui(jalv); - g_timeout_add(1000 / jalv->ui_update_hz, (GSourceFunc)jalv_update, jalv); + const float update_interval_ms = 1000.0f / jalv->settings.ui_update_hz; + g_timeout_add((unsigned)update_interval_ms, (GSourceFunc)jalv_update, jalv); gtk_window_present(GTK_WINDOW(window)); gtk_main(); suil_instance_free(jalv->ui_instance); + + for (unsigned i = 0U; i < jalv->controls.n_controls; ++i) { + free(jalv->controls.controls[i]->widget); // free Controller + } + jalv->ui_instance = NULL; zix_sem_post(&jalv->done); return 0; } int -jalv_frontend_close(Jalv* ZIX_UNUSED(jalv)) +jalv_frontend_close(Jalv* jalv) { + (void)jalv; gtk_main_quit(); s_jalv = NULL; return 0; diff --git a/src/jalv_internal.h b/src/jalv_internal.h deleted file mode 100644 index 01d4c86..0000000 --- a/src/jalv_internal.h +++ /dev/null @@ -1,230 +0,0 @@ -// Copyright 2007-2022 David Robillard <d@drobilla.net> -// SPDX-License-Identifier: ISC - -#ifndef JALV_INTERNAL_H -#define JALV_INTERNAL_H - -#include "attributes.h" -#include "control.h" -#include "jalv_config.h" -#include "log.h" -#include "nodes.h" -#include "options.h" -#include "symap.h" -#include "types.h" -#include "urids.h" -#include "worker.h" - -#include "zix/ring.h" -#include "zix/sem.h" - -#include "lilv/lilv.h" -#include "serd/serd.h" -#include "sratom/sratom.h" -#if USE_SUIL -# include "suil/suil.h" -#endif - -#include "lv2/atom/forge.h" -#include "lv2/core/lv2.h" -#include "lv2/data-access/data-access.h" -#include "lv2/log/log.h" -#include "lv2/options/options.h" -#include "lv2/state/state.h" -#include "lv2/ui/ui.h" -#include "lv2/urid/urid.h" -#include "lv2/worker/worker.h" - -#include <stdbool.h> -#include <stdint.h> -#include <stdio.h> - -JALV_BEGIN_DECLS - -typedef struct { - LV2_Feature map_feature; - LV2_Feature unmap_feature; - LV2_State_Make_Path make_path; - LV2_Feature make_path_feature; - LV2_Worker_Schedule sched; - LV2_Feature sched_feature; - LV2_Worker_Schedule ssched; - LV2_Feature state_sched_feature; - LV2_Log_Log llog; - LV2_Feature log_feature; - LV2_Options_Option options[7]; - LV2_Feature options_feature; - LV2_Feature safe_restore_feature; - LV2UI_Request_Value request_value; - LV2_Feature request_value_feature; - LV2_Extension_Data_Feature ext_data; -} JalvFeatures; - -struct JalvImpl { - JalvOptions opts; ///< Command-line options - JalvURIDs urids; ///< URIDs - JalvNodes nodes; ///< Nodes - JalvLog log; ///< Log for error/warning/debug messages - LV2_Atom_Forge forge; ///< Atom forge - LilvWorld* world; ///< Lilv World - LV2_URID_Map map; ///< URI => Int map - LV2_URID_Unmap unmap; ///< Int => URI map - SerdEnv* env; ///< Environment for RDF printing - Sratom* sratom; ///< Atom serialiser - Sratom* ui_sratom; ///< Atom serialiser for UI thread - Symap* symap; ///< URI map - ZixSem symap_lock; ///< Lock for URI map - JalvBackend* backend; ///< Audio system backend - ZixRing* ui_to_plugin; ///< Port events from UI - ZixRing* plugin_to_ui; ///< Port events from plugin - void* ui_event_buf; ///< Buffer for reading UI port events - JalvWorker* worker; ///< Worker thread implementation - JalvWorker* state_worker; ///< Synchronous worker for state restore - ZixSem work_lock; ///< Lock for plugin work() method - ZixSem done; ///< Exit semaphore - ZixSem paused; ///< Paused signal from process thread - JalvPlayState play_state; ///< Current play state - char* temp_dir; ///< Temporary plugin state directory - char* save_dir; ///< Plugin save directory - const LilvPlugin* plugin; ///< Plugin class (RDF data) - LilvState* preset; ///< Current preset - LilvUIs* uis; ///< All plugin UIs (RDF data) - const LilvUI* ui; ///< Plugin UI (RDF data) - const LilvNode* ui_type; ///< Plugin UI type (unwrapped) - LilvInstance* instance; ///< Plugin instance (shared library) -#if USE_SUIL - SuilHost* ui_host; ///< Plugin UI host support - SuilInstance* ui_instance; ///< Plugin UI instance (shared library) -#endif - void* window; ///< Window (if applicable) - struct Port* ports; ///< Port array of size num_ports - Controls controls; ///< Available plugin controls - uint32_t block_length; ///< Audio buffer size (block length) - size_t midi_buf_size; ///< Size of MIDI port buffers - uint32_t control_in; ///< Index of control input port - uint32_t num_ports; ///< Size of the two following arrays: - uint32_t plugin_latency; ///< Latency reported by plugin (if any) - float ui_update_hz; ///< Frequency of UI updates - float ui_scale_factor; ///< UI scale factor - float sample_rate; ///< Sample rate - uint32_t event_delta_t; ///< Frames since last update sent to UI - uint32_t position; ///< Transport position in frames - float bpm; ///< Transport tempo in beats per minute - bool rolling; ///< Transport speed (0=stop, 1=play) - bool buf_size_set; ///< True iff buffer size callback fired - bool has_ui; ///< True iff a control UI is present - bool request_update; ///< True iff a plugin update is needed - bool safe_restore; ///< Plugin restore() is thread-safe - JalvFeatures features; - const LV2_Feature** feature_list; -}; - -int -jalv_open(Jalv* jalv, int* argc, char*** argv); - -int -jalv_close(Jalv* jalv); - -void -jalv_create_ports(Jalv* jalv); - -void -jalv_allocate_port_buffers(Jalv* jalv); - -struct Port* -jalv_port_by_symbol(Jalv* jalv, const char* sym); - -void -jalv_create_controls(Jalv* jalv, bool writable); - -ControlID* -jalv_control_by_symbol(Jalv* jalv, const char* sym); - -void -jalv_set_control(Jalv* jalv, - const ControlID* control, - uint32_t size, - LV2_URID type, - const void* body); - -void -jalv_init_ui(Jalv* jalv); - -void -jalv_ui_instantiate(Jalv* jalv, const char* native_ui_type, void* parent); - -bool -jalv_ui_is_resizable(Jalv* jalv); - -void -jalv_send_to_plugin(void* jalv_handle, - uint32_t port_index, - uint32_t buffer_size, - uint32_t protocol, - const void* buffer); - -void -jalv_apply_ui_events(Jalv* jalv, uint32_t nframes); - -void -jalv_ui_port_event(Jalv* jalv, - uint32_t port_index, - uint32_t buffer_size, - uint32_t protocol, - const void* buffer); - -/** - Write a port event using the atom:eventTransfer protocol. - - This is used to transfer atoms between the plugin and UI via sequence ports. - - @param jalv Jalv instance. - @param target Communication ring (jalv->plugin_to_ui or jalv->ui_to_plugin). - @param port_index Index of the port this change is for. - @param size Size of body in bytes. - @param type Atom type URID. - @param body Atom body. - @return 0 on success, non-zero on failure (overflow). -*/ -int -jalv_write_event(Jalv* jalv, - ZixRing* target, - uint32_t port_index, - uint32_t size, - LV2_URID type, - const void* body); - -/** - Write a control port change using the default (0) protocol. - - This is used to transfer control port value changes between the plugin and - UI. - - @param jalv Jalv instance. - @param target Communication ring (jalv->plugin_to_ui or jalv->ui_to_plugin). - @param port_index Index of the port this change is for. - @param value New control port value. - @return 0 on success, non-zero on failure (overflow). -*/ -int -jalv_write_control(Jalv* jalv, - ZixRing* target, - uint32_t port_index, - float value); - -void -jalv_dump_atom(Jalv* jalv, - FILE* stream, - const char* label, - const LV2_Atom* atom, - int color); - -bool -jalv_run(Jalv* jalv, uint32_t nframes); - -int -jalv_update(Jalv* jalv); - -JALV_END_DECLS - -#endif // JALV_INTERNAL_H diff --git a/src/jalv_qt.cpp b/src/jalv_qt.cpp index a63a508..7d99118 100644 --- a/src/jalv_qt.cpp +++ b/src/jalv_qt.cpp @@ -2,15 +2,19 @@ // SPDX-License-Identifier: ISC #include "jalv_qt.hpp" +#include "comm.h" #include "frontend.h" -#include "jalv_internal.h" +#include "jalv.h" #include "nodes.h" #include "options.h" #include "port.h" +#include "query.h" +#include "state.h" +#include "types.h" -#include "lilv/lilv.h" -#include "suil/suil.h" -#include "zix/sem.h" +#include <lilv/lilv.h> +#include <suil/suil.h> +#include <zix/sem.h> #include <QAction> #include <QApplication> @@ -25,6 +29,7 @@ #include <QLayoutItem> #include <QList> #include <QMainWindow> +#include <QMargins> #include <QMenu> #include <QMenuBar> #include <QObject> @@ -40,6 +45,7 @@ #include <QVBoxLayout> #include <QWidget> #include <QtCore> +#include <QtGlobal> #include <algorithm> #include <cmath> @@ -51,13 +57,13 @@ constexpr int CONTROL_WIDTH = 150; constexpr int DIAL_STEPS = 10000; -static QApplication* app = nullptr; +namespace { + +QApplication* app = nullptr; class FlowLayout final : public QLayout { public: - explicit FlowLayout(QWidget* parent, int margin, int hSpacing, int vSpacing); - explicit FlowLayout(int margin, int hSpacing, int vSpacing); FlowLayout(const FlowLayout&) = delete; @@ -85,22 +91,14 @@ private: int doLayout(const QRect& rect, bool testOnly) const; int smartSpacing(QStyle::PixelMetric pm) const; - QList<QLayoutItem*> itemList; - int m_hSpace; - int m_vSpace; + QList<QLayoutItem*> _itemList; + int _hSpace; + int _vSpace; }; -FlowLayout::FlowLayout(QWidget* parent, int margin, int hSpacing, int vSpacing) - : QLayout(parent) - , m_hSpace(hSpacing) - , m_vSpace(vSpacing) -{ - setContentsMargins(margin, margin, margin, margin); -} - FlowLayout::FlowLayout(int margin, int hSpacing, int vSpacing) - : m_hSpace(hSpacing) - , m_vSpace(vSpacing) + : _hSpace(hSpacing) + , _vSpace(vSpacing) { setContentsMargins(margin, margin, margin, margin); } @@ -116,14 +114,14 @@ FlowLayout::~FlowLayout() void FlowLayout::addItem(QLayoutItem* item) { - itemList.append(item); + _itemList.append(item); } int FlowLayout::horizontalSpacing() const { - if (m_hSpace >= 0) { - return m_hSpace; + if (_hSpace >= 0) { + return _hSpace; } return smartSpacing(QStyle::PM_LayoutHorizontalSpacing); @@ -132,8 +130,8 @@ FlowLayout::horizontalSpacing() const int FlowLayout::verticalSpacing() const { - if (m_vSpace >= 0) { - return m_vSpace; + if (_vSpace >= 0) { + return _vSpace; } return smartSpacing(QStyle::PM_LayoutVerticalSpacing); @@ -142,20 +140,20 @@ FlowLayout::verticalSpacing() const int FlowLayout::count() const { - return itemList.size(); + return _itemList.size(); } QLayoutItem* FlowLayout::itemAt(int index) const { - return itemList.value(index); + return _itemList.value(index); } QLayoutItem* FlowLayout::takeAt(int index) { - if (index >= 0 && index < itemList.size()) { - return itemList.takeAt(index); + if (index >= 0 && index < _itemList.size()) { + return _itemList.takeAt(index); } return nullptr; @@ -195,13 +193,13 @@ FlowLayout::sizeHint() const QSize FlowLayout::minimumSize() const { - QSize size = {}; - QLayoutItem* item = nullptr; - foreach (item, itemList) { + QSize size = {}; + for (QLayoutItem* const item : _itemList) { size = size.expandedTo(item->minimumSize()); } - return size + QSize(2 * margin(), 2 * margin()); + const auto m = contentsMargins(); + return size + QSize{m.left() + m.right(), m.top() + m.bottom()}; } int @@ -218,8 +216,7 @@ FlowLayout::doLayout(const QRect& rect, bool testOnly) const int y = effectiveRect.y(); int lineHeight = 0; - QLayoutItem* item = nullptr; - foreach (item, itemList) { + for (QLayoutItem* const item : _itemList) { QWidget* wid = item->widget(); int spaceX = horizontalSpacing(); @@ -260,52 +257,22 @@ FlowLayout::doLayout(const QRect& rect, bool testOnly) const int FlowLayout::smartSpacing(QStyle::PixelMetric pm) const { - QObject* parent = this->parent(); + QObject* const parent = this->parent(); if (!parent) { return -1; } - if (parent->isWidgetType()) { - auto* const pw = static_cast<QWidget*>(parent); - return pw->style()->pixelMetric(pm, nullptr, pw); + auto* const parentWidget = qobject_cast<QWidget*>(parent); + if (parentWidget) { + return parentWidget->style()->pixelMetric(pm, nullptr, parentWidget); } - return static_cast<QLayout*>(parent)->spacing(); -} - -extern "C" { - -int -jalv_frontend_init(int* argc, char*** argv, JalvOptions*) -{ - app = new QApplication(*argc, *argv, true); - app->setStyleSheet("QGroupBox::title { subcontrol-position: top center }"); - - return 0; -} - -const char* -jalv_frontend_ui_type(void) -{ - return "http://lv2plug.in/ns/extensions/ui#Qt5UI"; -} - -void -jalv_ui_port_event(Jalv* jalv, - uint32_t port_index, - uint32_t buffer_size, - uint32_t protocol, - const void* buffer) -{ - if (jalv->ui_instance) { - suil_instance_port_event( - jalv->ui_instance, port_index, buffer_size, protocol, buffer); - } else { - auto* const control = static_cast<Control*>(jalv->ports[port_index].widget); - if (control) { - control->setValue(*static_cast<const float*>(buffer)); - } + auto* const parentLayout = qobject_cast<QLayout*>(parent); + if (parentLayout) { + return parentLayout->spacing(); } + + return -1; } class Timer : public QTimer @@ -321,7 +288,7 @@ private: Jalv* _jalv; }; -static int +int add_preset_to_menu(Jalv* jalv, const LilvNode* node, const LilvNode* title, @@ -336,15 +303,18 @@ add_preset_to_menu(Jalv* jalv, return 0; } +} // namespace + Control::Control(PortContainer portContainer, QWidget* parent) : QGroupBox(parent) - , dial(new QDial()) - , plugin(portContainer.jalv->plugin) - , port(portContainer.port) - , label(new QLabel()) + , _dial(new QDial()) + , _jalv(portContainer.jalv) + , _port(portContainer.port) + , _label(new QLabel()) { - JalvNodes* nodes = &portContainer.jalv->nodes; - const LilvPort* lilvPort = port->lilv_port; + const JalvNodes* nodes = &portContainer.jalv->nodes; + const LilvPlugin* plugin = portContainer.jalv->plugin; + const LilvPort* lilvPort = _port->lilv_port; LilvNode* nmin = nullptr; LilvNode* nmax = nullptr; @@ -354,9 +324,9 @@ Control::Control(PortContainer portContainer, QWidget* parent) LilvNode* stepsNode = lilv_port_get(plugin, lilvPort, nodes->pprops_rangeSteps); if (lilv_node_is_int(stepsNode)) { - steps = std::max(lilv_node_as_int(stepsNode), 2); + _steps = std::max(lilv_node_as_int(stepsNode), 2); } else { - steps = DIAL_STEPS; + _steps = DIAL_STEPS; } lilv_node_free(stepsNode); @@ -371,52 +341,54 @@ Control::Control(PortContainer portContainer, QWidget* parent) } const float f = lilv_node_as_float(val); - scalePoints.push_back(f); - scaleMap[f] = lilv_node_as_string(lilv_scale_point_get_label(p)); + _scalePoints.push_back(f); + _scaleMap[f] = lilv_node_as_string(lilv_scale_point_get_label(p)); } lilv_scale_points_free(sp); } // Check port properties - isLogarithmic = + _isLogarithmic = lilv_port_has_property(plugin, lilvPort, nodes->pprops_logarithmic); - isInteger = lilv_port_has_property(plugin, lilvPort, nodes->lv2_integer); - isEnum = lilv_port_has_property(plugin, lilvPort, nodes->lv2_enumeration); + _isInteger = lilv_port_has_property(plugin, lilvPort, nodes->lv2_integer); + _isEnum = lilv_port_has_property(plugin, lilvPort, nodes->lv2_enumeration); if (lilv_port_has_property(plugin, lilvPort, nodes->lv2_toggled)) { - isInteger = true; + _isInteger = true; - if (!scaleMap[0]) { - scaleMap[0] = "Off"; + if (!_scaleMap[0]) { + _scaleMap[0] = "Off"; } - if (!scaleMap[1]) { - scaleMap[1] = "On"; + if (!_scaleMap[1]) { + _scaleMap[1] = "On"; } } // Find and set min, max and default values for port - const float defaultValue = ndef ? lilv_node_as_float(ndef) : port->control; + const float defaultValue = + ndef ? lilv_node_as_float(ndef) + : portContainer.jalv->process.controls_buf[_port->index]; setRange(lilv_node_as_float(nmin), lilv_node_as_float(nmax)); setValue(defaultValue); // Fill layout auto* const layout = new QVBoxLayout(); - layout->addWidget(label, 0, Qt::AlignHCenter); - layout->addWidget(dial, 0, Qt::AlignHCenter); + layout->addWidget(_label, 0, Qt::AlignHCenter); + layout->addWidget(_dial, 0, Qt::AlignHCenter); setLayout(layout); setMinimumWidth(CONTROL_WIDTH); setMaximumWidth(CONTROL_WIDTH); LilvNode* nname = lilv_port_get_name(plugin, lilvPort); - name = QString("%1").arg(lilv_node_as_string(nname)); + _name = QString("%1").arg(lilv_node_as_string(nname)); // Handle long names - if (stringWidth(name) > CONTROL_WIDTH) { - setTitle(fontMetrics().elidedText(name, Qt::ElideRight, CONTROL_WIDTH)); + if (stringWidth(_name) > CONTROL_WIDTH) { + setTitle(fontMetrics().elidedText(_name, Qt::ElideRight, CONTROL_WIDTH)); } else { - setTitle(name); + setTitle(_name); } // Set tooltip if comment is available @@ -429,7 +401,7 @@ Control::Control(PortContainer portContainer, QWidget* parent) setFlat(true); - connect(dial, SIGNAL(valueChanged(int)), this, SLOT(dialChanged(int))); + connect(_dial, SIGNAL(valueChanged(int)), this, SLOT(dialChanged(int))); lilv_node_free(nmin); lilv_node_free(nmax); @@ -443,31 +415,31 @@ Control::setValue(float value) { float step = 0.0f; - if (isInteger) { + if (_isInteger) { step = value; - } else if (isEnum) { - step = (std::find(scalePoints.begin(), scalePoints.end(), value) - - scalePoints.begin()); - } else if (isLogarithmic) { - step = steps * logf(value / min) / logf(max / min); + } else if (_isEnum) { + step = (std::find(_scalePoints.begin(), _scalePoints.end(), value) - + _scalePoints.begin()); + } else if (_isLogarithmic) { + step = _steps * logf(value / _min) / logf(_max / _min); } else { - step = value * steps; + step = value * _steps; } - dial->setValue(step); - label->setText(getValueLabel(value)); + _dial->setValue(step); + _label->setText(getValueLabel(value)); } QString Control::getValueLabel(float value) { - if (scaleMap[value]) { - if (stringWidth(scaleMap[value]) > CONTROL_WIDTH) { - label->setToolTip(scaleMap[value]); + if (_scaleMap[value]) { + if (stringWidth(_scaleMap[value]) > CONTROL_WIDTH) { + _label->setToolTip(_scaleMap[value]); return fontMetrics().elidedText( - QString(scaleMap[value]), Qt::ElideRight, CONTROL_WIDTH); + QString(_scaleMap[value]), Qt::ElideRight, CONTROL_WIDTH); } - return scaleMap[value]; + return _scaleMap[value]; } return QString("%1").arg(value); @@ -476,40 +448,40 @@ Control::getValueLabel(float value) void Control::setRange(float minRange, float maxRange) { - min = minRange; - max = maxRange; + _min = minRange; + _max = maxRange; - if (isLogarithmic) { + if (_isLogarithmic) { minRange = 1; - maxRange = steps; - } else if (isEnum) { + maxRange = _steps; + } else if (_isEnum) { minRange = 0; - maxRange = scalePoints.size() - 1; - } else if (!isInteger) { - minRange *= steps; - maxRange *= steps; + maxRange = _scalePoints.size() - 1; + } else if (!_isInteger) { + minRange *= _steps; + maxRange *= _steps; } - dial->setRange(minRange, maxRange); + _dial->setRange(minRange, maxRange); } float Control::getValue() { - if (isEnum) { - return scalePoints[dial->value()]; + if (_isEnum) { + return _scalePoints[_dial->value()]; } - if (isInteger) { - return dial->value(); + if (_isInteger) { + return _dial->value(); } - if (isLogarithmic) { - return min * - powf(max / min, static_cast<float>(dial->value()) / (steps - 1)); + if (_isLogarithmic) { + return _min * + powf(_max / _min, static_cast<float>(_dial->value()) / (_steps - 1)); } - return static_cast<float>(dial->value()) / steps; + return static_cast<float>(_dial->value()) / _steps; } int @@ -527,14 +499,16 @@ Control::dialChanged(int) { const float value = getValue(); - label->setText(getValueLabel(value)); - port->control = value; + _label->setText(getValueLabel(value)); + jalv_write_control(_jalv->process.ui_to_plugin, _port->index, value); } -static bool +namespace { + +bool portGroupLessThan(const PortContainer& p1, const PortContainer& p2) { - Jalv* jalv = p1.jalv; + const Jalv* jalv = p1.jalv; const LilvPort* port1 = p1.port->lilv_port; const LilvPort* port2 = p2.port->lilv_port; @@ -543,7 +517,8 @@ portGroupLessThan(const PortContainer& p1, const PortContainer& p2) const int cmp = (group1 && group2) ? strcmp(lilv_node_as_string(group1), lilv_node_as_string(group2)) - : (intptr_t(group1) - intptr_t(group2)); + : (reinterpret_cast<intptr_t>(group1) - + reinterpret_cast<intptr_t>(group2)); lilv_node_free(group2); lilv_node_free(group1); @@ -551,7 +526,7 @@ portGroupLessThan(const PortContainer& p1, const PortContainer& p2) return cmp < 0; } -static QWidget* +QWidget* build_control_widget(Jalv* jalv) { const LilvPlugin* plugin = jalv->plugin; @@ -579,8 +554,8 @@ build_control_widget(Jalv* jalv) LilvNode* lastGroup = nullptr; QHBoxLayout* groupLayout = nullptr; for (int i = 0; i < portContainers.count(); ++i) { - const PortContainer portContainer = portContainers[i]; - Port* const port = portContainer.port; + const PortContainer portContainer = portContainers[i]; + const JalvPort* const port = portContainer.port; auto* const control = new Control(portContainer, nullptr); LilvNode* group = @@ -588,7 +563,7 @@ build_control_widget(Jalv* jalv) if (group) { if (!groupLayout || !lilv_node_equals(group, lastGroup)) { // Group has changed - LilvNode* groupName = + const LilvNode* groupName = lilv_world_get(world, group, jalv->nodes.lv2_name, nullptr); if (!groupName) { groupName = @@ -619,20 +594,62 @@ build_control_widget(Jalv* jalv) return grid; } +} // namespace + +extern "C" { + +int +jalv_frontend_init(JalvFrontendArgs* const args, JalvOptions*) +{ + app = new QApplication(*args->argc, *args->argv, true); + app->setStyleSheet("QGroupBox::title { subcontrol-position: top center }"); + --*args->argc; + ++*args->argv; + return 0; +} + +const char* +jalv_frontend_ui_type(void) +{ +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + return "http://lv2plug.in/ns/extensions/ui#Qt5UI"; +#else + return "http://lv2plug.in/ns/extensions/ui#Qt6UI"; +#endif +} + +void +jalv_frontend_port_event(Jalv* jalv, + uint32_t port_index, + uint32_t buffer_size, + uint32_t protocol, + const void* buffer) +{ + if (jalv->ui_instance) { + suil_instance_port_event( + jalv->ui_instance, port_index, buffer_size, protocol, buffer); + } else { + auto* const control = static_cast<Control*>(jalv->ports[port_index].widget); + if (control) { + control->setValue(*static_cast<const float*>(buffer)); + } + } +} + bool -jalv_frontend_discover(Jalv*) +jalv_frontend_discover(const Jalv*) { return true; } float -jalv_frontend_refresh_rate(Jalv*) +jalv_frontend_refresh_rate(const Jalv*) { return static_cast<float>(QGuiApplication::primaryScreen()->refreshRate()); } float -jalv_frontend_scale_factor(Jalv*) +jalv_frontend_scale_factor(const Jalv*) { return static_cast<float>( QGuiApplication::primaryScreen()->devicePixelRatio()); @@ -667,13 +684,13 @@ jalv_frontend_open(Jalv* jalv) if (jalv->ui_instance) { widget = static_cast<QWidget*>(suil_instance_get_widget(jalv->ui_instance)); } else { - QWidget* controlWidget = build_control_widget(jalv); - - widget = new QScrollArea(); - static_cast<QScrollArea*>(widget)->setWidget(controlWidget); - static_cast<QScrollArea*>(widget)->setWidgetResizable(true); - widget->setMinimumWidth(800); - widget->setMinimumHeight(600); + auto* const controlWidget = build_control_widget(jalv); + auto* const scrollArea = new QScrollArea(); + scrollArea->setWidget(controlWidget); + scrollArea->setWidgetResizable(true); + scrollArea->setMinimumWidth(800); + scrollArea->setMinimumHeight(600); + widget = scrollArea; } LilvNode* name = lilv_plugin_get_name(jalv->plugin); @@ -686,7 +703,7 @@ jalv_frontend_open(Jalv* jalv) jalv_init_ui(jalv); win->show(); - if (jalv->ui_instance && !jalv_ui_is_resizable(jalv)) { + if (jalv->ui_instance && !jalv_ui_is_resizable(jalv->world, jalv->ui)) { widget->setMinimumSize(widget->width(), widget->height()); widget->setMaximumSize(widget->width(), widget->height()); win->adjustSize(); @@ -696,7 +713,7 @@ jalv_frontend_open(Jalv* jalv) } auto* const timer = new Timer(jalv); - timer->start(1000 / jalv->ui_update_hz); + timer->start((int)(1000.0f / jalv->settings.ui_update_hz)); const int ret = app->exec(); zix_sem_post(&jalv->done); diff --git a/src/jalv_qt.hpp b/src/jalv_qt.hpp index d371098..40f9af7 100644 --- a/src/jalv_qt.hpp +++ b/src/jalv_qt.hpp @@ -1,26 +1,26 @@ // Copyright 2007-2022 David Robillard <d@drobilla.net> // SPDX-License-Identifier: ISC +#include "port.h" #include "state.h" #include "types.h" -#include "lilv/lilv.h" +#include <lilv/lilv.h> #include <QAction> #include <QGroupBox> +#include <QObject> #include <QString> #include <QtCore> #include <map> #include <vector> -struct Port; - class QDial; class QLabel; class QWidget; -class PresetAction : public QAction +class PresetAction final : public QAction { Q_OBJECT // NOLINT @@ -41,11 +41,11 @@ private: }; struct PortContainer { - Jalv* jalv; - Port* port; + Jalv* jalv; + JalvPort* port; }; -class Control : public QGroupBox +class Control final : public QGroupBox { Q_OBJECT // NOLINT @@ -62,19 +62,19 @@ private: float getValue(); int stringWidth(const QString& str); - QDial* dial; - const LilvPlugin* plugin; - Port* port; - - QLabel* label; - QString name; - int steps; - float max{1.0f}; - float min{0.0f}; - bool isInteger{}; - bool isEnum{}; - bool isLogarithmic{}; - - std::vector<float> scalePoints; - std::map<float, const char*> scaleMap; + QDial* _dial; + Jalv* _jalv; + JalvPort* _port; + + QLabel* _label; + QString _name; + int _steps; + float _max{1.0f}; + float _min{0.0f}; + bool _isInteger{}; + bool _isEnum{}; + bool _isLogarithmic{}; + + std::vector<float> _scalePoints; + std::map<float, const char*> _scaleMap; }; @@ -4,12 +4,9 @@ #include "log.h" #include "jalv_config.h" -#include "jalv_internal.h" -#include "port.h" -#include "lilv/lilv.h" -#include "lv2/log/log.h" -#include "lv2/urid/urid.h" +#include <lv2/log/log.h> +#include <lv2/urid/urid.h> #if USE_ISATTY # include <unistd.h> @@ -18,42 +15,9 @@ #include <stdarg.h> #include <stdbool.h> #include <stdio.h> -#include <stdlib.h> -#include <string.h> -void -jalv_print_control(Jalv* const jalv, - const struct Port* const port, - const float value) -{ - const LilvNode* sym = lilv_port_get_symbol(jalv->plugin, port->lilv_port); - jalv_log(JALV_LOG_INFO, "%s = %f\n", lilv_node_as_string(sym), value); -} - -char* -jalv_strdup(const char* const str) -{ - const size_t len = strlen(str); - char* copy = (char*)malloc(len + 1); - memcpy(copy, str, len + 1); - return copy; -} - -char* -jalv_strjoin(const char* const a, const char* const b) -{ - const size_t a_len = strlen(a); - const size_t b_len = strlen(b); - char* const out = (char*)malloc(a_len + b_len + 1); - - memcpy(out, a, a_len); - memcpy(out + a_len, b, b_len); - out[a_len + b_len] = '\0'; - - return out; -} - -int +JALV_LOG_FUNC(2, 0) +static int jalv_vlog(const JalvLogLevel level, const char* const fmt, va_list ap) { bool fancy = false; @@ -132,6 +96,9 @@ jalv_ansi_start(FILE* stream, int color) if (isatty(fileno(stream))) { return fprintf(stream, "\033[0;%dm", color); } +#else + (void)stream; + (void)color; #endif return 0; } @@ -144,5 +111,7 @@ jalv_ansi_reset(FILE* stream) fprintf(stream, "\033[0m"); fflush(stream); } +#else + (void)stream; #endif } @@ -5,12 +5,12 @@ #define JALV_LOG_H #include "attributes.h" -#include "types.h" #include "urids.h" -#include "lv2/log/log.h" -#include "lv2/urid/urid.h" +#include <lv2/log/log.h> +#include <lv2/urid/urid.h> +#include <stdarg.h> #include <stdbool.h> #include <stdio.h> @@ -20,11 +20,8 @@ # define JALV_LOG_FUNC(fmt, arg1) #endif -JALV_BEGIN_DECLS - -struct Port; - // String and log utilities +JALV_BEGIN_DECLS typedef enum { JALV_LOG_ERR = 3, @@ -38,34 +35,26 @@ typedef struct { bool tracing; } JalvLog; -void -jalv_print_control(Jalv* jalv, const struct Port* port, float value); - -char* -jalv_strdup(const char* str); - -char* -jalv_strjoin(const char* a, const char* b); - -JALV_LOG_FUNC(2, 0) -int -jalv_vlog(JalvLogLevel level, const char* fmt, va_list ap); - +/// Print a log message to stderr with a GCC-like prefix and color JALV_LOG_FUNC(2, 3) int jalv_log(JalvLogLevel level, const char* fmt, ...); +/// LV2 log vprintf function JALV_LOG_FUNC(3, 0) int jalv_vprintf(LV2_Log_Handle handle, LV2_URID type, const char* fmt, va_list ap); +/// LV2 log printf function JALV_LOG_FUNC(3, 4) int jalv_printf(LV2_Log_Handle handle, LV2_URID type, const char* fmt, ...); +/// Write an ANSI escape sequence to set the foreground color bool jalv_ansi_start(FILE* stream, int color); +/// Write an ANSI escape sequence to reset the foreground color void jalv_ansi_reset(FILE* stream); diff --git a/src/lv2_evbuf.c b/src/lv2_evbuf.c index f19aac2..cdb5dc2 100644 --- a/src/lv2_evbuf.c +++ b/src/lv2_evbuf.c @@ -4,8 +4,8 @@ #include "lv2_evbuf.h" #include "jalv_config.h" -#include "lv2/atom/atom.h" -#include "lv2/atom/util.h" +#include <lv2/atom/atom.h> +#include <lv2/atom/util.h> #include <assert.h> #include <stdlib.h> @@ -50,7 +50,9 @@ lv2_evbuf_new(uint32_t capacity, uint32_t atom_Chunk, uint32_t atom_Sequence) void lv2_evbuf_free(LV2_Evbuf* evbuf) { - free(evbuf); + if (evbuf) { + free(evbuf); + } } void @@ -110,9 +112,8 @@ lv2_evbuf_next(const LV2_Evbuf_Iterator iter) } LV2_Atom_Sequence* aseq = &iter.evbuf->buf; - LV2_Atom_Event* aev = - (LV2_Atom_Event*)((char*)LV2_ATOM_CONTENTS(LV2_Atom_Sequence, aseq) + - iter.offset); + const char* abuf = (const char*)LV2_ATOM_CONTENTS(LV2_Atom_Sequence, aseq); + const LV2_Atom_Event* aev = (const LV2_Atom_Event*)(abuf + iter.offset); const uint32_t offset = iter.offset + lv2_atom_pad_size(sizeof(LV2_Atom_Event) + aev->body.size); diff --git a/src/macros.h b/src/macros.h new file mode 100644 index 0000000..509a75c --- /dev/null +++ b/src/macros.h @@ -0,0 +1,19 @@ +// Copyright 2012-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#ifndef JALV_MACROS_H +#define JALV_MACROS_H + +#ifndef MIN +# define MIN(a, b) (((a) < (b)) ? (a) : (b)) +#endif + +#ifndef MAX +# define MAX(a, b) (((a) > (b)) ? (a) : (b)) +#endif + +#ifndef ARRAY_SIZE +# define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) +#endif + +#endif // JALV_MACROS_H diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..5c49b10 --- /dev/null +++ b/src/main.c @@ -0,0 +1,77 @@ +// Copyright 2007-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include "backend.h" +#include "frontend.h" +#include "jalv.h" +#include "jalv_config.h" +#include "types.h" + +#include <zix/attributes.h> +#include <zix/sem.h> + +#if USE_SUIL +#endif + +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static ZixSem* exit_sem = NULL; ///< Exit semaphore used by signal handler + +static void +signal_handler(int ZIX_UNUSED(sig)) +{ + zix_sem_post(exit_sem); +} + +static void +setup_signals(Jalv* const jalv) +{ + exit_sem = &jalv->done; + +#if !defined(_WIN32) && USE_SIGACTION + struct sigaction action; + sigemptyset(&action.sa_mask); + action.sa_flags = 0; + action.sa_handler = signal_handler; + sigaction(SIGINT, &action, NULL); + sigaction(SIGTERM, &action, NULL); +#else + // May not work in combination with fgets in the console interface + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); +#endif +} + +int +main(int argc, char** argv) +{ + Jalv jalv; + memset(&jalv, '\0', sizeof(Jalv)); + jalv.backend = jalv_backend_allocate(); + + // Initialize application + const int orc = jalv_open(&jalv, &argc, &argv); + if (orc) { + jalv_close(&jalv); + return orc == JALV_EARLY_EXIT_STATUS ? EXIT_SUCCESS : EXIT_FAILURE; + } + + // Set up signal handlers and activate audio processing + setup_signals(&jalv); + jalv_activate(&jalv); + + // Run UI (or prompt at console) + jalv_frontend_open(&jalv); + + // Wait for finish signal from UI or signal handler + zix_sem_wait(&jalv.done); + + // Deactivate audio processing and tear down application + jalv_deactivate(&jalv); + const int crc = jalv_close(&jalv); + jalv_backend_free(jalv.backend); + return crc; +} diff --git a/src/mapper.c b/src/mapper.c new file mode 100644 index 0000000..3e5c2af --- /dev/null +++ b/src/mapper.c @@ -0,0 +1,92 @@ +// Copyright 2012-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include "mapper.h" + +#include "symap.h" + +#include <lv2/urid/urid.h> +#include <zix/sem.h> + +#include <stdint.h> +#include <stdlib.h> + +struct JalvMapperImpl { + Symap* symap; + ZixSem lock; + LV2_URID_Map map; + LV2_URID_Unmap unmap; +}; + +static LV2_URID +map_uri(LV2_URID_Map_Handle handle, const char* const uri) +{ + JalvMapper* const mapper = (JalvMapper*)handle; + zix_sem_wait(&mapper->lock); + + const LV2_URID id = symap_map(mapper->symap, uri); + + zix_sem_post(&mapper->lock); + return id; +} + +static const char* +unmap_uri(LV2_URID_Unmap_Handle handle, const LV2_URID urid) +{ + JalvMapper* const mapper = (JalvMapper*)handle; + zix_sem_wait(&mapper->lock); + + const char* const uri = symap_unmap(mapper->symap, urid); + + zix_sem_post(&mapper->lock); + return uri; +} + +JalvMapper* +jalv_mapper_new(void) +{ + JalvMapper* const mapper = (JalvMapper*)calloc(1, sizeof(JalvMapper)); + if (mapper) { + mapper->symap = symap_new(); + mapper->map.handle = mapper; + mapper->map.map = map_uri; + mapper->unmap.handle = mapper; + mapper->unmap.unmap = unmap_uri; + zix_sem_init(&mapper->lock, 1); + } + return mapper; +} + +void +jalv_mapper_free(JalvMapper* const mapper) +{ + if (mapper) { + zix_sem_destroy(&mapper->lock); + symap_free(mapper->symap); + free(mapper); + } +} + +LV2_URID_Map* +jalv_mapper_urid_map(JalvMapper* const mapper) +{ + return mapper ? &mapper->map : NULL; +} + +LV2_URID_Unmap* +jalv_mapper_urid_unmap(JalvMapper* const mapper) +{ + return mapper ? &mapper->unmap : NULL; +} + +LV2_URID +jalv_mapper_map_uri(JalvMapper* const mapper, const char* const sym) +{ + return symap_map(mapper->symap, sym); +} + +const char* +jalv_mapper_unmap_uri(const JalvMapper* mapper, uint32_t id) +{ + return mapper ? symap_unmap(mapper->symap, id) : NULL; +} diff --git a/src/mapper.h b/src/mapper.h new file mode 100644 index 0000000..ac4c53e --- /dev/null +++ b/src/mapper.h @@ -0,0 +1,44 @@ +// Copyright 2012-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#ifndef JALV_MAPPER_H +#define JALV_MAPPER_H + +#include "attributes.h" + +#include <lv2/urid/urid.h> +#include <zix/attributes.h> + +// URI to URID mapping and unmapping +JALV_BEGIN_DECLS + +/// Opaque URI mapper implementation +typedef struct JalvMapperImpl JalvMapper; + +/// Allocate, configure, and return a new URI mapper +JalvMapper* +jalv_mapper_new(void); + +/// Free memory allocated by jalv_mapper_new() +void +jalv_mapper_free(JalvMapper* mapper); + +/// Return a pointer to the mapper's LV2 URID map +LV2_URID_Map* +jalv_mapper_urid_map(JalvMapper* mapper); + +/// Return a pointer to the mapper's LV2 URID unmap +LV2_URID_Unmap* +jalv_mapper_urid_unmap(JalvMapper* mapper); + +/// Map a URI string to a URID +LV2_URID +jalv_mapper_map_uri(JalvMapper* mapper, const char* sym); + +/// Unmap a URID back to a URI string if possible, or return NULL +ZIX_PURE_FUNC const char* +jalv_mapper_unmap_uri(const JalvMapper* mapper, LV2_URID id); + +JALV_END_DECLS + +#endif // JALV_MAPPER_H diff --git a/src/nodes.c b/src/nodes.c new file mode 100644 index 0000000..d448657 --- /dev/null +++ b/src/nodes.c @@ -0,0 +1,76 @@ +// Copyright 2022-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include "nodes.h" + +#include <lilv/lilv.h> +#include <lv2/atom/atom.h> +#include <lv2/core/lv2.h> +#include <lv2/midi/midi.h> +#include <lv2/port-groups/port-groups.h> +#include <lv2/port-props/port-props.h> +#include <lv2/presets/presets.h> +#include <lv2/resize-port/resize-port.h> +#include <lv2/state/state.h> +#include <lv2/ui/ui.h> +#include <lv2/worker/worker.h> + +#include <stddef.h> + +void +jalv_init_nodes(LilvWorld* const world, JalvNodes* const nodes) +{ +#define MAP_NODE(uri) lilv_new_uri(world, (uri)) + + nodes->atom_AtomPort = MAP_NODE(LV2_ATOM__AtomPort); + nodes->atom_Chunk = MAP_NODE(LV2_ATOM__Chunk); + nodes->atom_Float = MAP_NODE(LV2_ATOM__Float); + nodes->atom_Path = MAP_NODE(LV2_ATOM__Path); + nodes->atom_Sequence = MAP_NODE(LV2_ATOM__Sequence); + nodes->lv2_AudioPort = MAP_NODE(LV2_CORE__AudioPort); + nodes->lv2_CVPort = MAP_NODE(LV2_CORE__CVPort); + nodes->lv2_ControlPort = MAP_NODE(LV2_CORE__ControlPort); + nodes->lv2_InputPort = MAP_NODE(LV2_CORE__InputPort); + nodes->lv2_OutputPort = MAP_NODE(LV2_CORE__OutputPort); + nodes->lv2_connectionOptional = MAP_NODE(LV2_CORE__connectionOptional); + nodes->lv2_control = MAP_NODE(LV2_CORE__control); + nodes->lv2_default = MAP_NODE(LV2_CORE__default); + nodes->lv2_designation = MAP_NODE(LV2_CORE__designation); + nodes->lv2_enumeration = MAP_NODE(LV2_CORE__enumeration); + nodes->lv2_extensionData = MAP_NODE(LV2_CORE__extensionData); + nodes->lv2_integer = MAP_NODE(LV2_CORE__integer); + nodes->lv2_latency = MAP_NODE(LV2_CORE__latency); + nodes->lv2_maximum = MAP_NODE(LV2_CORE__maximum); + nodes->lv2_minimum = MAP_NODE(LV2_CORE__minimum); + nodes->lv2_name = MAP_NODE(LV2_CORE__name); + nodes->lv2_reportsLatency = MAP_NODE(LV2_CORE__reportsLatency); + nodes->lv2_sampleRate = MAP_NODE(LV2_CORE__sampleRate); + nodes->lv2_symbol = MAP_NODE(LV2_CORE__symbol); + nodes->lv2_toggled = MAP_NODE(LV2_CORE__toggled); + nodes->midi_MidiEvent = MAP_NODE(LV2_MIDI__MidiEvent); + nodes->pg_group = MAP_NODE(LV2_PORT_GROUPS__group); + nodes->pprops_logarithmic = MAP_NODE(LV2_PORT_PROPS__logarithmic); + nodes->pprops_notOnGUI = MAP_NODE(LV2_PORT_PROPS__notOnGUI); + nodes->pprops_rangeSteps = MAP_NODE(LV2_PORT_PROPS__rangeSteps); + nodes->pset_Preset = MAP_NODE(LV2_PRESETS__Preset); + nodes->pset_bank = MAP_NODE(LV2_PRESETS__bank); + nodes->rdfs_comment = MAP_NODE(LILV_NS_RDFS "comment"); + nodes->rdfs_label = MAP_NODE(LILV_NS_RDFS "label"); + nodes->rdfs_range = MAP_NODE(LILV_NS_RDFS "range"); + nodes->rsz_minimumSize = MAP_NODE(LV2_RESIZE_PORT__minimumSize); + nodes->state_threadSafeRestore = MAP_NODE(LV2_STATE__threadSafeRestore); + nodes->ui_showInterface = MAP_NODE(LV2_UI__showInterface); + nodes->work_interface = MAP_NODE(LV2_WORKER__interface); + nodes->work_schedule = MAP_NODE(LV2_WORKER__schedule); + nodes->end = NULL; + +#undef MAP_NODE +} + +void +jalv_free_nodes(JalvNodes* const nodes) +{ + for (LilvNode** n = (LilvNode**)nodes; *n; ++n) { + lilv_node_free(*n); + } +} diff --git a/src/nodes.h b/src/nodes.h index ff9d4da..8e93426 100644 --- a/src/nodes.h +++ b/src/nodes.h @@ -6,8 +6,9 @@ #include "attributes.h" -#include "lilv/lilv.h" +#include <lilv/lilv.h> +// Cached lilv nodes JALV_BEGIN_DECLS typedef struct { @@ -24,9 +25,11 @@ typedef struct { LilvNode* lv2_connectionOptional; LilvNode* lv2_control; LilvNode* lv2_default; + LilvNode* lv2_designation; LilvNode* lv2_enumeration; LilvNode* lv2_extensionData; LilvNode* lv2_integer; + LilvNode* lv2_latency; LilvNode* lv2_maximum; LilvNode* lv2_minimum; LilvNode* lv2_name; @@ -45,12 +48,19 @@ typedef struct { LilvNode* rdfs_label; LilvNode* rdfs_range; LilvNode* rsz_minimumSize; + LilvNode* state_threadSafeRestore; LilvNode* ui_showInterface; LilvNode* work_interface; LilvNode* work_schedule; LilvNode* end; ///< NULL terminator for easy freeing of entire structure } JalvNodes; +void +jalv_init_nodes(LilvWorld* world, JalvNodes* nodes); + +void +jalv_free_nodes(JalvNodes* nodes); + JALV_END_DECLS #endif // JALV_NODES_H diff --git a/src/options.h b/src/options.h index 91fbb77..1373e95 100644 --- a/src/options.h +++ b/src/options.h @@ -8,6 +8,7 @@ #include <stdint.h> +// Program options JALV_BEGIN_DECLS typedef struct { @@ -16,7 +17,7 @@ typedef struct { char* load; ///< Path for state to load char* preset; ///< URI of preset to load char** controls; ///< Control values - uint32_t buffer_size; ///< Plugin <=> UI communication buffer size + uint32_t ring_size; ///< Plugin <=> UI communication buffer size double update_rate; ///< UI update rate in Hz double scale_factor; ///< UI scale factor int dump; ///< Dump communication iff true @@ -5,30 +5,22 @@ #define JALV_PORT_H #include "attributes.h" -#include "lv2_evbuf.h" +#include "types.h" -#include "lilv/lilv.h" +#include <lilv/lilv.h> -#include <stddef.h> #include <stdint.h> +// Application port state JALV_BEGIN_DECLS -enum PortFlow { FLOW_UNKNOWN, FLOW_INPUT, FLOW_OUTPUT }; - -enum PortType { TYPE_UNKNOWN, TYPE_CONTROL, TYPE_AUDIO, TYPE_EVENT, TYPE_CV }; - -struct Port { +typedef struct { const LilvPort* lilv_port; ///< LV2 port - enum PortType type; ///< Data type - enum PortFlow flow; ///< Data flow direction - void* sys_port; ///< For audio/MIDI ports, otherwise NULL - LV2_Evbuf* evbuf; ///< For MIDI ports, otherwise NULL + PortType type; ///< Data type + PortFlow flow; ///< Data flow direction void* widget; ///< Control widget, if applicable - size_t buf_size; ///< Custom buffer size, or 0 uint32_t index; ///< Port index - float control; ///< For control ports, otherwise 0.0f -}; +} JalvPort; JALV_END_DECLS diff --git a/src/portaudio.c b/src/portaudio.c index f9283dd..8ce1945 100644 --- a/src/portaudio.c +++ b/src/portaudio.c @@ -1,102 +1,138 @@ -// Copyright 2007-2022 David Robillard <d@drobilla.net> +// Copyright 2007-2024 David Robillard <d@drobilla.net> // SPDX-License-Identifier: ISC #include "backend.h" +#include "comm.h" +#include "log.h" +#include "lv2_evbuf.h" +#include "process.h" +#include "settings.h" +#include "types.h" +#include "urids.h" + +#include <lilv/lilv.h> +#include <zix/attributes.h> +#include <zix/sem.h> -#include "jalv_internal.h" -#include "port.h" -#include "worker.h" - -#include <math.h> #include <portaudio.h> +#include <stdbool.h> +#include <stdint.h> #include <stdio.h> #include <stdlib.h> +#include <string.h> struct JalvBackendImpl { PaStream* stream; }; static int -pa_process_cb(const void* inputs, - void* outputs, - unsigned long nframes, - const PaStreamCallbackTimeInfo* time, - PaStreamCallbackFlags flags, - void* handle) +process_silent(JalvProcess* const proc, + void* const outputs, + const unsigned long nframes) +{ + for (uint32_t i = 0; i < proc->num_ports; ++i) { + memset(((float**)outputs)[i], '\0', nframes * sizeof(float)); + } + + return jalv_bypass(proc, nframes); +} + +static int +process_cb(const void* inputs, + void* outputs, + unsigned long nframes, + const PaStreamCallbackTimeInfo* time, + PaStreamCallbackFlags flags, + void* handle) { - Jalv* jalv = (Jalv*)handle; + (void)time; + (void)flags; + + JalvProcess* const proc = (JalvProcess*)handle; + + // If execution is paused, emit silence and return + if (proc->run_state == JALV_PAUSED) { + return process_silent(proc, outputs, nframes); + } // Prepare port buffers uint32_t in_index = 0; uint32_t out_index = 0; - for (uint32_t i = 0; i < jalv->num_ports; ++i) { - struct Port* port = &jalv->ports[i]; + for (uint32_t i = 0; i < proc->num_ports; ++i) { + JalvProcessPort* const port = &proc->ports[i]; if (port->type == TYPE_AUDIO) { if (port->flow == FLOW_INPUT) { lilv_instance_connect_port( - jalv->instance, i, ((float**)inputs)[in_index++]); + proc->instance, i, ((float**)inputs)[in_index++]); } else if (port->flow == FLOW_OUTPUT) { lilv_instance_connect_port( - jalv->instance, i, ((float**)outputs)[out_index++]); - } - } else if (port->type == TYPE_EVENT && port->flow == FLOW_INPUT) { - lv2_evbuf_reset(port->evbuf, true); - - if (jalv->request_update) { - // Plugin state has changed, request an update - const LV2_Atom_Object get = { - {sizeof(LV2_Atom_Object_Body), jalv->urids.atom_Object}, - {0, jalv->urids.patch_Get}}; - LV2_Evbuf_Iterator iter = lv2_evbuf_begin(port->evbuf); - lv2_evbuf_write( - &iter, 0, 0, get.atom.type, get.atom.size, LV2_ATOM_BODY(&get)); + proc->instance, i, ((float**)outputs)[out_index++]); } } else if (port->type == TYPE_EVENT) { - // Clear event output for plugin to write to - lv2_evbuf_reset(port->evbuf, false); + lv2_evbuf_reset(port->evbuf, port->flow == FLOW_INPUT); } } - jalv->request_update = false; // Run plugin for this cycle - const bool send_ui_updates = jalv_run(jalv, nframes); + const bool send_ui_updates = jalv_run(proc, nframes); // Deliver UI events - for (uint32_t p = 0; p < jalv->num_ports; ++p) { - struct Port* const port = &jalv->ports[p]; + for (uint32_t p = 0; p < proc->num_ports; ++p) { + JalvProcessPort* const port = &proc->ports[p]; if (port->flow == FLOW_OUTPUT && port->type == TYPE_EVENT) { for (LV2_Evbuf_Iterator i = lv2_evbuf_begin(port->evbuf); lv2_evbuf_is_valid(i); i = lv2_evbuf_next(i)) { // Get event from LV2 buffer - uint32_t frames, subframes, type, size; - void* body; + uint32_t frames = 0U; + uint32_t subframes = 0U; + uint32_t type = 0U; + uint32_t size = 0U; + void* body = NULL; lv2_evbuf_get(i, &frames, &subframes, &type, &size, &body); - if (jalv->has_ui) { + if (proc->has_ui) { // Forward event to UI - jalv_write_event(jalv, jalv->plugin_to_ui, p, size, type, body); + jalv_write_event(proc->plugin_to_ui, p, size, type, body); } } } else if (send_ui_updates && port->flow == FLOW_OUTPUT && port->type == TYPE_CONTROL) { - jalv_write_control(jalv, jalv->plugin_to_ui, p, port->control); + jalv_write_control(proc->plugin_to_ui, p, proc->controls_buf[p]); } } return paContinue; } -static JalvBackend* -pa_error(const char* msg, PaError err) +static int +setup_error(const char* msg, PaError err) { jalv_log(JALV_LOG_ERR, "%s (%s)\n", msg, Pa_GetErrorText(err)); Pa_Terminate(); - return NULL; + return 1; } JalvBackend* -jalv_backend_init(Jalv* jalv) +jalv_backend_allocate(void) +{ + return (JalvBackend*)calloc(1, sizeof(JalvBackend)); +} + +void +jalv_backend_free(JalvBackend* const backend) +{ + free(backend); +} + +int +jalv_backend_open(JalvBackend* const backend, + const JalvURIDs* const ZIX_UNUSED(urids), + JalvSettings* const settings, + JalvProcess* const proc, + ZixSem* const ZIX_UNUSED(done), + const char* const ZIX_UNUSED(name), + const bool ZIX_UNUSED(exact_name)) { PaStreamParameters inputParameters; PaStreamParameters outputParameters; @@ -104,16 +140,18 @@ jalv_backend_init(Jalv* jalv) PaError st = paNoError; if ((st = Pa_Initialize())) { - return pa_error("Failed to initialize audio system", st); + return setup_error("Failed to initialize audio system", st); } // Get default input and output devices inputParameters.device = Pa_GetDefaultInputDevice(); outputParameters.device = Pa_GetDefaultOutputDevice(); if (inputParameters.device == paNoDevice) { - return pa_error("No default input device", paDeviceUnavailable); - } else if (outputParameters.device == paNoDevice) { - return pa_error("No default output device", paDeviceUnavailable); + return setup_error("No default input device", paDeviceUnavailable); + } + + if (outputParameters.device == paNoDevice) { + return setup_error("No default output device", paDeviceUnavailable); } const PaDeviceInfo* in_dev = Pa_GetDeviceInfo(inputParameters.device); @@ -122,11 +160,11 @@ jalv_backend_init(Jalv* jalv) // Count number of input and output audio ports/channels inputParameters.channelCount = 0; outputParameters.channelCount = 0; - for (uint32_t i = 0; i < jalv->num_ports; ++i) { - if (jalv->ports[i].type == TYPE_AUDIO) { - if (jalv->ports[i].flow == FLOW_INPUT) { + for (uint32_t i = 0; i < proc->num_ports; ++i) { + if (proc->ports[i].type == TYPE_AUDIO) { + if (proc->ports[i].flow == FLOW_INPUT) { ++inputParameters.channelCount; - } else if (jalv->ports[i].flow == FLOW_OUTPUT) { + } else if (proc->ports[i].flow == FLOW_OUTPUT) { ++outputParameters.channelCount; } } @@ -148,59 +186,67 @@ jalv_backend_init(Jalv* jalv) in_dev->defaultSampleRate, paFramesPerBufferUnspecified, 0, - pa_process_cb, - jalv))) { - return pa_error("Failed to open audio stream", st); + process_cb, + proc))) { + return setup_error("Failed to open audio stream", st); } // Set audio parameters - jalv->sample_rate = in_dev->defaultSampleRate; - // jalv->block_length = FIXME - jalv->midi_buf_size = 4096; - - // Allocate and return opaque backend - JalvBackend* backend = (JalvBackend*)calloc(1, sizeof(JalvBackend)); - backend->stream = stream; - return backend; + settings->sample_rate = in_dev->defaultSampleRate; + // settings->block_length = FIXME + settings->midi_buf_size = 4096; + + backend->stream = stream; + return 0; } void -jalv_backend_close(Jalv* jalv) +jalv_backend_close(JalvBackend* const backend) { - Pa_Terminate(); - free(jalv->backend); - jalv->backend = NULL; + if (backend) { + PaError st = paNoError; + if (backend->stream && (st = Pa_CloseStream(backend->stream))) { + jalv_log(JALV_LOG_ERR, "Error closing audio (%s)\n", Pa_GetErrorText(st)); + } + + if ((st = Pa_Terminate())) { + jalv_log( + JALV_LOG_ERR, "Error terminating audio (%s)\n", Pa_GetErrorText(st)); + } + } } void -jalv_backend_activate(Jalv* jalv) +jalv_backend_activate(JalvBackend* const backend) { - const int st = Pa_StartStream(jalv->backend->stream); + const PaError st = Pa_StartStream(backend->stream); if (st != paNoError) { - jalv_log( - JALV_LOG_ERR, "Error starting audio stream (%s)\n", Pa_GetErrorText(st)); + jalv_log(JALV_LOG_ERR, "Error starting audio (%s)\n", Pa_GetErrorText(st)); } } void -jalv_backend_deactivate(Jalv* jalv) +jalv_backend_deactivate(JalvBackend* const backend) { - const int st = Pa_CloseStream(jalv->backend->stream); + const PaError st = Pa_StopStream(backend->stream); if (st != paNoError) { - jalv_log( - JALV_LOG_ERR, "Error closing audio stream (%s)\n", Pa_GetErrorText(st)); + jalv_log(JALV_LOG_ERR, "Error stopping audio (%s)\n", Pa_GetErrorText(st)); } } void -jalv_backend_activate_port(Jalv* jalv, uint32_t port_index) +jalv_backend_activate_port(JalvBackend* const ZIX_UNUSED(backend), + JalvProcess* const proc, + const uint32_t port_index) { - struct Port* const port = &jalv->ports[port_index]; - switch (port->type) { - case TYPE_CONTROL: - lilv_instance_connect_port(jalv->instance, port_index, &port->control); - break; - default: - break; + JalvProcessPort* const port = &proc->ports[port_index]; + + if (port->type == TYPE_CONTROL) { + lilv_instance_connect_port( + proc->instance, port_index, &proc->controls_buf[port_index]); } } + +void +jalv_backend_recompute_latencies(JalvBackend* const ZIX_UNUSED(backend)) +{} diff --git a/src/process.c b/src/process.c new file mode 100644 index 0000000..efb2602 --- /dev/null +++ b/src/process.c @@ -0,0 +1,130 @@ +// Copyright 2016-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include "process.h" + +#include "comm.h" +#include "log.h" +#include "lv2_evbuf.h" +#include "types.h" +#include "worker.h" + +#include <lilv/lilv.h> +#include <lv2/atom/atom.h> +#include <lv2/core/lv2.h> +#include <zix/ring.h> +#include <zix/sem.h> + +#include <assert.h> +#include <stddef.h> + +static int +ring_error(const char* const message) +{ + jalv_log(JALV_LOG_ERR, "%s", message); + return 1; +} + +static int +apply_ui_events(JalvProcess* const proc, const uint32_t nframes) +{ + ZixRing* const ring = proc->ui_to_plugin; + JalvMessageHeader header = {NO_MESSAGE, 0U}; + const size_t space = zix_ring_read_space(ring); + for (size_t i = 0; i < space; i += sizeof(header) + header.size) { + // Read message header (which includes the body size) + if (zix_ring_read(ring, &header, sizeof(header)) != sizeof(header)) { + return ring_error("Failed to read header from UI ring\n"); + } + + if (header.type == CONTROL_PORT_CHANGE) { + assert(header.size == sizeof(JalvControlChange)); + JalvControlChange msg = {0U, 0.0f}; + if (zix_ring_read(ring, &msg, sizeof(msg)) != sizeof(msg)) { + return ring_error("Failed to read control value from UI ring\n"); + } + + assert(msg.port_index < proc->num_ports); + proc->controls_buf[msg.port_index] = msg.value; + + } else if (header.type == EVENT_TRANSFER) { + assert(header.size <= proc->process_msg_size); + void* const body = proc->process_msg; + if (zix_ring_read(ring, body, header.size) != header.size) { + return ring_error("Failed to read event from UI ring\n"); + } + + const JalvEventTransfer* const msg = (const JalvEventTransfer*)body; + assert(msg->port_index < proc->num_ports); + JalvProcessPort* const port = &proc->ports[msg->port_index]; + LV2_Evbuf_Iterator e = lv2_evbuf_end(port->evbuf); + const LV2_Atom* const atom = &msg->atom; + lv2_evbuf_write( + &e, nframes, 0U, atom->type, atom->size, LV2_ATOM_BODY_CONST(atom)); + + } else if (header.type == STATE_REQUEST) { + JalvProcessPort* const port = &proc->ports[proc->control_in]; + assert(port->type == TYPE_EVENT); + assert(port->flow == FLOW_INPUT); + assert(port->evbuf); + + LV2_Evbuf_Iterator e = lv2_evbuf_end(port->evbuf); + lv2_evbuf_write(&e, + nframes, + 0U, + proc->get_msg.atom.type, + proc->get_msg.atom.size, + &proc->get_msg.body); + + } else if (header.type == RUN_STATE_CHANGE) { + assert(header.size == sizeof(JalvRunStateChange)); + JalvRunStateChange msg = {JALV_RUNNING}; + if (zix_ring_read(ring, &msg, sizeof(msg)) != sizeof(msg)) { + return ring_error("Failed to read run state change from UI ring\n"); + } + + proc->run_state = msg.state; + if (msg.state == JALV_PAUSED) { + zix_sem_post(&proc->paused); + } + + } else { + return ring_error("Unknown message type received from UI ring\n"); + } + } + + return 0; +} + +bool +jalv_run(JalvProcess* const proc, const uint32_t nframes) +{ + // Read and apply control change events from UI + apply_ui_events(proc, nframes); + + // Run plugin for this cycle + lilv_instance_run(proc->instance, nframes); + + // Process any worker replies and end the cycle + LV2_Handle handle = lilv_instance_get_handle(proc->instance); + jalv_worker_emit_responses(proc->state_worker, handle); + jalv_worker_emit_responses(proc->worker, handle); + jalv_worker_end_run(proc->worker); + + // Check if it's time to send updates to the UI + proc->pending_frames += nframes; + if (proc->update_frames && proc->pending_frames > proc->update_frames) { + proc->pending_frames = 0U; + return true; + } + + return false; +} + +int +jalv_bypass(JalvProcess* const proc, const uint32_t nframes) +{ + // Read and apply control change events from UI + apply_ui_events(proc, nframes); + return 0; +} diff --git a/src/process.h b/src/process.h new file mode 100644 index 0000000..cb6a122 --- /dev/null +++ b/src/process.h @@ -0,0 +1,99 @@ +// Copyright 2016-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#ifndef JALV_PROCESS_H +#define JALV_PROCESS_H + +#include "attributes.h" +#include "lv2_evbuf.h" +#include "types.h" +#include "worker.h" + +#include <lilv/lilv.h> +#include <lv2/atom/atom.h> +#include <lv2/atom/forge.h> +#include <zix/ring.h> +#include <zix/sem.h> + +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> + +// Code and data used in the realtime process thread +JALV_BEGIN_DECLS + +/// Port state used in the process thread +typedef struct { + PortType type; ///< Data type + PortFlow flow; ///< Data flow direction + void* sys_port; ///< For audio/MIDI ports, otherwise NULL + char* symbol; ///< Port symbol (stable/unique C-like identifier) + char* label; ///< Human-readable label + LV2_Evbuf* evbuf; ///< Sequence port event buffer + uint32_t buf_size; ///< Custom buffer size, or 0 + bool reports_latency; ///< Whether control port reports latency + bool is_primary; ///< True for main control/response channel + bool supports_midi; ///< Whether event port supports MIDI +} JalvProcessPort; + +/** + State accessed in the process thread. + + Everything accessed by the process thread is stored here, to keep it + somewhat insulated from the UI and to make references to it stand out in the + code. +*/ +typedef struct { + LilvInstance* instance; ///< Plugin instance + ZixRing* ui_to_plugin; ///< Messages from UI to plugin/process + ZixRing* plugin_to_ui; ///< Messages from plugin/process to UI + JalvWorker* worker; ///< Worker thread implementation + JalvWorker* state_worker; ///< Synchronous worker for state restore + JalvProcessPort* ports; ///< Port array of size num_ports + LV2_Atom_Forge forge; ///< Atom forge + LV2_Atom_Object get_msg; ///< General patch:Get message + float* controls_buf; ///< Control port buffers array + size_t process_msg_size; ///< Maximum size of a single message + void* process_msg; ///< Buffer for receiving messages + ZixSem paused; ///< Paused signal from process thread + JalvRunState run_state; ///< Current run state + uint32_t control_in; ///< Index of control input port + uint32_t num_ports; ///< Total number of ports on the plugin + uint32_t pending_frames; ///< Frames since last UI update sent + uint32_t update_frames; ///< UI update period in frames, or zero + uint32_t plugin_latency; ///< Latency reported by plugin (if any) + uint32_t position; ///< Transport position in frames + float bpm; ///< Transport tempo in beats per minute + bool rolling; ///< Transport speed (0=stop, 1=play) + bool has_ui; ///< True iff a control UI is present +} JalvProcess; + +/** + Run the plugin for a block of frames. + + Applies any pending messages from the UI, runs the plugin instance, and + processes any worker replies. + + @param proc Process thread state. + @param nframes Number of frames to process. + @return Whether output value updates should be sent to the UI now. +*/ +bool +jalv_run(JalvProcess* proc, uint32_t nframes); + +/** + Bypass the plugin for a block of frames. + + This is like jalv_run(), but doesn't actually run the plugin and only does + the minimum necessary internal work for the cycle. + + @param proc Process thread state. + @param nframes Number of frames to bypass. + @return Zero. +*/ +int +jalv_bypass(JalvProcess* proc, uint32_t nframes); + +JALV_END_DECLS + +#endif // JALV_PROCESS_H diff --git a/src/process_setup.c b/src/process_setup.c new file mode 100644 index 0000000..169ddcd --- /dev/null +++ b/src/process_setup.c @@ -0,0 +1,219 @@ +// Copyright 2016-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include "process_setup.h" + +#include "jalv_config.h" +#include "log.h" +#include "lv2_evbuf.h" +#include "macros.h" +#include "mapper.h" +#include "nodes.h" +#include "process.h" +#include "query.h" +#include "settings.h" +#include "string_utils.h" +#include "types.h" +#include "urids.h" +#include "worker.h" + +#include <lilv/lilv.h> +#include <lv2/atom/atom.h> +#include <lv2/atom/forge.h> +#include <zix/allocator.h> +#include <zix/ring.h> +#include <zix/sem.h> + +#include <stdbool.h> +#include <stdint.h> +#include <stdlib.h> + +int +jalv_process_init(JalvProcess* const proc, + const JalvURIDs* const urids, + JalvMapper* const mapper, + const uint32_t update_frames) +{ + proc->get_msg.atom.size = sizeof(LV2_Atom_Object_Body); + proc->get_msg.atom.type = urids->atom_Object; + proc->get_msg.body.id = 0U; + proc->get_msg.body.otype = urids->patch_Get; + + proc->instance = NULL; + proc->ui_to_plugin = NULL; + proc->plugin_to_ui = NULL; + proc->worker = NULL; + proc->state_worker = NULL; + proc->ports = NULL; + proc->process_msg_size = 1024U; + proc->process_msg = NULL; + proc->run_state = JALV_PAUSED; + proc->control_in = UINT32_MAX; + proc->num_ports = 0U; + proc->pending_frames = 0U; + proc->update_frames = update_frames; + proc->position = 0U; + proc->bpm = 120.0f; + proc->rolling = false; + proc->has_ui = false; + + zix_sem_init(&proc->paused, 0); + lv2_atom_forge_init(&proc->forge, jalv_mapper_urid_map(mapper)); + + return 0; +} + +void +jalv_process_cleanup(JalvProcess* const proc) +{ + zix_sem_destroy(&proc->paused); + jalv_worker_free(proc->worker); + jalv_worker_free(proc->state_worker); + zix_ring_free(proc->ui_to_plugin); + zix_ring_free(proc->plugin_to_ui); + zix_aligned_free(NULL, proc->process_msg); + + for (uint32_t i = 0U; i < proc->num_ports; ++i) { + jalv_process_port_cleanup(&proc->ports[i]); + } +} + +void +jalv_process_activate(JalvProcess* const proc, + const JalvURIDs* const urids, + LilvInstance* const instance, + const JalvSettings* const settings) +{ + proc->instance = instance; + + for (uint32_t i = 0U; i < proc->num_ports; ++i) { + JalvProcessPort* const port = &proc->ports[i]; + if (port->type == TYPE_EVENT) { + const size_t size = + port->buf_size ? port->buf_size : settings->midi_buf_size; + + lv2_evbuf_free(port->evbuf); + port->evbuf = + lv2_evbuf_new(size, urids->atom_Chunk, urids->atom_Sequence); + + lv2_evbuf_reset(port->evbuf, port->flow == FLOW_INPUT); + lilv_instance_connect_port( + proc->instance, i, lv2_evbuf_get_buffer(port->evbuf)); + + if (port->flow == FLOW_INPUT) { + proc->process_msg_size = MAX(proc->process_msg_size, port->buf_size); + } + } + } + + // Allocate UI<=>process communication rings and process receive buffer + proc->ui_to_plugin = zix_ring_new(NULL, settings->ring_size); + proc->plugin_to_ui = zix_ring_new(NULL, settings->ring_size); + proc->process_msg = zix_aligned_alloc(NULL, 8U, proc->process_msg_size); + zix_ring_mlock(proc->ui_to_plugin); + zix_ring_mlock(proc->plugin_to_ui); + zix_ring_mlock(proc->process_msg); +} + +void +jalv_process_deactivate(JalvProcess* const proc) +{ + zix_aligned_free(NULL, proc->process_msg); + proc->process_msg = NULL; + + for (uint32_t i = 0U; i < proc->num_ports; ++i) { + lv2_evbuf_free(proc->ports[i].evbuf); + lilv_instance_connect_port(proc->instance, i, NULL); + proc->ports[i].evbuf = NULL; + } +} + +int +jalv_process_port_init(JalvProcessPort* const port, + const JalvNodes* const nodes, + const LilvPlugin* const lilv_plugin, + const LilvPort* const lilv_port) +{ + const LilvNode* const symbol = lilv_port_get_symbol(lilv_plugin, lilv_port); + + port->type = TYPE_UNKNOWN; + port->flow = FLOW_UNKNOWN; + port->sys_port = NULL; + port->evbuf = NULL; + port->buf_size = 0U; + port->reports_latency = false; + + const bool optional = lilv_port_has_property( + lilv_plugin, lilv_port, nodes->lv2_connectionOptional); + + // Set port flow (input or output) + if (lilv_port_is_a(lilv_plugin, lilv_port, nodes->lv2_InputPort)) { + port->flow = FLOW_INPUT; + } else if (lilv_port_is_a(lilv_plugin, lilv_port, nodes->lv2_OutputPort)) { + port->flow = FLOW_OUTPUT; + } else if (!optional) { + jalv_log(JALV_LOG_ERR, + "Mandatory port \"%s\" is neither input nor output\n", + lilv_node_as_string(symbol)); + return 1; + } + + // Set port type + if (lilv_port_is_a(lilv_plugin, lilv_port, nodes->lv2_ControlPort)) { + port->type = TYPE_CONTROL; + } else if (lilv_port_is_a(lilv_plugin, lilv_port, nodes->lv2_AudioPort)) { + port->type = TYPE_AUDIO; +#if USE_JACK_METADATA + } else if (lilv_port_is_a(lilv_plugin, lilv_port, nodes->lv2_CVPort)) { + port->type = TYPE_CV; +#endif + } else if (lilv_port_is_a(lilv_plugin, lilv_port, nodes->atom_AtomPort)) { + port->type = TYPE_EVENT; + } else if (!optional) { + jalv_log(JALV_LOG_ERR, + "Mandatory port \"%s\" has unknown data type\n", + lilv_node_as_string(symbol)); + return 1; + } + + // Set symbol and label + LilvNode* const name = lilv_port_get_name(lilv_plugin, lilv_port); + port->symbol = symbol ? jalv_strdup(lilv_node_as_string(symbol)) : NULL; + port->label = name ? jalv_strdup(lilv_node_as_string(name)) : NULL; + lilv_node_free(name); + + // Set buffer size + LilvNode* const min_size = + lilv_port_get(lilv_plugin, lilv_port, nodes->rsz_minimumSize); + if (min_size && lilv_node_is_int(min_size)) { + port->buf_size = (uint32_t)MAX(lilv_node_as_int(min_size), 0); + } + lilv_node_free(min_size); + + // Set reports_latency flag + if (port->flow == FLOW_OUTPUT && port->type == TYPE_CONTROL && + (lilv_port_has_property( + lilv_plugin, lilv_port, nodes->lv2_reportsLatency) || + jalv_port_has_designation( + nodes, lilv_plugin, lilv_port, nodes->lv2_latency))) { + port->reports_latency = true; + } + + // Set supports_midi flag + port->supports_midi = + lilv_port_supports_event(lilv_plugin, lilv_port, nodes->midi_MidiEvent); + + return 0; +} + +void +jalv_process_port_cleanup(JalvProcessPort* const port) +{ + if (port) { + if (port->evbuf) { + lv2_evbuf_free(port->evbuf); + } + free(port->label); + free(port->symbol); + } +} diff --git a/src/process_setup.h b/src/process_setup.h new file mode 100644 index 0000000..8c99312 --- /dev/null +++ b/src/process_setup.h @@ -0,0 +1,83 @@ +// Copyright 2016-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#ifndef JALV_PROCESS_SETUP_H +#define JALV_PROCESS_SETUP_H + +#include "attributes.h" +#include "mapper.h" +#include "nodes.h" +#include "process.h" +#include "settings.h" +#include "urids.h" + +#include <lilv/lilv.h> + +#include <stdint.h> + +// Code for setting up the realtime process thread (but that isn't used in it) +JALV_BEGIN_DECLS + +/** + Initialize process thread and allocate necessary structures. + + This only initializes the state structure, it doesn't create any threads or + start plugin execution. +*/ +int +jalv_process_init(JalvProcess* proc, + const JalvURIDs* urids, + JalvMapper* mapper, + uint32_t update_frames); + +/** + Clean up process thread. + + This frees everything allocated by jalv_process_init() and + jalv_process_activate(). +*/ +void +jalv_process_cleanup(JalvProcess* proc); + +/** + Allocate necessary buffers, connect the plugin to them, and prepare to run. + + @param proc Process thread state. + @param urids Application vocabulary. + @param instance Plugin instance to run. + @param settings Process thread settings. +*/ +void +jalv_process_activate(JalvProcess* proc, + const JalvURIDs* urids, + LilvInstance* instance, + const JalvSettings* settings); + +/** + Clean up after jalv_process_activate() and disconnect plugin. + + @param proc Process thread state. +*/ +void +jalv_process_deactivate(JalvProcess* proc); + +/** + Initialize the process thread state for a port. + + @return Zero on success. +*/ +int +jalv_process_port_init(JalvProcessPort* port, + const JalvNodes* nodes, + const LilvPlugin* lilv_plugin, + const LilvPort* lilv_port); + +/** + Free memory allocated by jalv_setup_init_port(). +*/ +void +jalv_process_port_cleanup(JalvProcessPort* port); + +JALV_END_DECLS + +#endif // JALV_PROCESS_SETUP_H diff --git a/src/query.c b/src/query.c new file mode 100644 index 0000000..bef2fb5 --- /dev/null +++ b/src/query.c @@ -0,0 +1,56 @@ +// Copyright 2012-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include "query.h" + +#include "nodes.h" + +#include <lilv/lilv.h> +#include <lv2/core/lv2.h> +#include <lv2/ui/ui.h> + +bool +jalv_port_has_designation(const JalvNodes* const nodes, + const LilvPlugin* const plugin, + const LilvPort* const port, + const LilvNode* const designation) +{ + LilvNodes* const designations = + lilv_port_get_value(plugin, port, nodes->lv2_designation); + + bool found = false; + LILV_FOREACH (nodes, n, designations) { + const LilvNode* const node = lilv_nodes_get(designations, n); + if (lilv_node_equals(node, designation)) { + found = true; + break; + } + } + + lilv_nodes_free(designations); + return found; +} + +bool +jalv_ui_is_resizable(LilvWorld* const world, const LilvUI* const ui) +{ + if (!ui) { + return false; + } + + const LilvNode* s = lilv_ui_get_uri(ui); + LilvNode* p = lilv_new_uri(world, LV2_CORE__optionalFeature); + LilvNode* fs = lilv_new_uri(world, LV2_UI__fixedSize); + LilvNode* nrs = lilv_new_uri(world, LV2_UI__noUserResize); + + LilvNodes* fs_matches = lilv_world_find_nodes(world, s, p, fs); + LilvNodes* nrs_matches = lilv_world_find_nodes(world, s, p, nrs); + + lilv_nodes_free(nrs_matches); + lilv_nodes_free(fs_matches); + lilv_node_free(nrs); + lilv_node_free(fs); + lilv_node_free(p); + + return !fs_matches && !nrs_matches; +} diff --git a/src/query.h b/src/query.h new file mode 100644 index 0000000..4a452c6 --- /dev/null +++ b/src/query.h @@ -0,0 +1,30 @@ +// Copyright 2012-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#ifndef JALV_MODEL_H +#define JALV_MODEL_H + +#include "attributes.h" +#include "nodes.h" + +#include <lilv/lilv.h> + +#include <stdbool.h> + +// Lilv query utilities +JALV_BEGIN_DECLS + +/// Return whether a port has a given designation +bool +jalv_port_has_designation(const JalvNodes* nodes, + const LilvPlugin* plugin, + const LilvPort* port, + const LilvNode* designation); + +/// Return whether a UI is described as resizable +bool +jalv_ui_is_resizable(LilvWorld* world, const LilvUI* ui); + +JALV_END_DECLS + +#endif // JALV_MODEL_H diff --git a/src/settings.h b/src/settings.h new file mode 100644 index 0000000..0cb548e --- /dev/null +++ b/src/settings.h @@ -0,0 +1,27 @@ +// Copyright 2018-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#ifndef JALV_SETTINGS_H +#define JALV_SETTINGS_H + +#include "attributes.h" + +#include <stddef.h> +#include <stdint.h> + +// Process thread settings +JALV_BEGIN_DECLS + +/// System and/or configuration settings for the execution process +typedef struct { + float sample_rate; ///< Sample rate + uint32_t block_length; ///< Audio buffer length in frames + size_t midi_buf_size; ///< MIDI buffer size in bytes + uint32_t ring_size; ///< Communication ring size in bytes + float ui_update_hz; ///< Frequency of UI updates + float ui_scale_factor; ///< UI scale factor +} JalvSettings; + +JALV_END_DECLS + +#endif // JALV_SETTINGS_H diff --git a/src/state.c b/src/state.c index 282b907..472244e 100644 --- a/src/state.c +++ b/src/state.c @@ -1,30 +1,39 @@ -// Copyright 2007-2016 David Robillard <d@drobilla.net> +// Copyright 2007-2024 David Robillard <d@drobilla.net> // SPDX-License-Identifier: ISC #include "state.h" -#include "jalv_internal.h" +#include "comm.h" +#include "jalv.h" #include "log.h" +#include "mapper.h" #include "port.h" - -#include "lilv/lilv.h" -#include "lv2/core/lv2.h" -#include "lv2/state/state.h" -#include "zix/attributes.h" -#include "zix/sem.h" +#include "process.h" +#include "types.h" + +#include <lilv/lilv.h> +#include <lv2/core/lv2.h> +#include <lv2/state/state.h> +#include <lv2/urid/urid.h> +#include <zix/attributes.h> +#include <zix/path.h> +#include <zix/ring.h> +#include <zix/sem.h> +#include <zix/status.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> -char* +ZIX_MALLOC_FUNC char* jalv_make_path(LV2_State_Make_Path_Handle handle, const char* path) { Jalv* jalv = (Jalv*)handle; // Create in save directory if saving, otherwise use temp directory - return jalv_strjoin(jalv->save_dir ? jalv->save_dir : jalv->temp_dir, path); + const char* const dir = jalv->save_dir ? jalv->save_dir : jalv->temp_dir; + return zix_path_join(NULL, dir, path); } static const void* @@ -33,12 +42,12 @@ get_port_value(const char* port_symbol, uint32_t* size, uint32_t* type) { - Jalv* jalv = (Jalv*)user_data; - struct Port* port = jalv_port_by_symbol(jalv, port_symbol); + Jalv* const jalv = (Jalv*)user_data; + const JalvPort* const port = jalv_port_by_symbol(jalv, port_symbol); if (port && port->flow == FLOW_INPUT && port->type == TYPE_CONTROL) { *size = sizeof(float); *type = jalv->forge.Float; - return &port->control; + return &jalv->process.controls_buf[port->index]; } *size = *type = 0; return NULL; @@ -47,12 +56,15 @@ get_port_value(const char* port_symbol, void jalv_save(Jalv* jalv, const char* dir) { - jalv->save_dir = jalv_strjoin(dir, "/"); + LV2_URID_Map* const map = jalv_mapper_urid_map(jalv->mapper); + LV2_URID_Unmap* const unmap = jalv_mapper_urid_unmap(jalv->mapper); + + jalv->save_dir = zix_path_join(NULL, dir, NULL); LilvState* const state = lilv_state_new_from_instance(jalv->plugin, - jalv->instance, - &jalv->map, + jalv->process.instance, + map, jalv->temp_dir, dir, dir, @@ -62,11 +74,8 @@ jalv_save(Jalv* jalv, const char* dir) LV2_STATE_IS_POD | LV2_STATE_IS_PORTABLE, NULL); - lilv_state_save( - jalv->world, &jalv->map, &jalv->unmap, state, NULL, dir, "state.ttl"); - + lilv_state_save(jalv->world, map, unmap, state, NULL, dir, "state.ttl"); lilv_state_free(state); - free(jalv->save_dir); jalv->save_dir = NULL; } @@ -121,8 +130,9 @@ set_port_value(const char* port_symbol, uint32_t ZIX_UNUSED(size), uint32_t type) { - Jalv* jalv = (Jalv*)user_data; - struct Port* port = jalv_port_by_symbol(jalv, port_symbol); + Jalv* const jalv = (Jalv*)user_data; + JalvProcess* const proc = &jalv->process; + const JalvPort* const port = jalv_port_by_symbol(jalv, port_symbol); if (!port) { jalv_log(JALV_LOG_ERR, "Preset port `%s' is missing\n", port_symbol); return; @@ -141,51 +151,71 @@ set_port_value(const char* port_symbol, jalv_log(JALV_LOG_ERR, "Preset `%s' value has bad type <%s>\n", port_symbol, - jalv->unmap.unmap(jalv->unmap.handle, type)); + jalv_mapper_unmap_uri(jalv->mapper, type)); return; } - if (jalv->play_state != JALV_RUNNING) { + ZixStatus st = ZIX_STATUS_SUCCESS; + if (proc->run_state != JALV_RUNNING) { // Set value on port struct directly - port->control = fvalue; + proc->controls_buf[port->index] = fvalue; } else { // Send value to plugin (as if from UI) - jalv_write_control(jalv, jalv->ui_to_plugin, port->index, fvalue); + st = jalv_write_control(proc->ui_to_plugin, port->index, fvalue); } - if (jalv->has_ui) { + if (proc->has_ui) { // Update UI (as if from plugin) - jalv_write_control(jalv, jalv->plugin_to_ui, port->index, fvalue); + st = jalv_write_control(proc->plugin_to_ui, port->index, fvalue); + } + + if (st) { + jalv_log( + JALV_LOG_ERR, "Failed to write control change (%s)\n", zix_strerror(st)); } } void -jalv_apply_state(Jalv* jalv, LilvState* state) +jalv_apply_state(Jalv* jalv, const LilvState* state) { - bool must_pause = !jalv->safe_restore && jalv->play_state == JALV_RUNNING; - if (state) { - if (must_pause) { - jalv->play_state = JALV_PAUSE_REQUESTED; - zix_sem_wait(&jalv->paused); - } + JalvProcess* const proc = &jalv->process; - const LV2_Feature* state_features[9] = { - &jalv->features.map_feature, - &jalv->features.unmap_feature, - &jalv->features.make_path_feature, - &jalv->features.state_sched_feature, - &jalv->features.safe_restore_feature, - &jalv->features.log_feature, - &jalv->features.options_feature, - NULL}; - - lilv_state_restore( - state, jalv->instance, set_port_value, jalv, 0, state_features); - - if (must_pause) { - jalv->request_update = true; - jalv->play_state = JALV_RUNNING; - } + typedef struct { + JalvMessageHeader head; + JalvRunStateChange body; + } PauseMessage; + + const bool must_pause = + !jalv->safe_restore && proc->run_state == JALV_RUNNING; + if (must_pause) { + const PauseMessage pause_msg = { + {RUN_STATE_CHANGE, sizeof(JalvRunStateChange)}, {JALV_PAUSED}}; + zix_ring_write(proc->ui_to_plugin, &pause_msg, sizeof(pause_msg)); + + zix_sem_wait(&proc->paused); + } + + const LV2_Feature* state_features[9] = { + &jalv->features.map_feature, + &jalv->features.unmap_feature, + &jalv->features.make_path_feature, + &jalv->features.state_sched_feature, + &jalv->features.safe_restore_feature, + &jalv->features.log_feature, + &jalv->features.options_feature, + NULL, + }; + + lilv_state_restore( + state, proc->instance, set_port_value, jalv, 0, state_features); + + if (must_pause) { + const JalvMessageHeader state_msg = {STATE_REQUEST, 0U}; + zix_ring_write(proc->ui_to_plugin, &state_msg, sizeof(state_msg)); + + const PauseMessage run_msg = { + {RUN_STATE_CHANGE, sizeof(JalvRunStateChange)}, {JALV_RUNNING}}; + zix_ring_write(proc->ui_to_plugin, &run_msg, sizeof(run_msg)); } } @@ -193,8 +223,11 @@ int jalv_apply_preset(Jalv* jalv, const LilvNode* preset) { lilv_state_free(jalv->preset); - jalv->preset = lilv_state_new_from_world(jalv->world, &jalv->map, preset); - jalv_apply_state(jalv, jalv->preset); + jalv->preset = lilv_state_new_from_world( + jalv->world, jalv_mapper_urid_map(jalv->mapper), preset); + if (jalv->preset) { + jalv_apply_state(jalv, jalv->preset); + } return 0; } @@ -205,10 +238,13 @@ jalv_save_preset(Jalv* jalv, const char* label, const char* filename) { + LV2_URID_Map* const map = jalv_mapper_urid_map(jalv->mapper); + LV2_URID_Unmap* const unmap = jalv_mapper_urid_unmap(jalv->mapper); + LilvState* const state = lilv_state_new_from_instance(jalv->plugin, - jalv->instance, - &jalv->map, + jalv->process.instance, + map, jalv->temp_dir, dir, dir, @@ -222,8 +258,7 @@ jalv_save_preset(Jalv* jalv, lilv_state_set_label(state, label); } - int ret = lilv_state_save( - jalv->world, &jalv->map, &jalv->unmap, state, uri, dir, filename); + int ret = lilv_state_save(jalv->world, map, unmap, state, uri, dir, filename); lilv_state_free(jalv->preset); jalv->preset = state; diff --git a/src/state.h b/src/state.h index 86defd7..c4c1fd3 100644 --- a/src/state.h +++ b/src/state.h @@ -7,15 +7,11 @@ #include "attributes.h" #include "types.h" -#include "lilv/lilv.h" -#include "lv2/atom/atom.h" -#include "lv2/state/state.h" -#include "lv2/urid/urid.h" -#include "serd/serd.h" - -JALV_BEGIN_DECLS +#include <lilv/lilv.h> +#include <lv2/state/state.h> // State and preset utilities +JALV_BEGIN_DECLS typedef int (*PresetSink)(Jalv* jalv, const LilvNode* node, @@ -44,20 +40,11 @@ jalv_save_preset(Jalv* jalv, void jalv_save(Jalv* jalv, const char* dir); -void -jalv_save_port_values(Jalv* jalv, SerdWriter* writer, const SerdNode* subject); - char* jalv_make_path(LV2_State_Make_Path_Handle handle, const char* path); void -jalv_apply_state(Jalv* jalv, LilvState* state); - -char* -atom_to_turtle(LV2_URID_Unmap* unmap, - const SerdNode* subject, - const SerdNode* predicate, - const LV2_Atom* atom); +jalv_apply_state(Jalv* jalv, const LilvState* state); JALV_END_DECLS diff --git a/src/string_utils.c b/src/string_utils.c new file mode 100644 index 0000000..dcabcac --- /dev/null +++ b/src/string_utils.c @@ -0,0 +1,16 @@ +// Copyright 2007-2022 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include "string_utils.h" + +#include <stdlib.h> +#include <string.h> + +char* +jalv_strdup(const char* const str) +{ + const size_t len = strlen(str); + char* copy = (char*)malloc(len + 1); + memcpy(copy, str, len + 1); + return copy; +} diff --git a/src/string_utils.h b/src/string_utils.h new file mode 100644 index 0000000..74193a0 --- /dev/null +++ b/src/string_utils.h @@ -0,0 +1,18 @@ +// Copyright 2007-2022 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#ifndef JALV_STRING_UTILS_H +#define JALV_STRING_UTILS_H + +#include "attributes.h" + +// String utilities +JALV_BEGIN_DECLS + +/// Return a newly allocated copy of a string +char* +jalv_strdup(const char* str); + +JALV_END_DECLS + +#endif // JALV_STRING_UTILS_H diff --git a/src/symap.c b/src/symap.c index 07002f2..46c147c 100644 --- a/src/symap.c +++ b/src/symap.c @@ -88,12 +88,11 @@ symap_search(const Symap* const map, const char* const sym, bool* const exact) uint32_t lower = 0; uint32_t upper = map->size - 1; uint32_t i = upper; - int cmp = 0; while (upper >= lower) { - i = lower + ((upper - lower) / 2); - cmp = strcmp(map->symbols[map->index[i] - 1], sym); + i = lower + ((upper - lower) / 2); + const int cmp = strcmp(map->symbols[map->index[i] - 1], sym); if (cmp == 0) { *exact = true; return i; @@ -191,7 +190,7 @@ symap_unmap(const Symap* const map, const uint32_t id) # include <stdio.h> static void -symap_dump(Symap* const map) +symap_dump(const Symap* const map) { fprintf(stderr, "{\n"); for (uint32_t i = 0; i < map->size; ++i) { diff --git a/src/symap.h b/src/symap.h index 74dc59a..66a1514 100644 --- a/src/symap.h +++ b/src/symap.h @@ -12,15 +12,14 @@ #ifndef SYMAP_H #define SYMAP_H -#include "zix/attributes.h" +#include <zix/attributes.h> #include <stdint.h> typedef struct SymapImpl Symap; /// Create a new symbol map -ZIX_MALLOC_FUNC -Symap* +ZIX_MALLOC_FUNC Symap* symap_new(void); /// Free a symbol map @@ -28,8 +27,7 @@ void symap_free(Symap* map); /// Map a string to a symbol if it is already mapped, otherwise return 0 -ZIX_PURE_FUNC -uint32_t +ZIX_PURE_FUNC uint32_t symap_try_map(const Symap* map, const char* sym); /// Map a string to a symbol @@ -37,8 +35,7 @@ uint32_t symap_map(Symap* map, const char* sym); /// Unmap a symbol back to a string if possible, otherwise return NULL -ZIX_PURE_FUNC -const char* +ZIX_PURE_FUNC const char* symap_unmap(const Symap* map, uint32_t id); #endif // SYMAP_H diff --git a/src/types.h b/src/types.h index ed577f6..4f25301 100644 --- a/src/types.h +++ b/src/types.h @@ -1,4 +1,4 @@ -// Copyright 2007-2022 David Robillard <d@drobilla.net> +// Copyright 2007-2024 David Robillard <d@drobilla.net> // SPDX-License-Identifier: ISC #ifndef JALV_TYPES_H @@ -6,10 +6,14 @@ #include "attributes.h" +// Basic internal type declarations JALV_BEGIN_DECLS -/// Backend playing state -typedef enum { JALV_RUNNING, JALV_PAUSE_REQUESTED, JALV_PAUSED } JalvPlayState; +/// Process thread running state +typedef enum { + JALV_RUNNING, ///< Active and running the plugin + JALV_PAUSED, ///< Active but bypassing the plugin (silent) +} JalvRunState; /// "Global" application state typedef struct JalvImpl Jalv; @@ -17,6 +21,18 @@ typedef struct JalvImpl Jalv; /// Audio/MIDI backend typedef struct JalvBackendImpl JalvBackend; +/// Plugin port "direction" +typedef enum { FLOW_UNKNOWN, FLOW_INPUT, FLOW_OUTPUT } PortFlow; + +/// Plugin port type +typedef enum { + TYPE_UNKNOWN, + TYPE_CONTROL, + TYPE_AUDIO, + TYPE_EVENT, + TYPE_CV +} PortType; + JALV_END_DECLS #endif // JALV_TYPES_H diff --git a/src/urids.c b/src/urids.c new file mode 100644 index 0000000..ff74635 --- /dev/null +++ b/src/urids.c @@ -0,0 +1,56 @@ +// Copyright 2022-2024 David Robillard <d@drobilla.net> +// SPDX-License-Identifier: ISC + +#include "urids.h" + +#include "mapper.h" + +#include <lv2/atom/atom.h> +#include <lv2/buf-size/buf-size.h> +#include <lv2/log/log.h> +#include <lv2/midi/midi.h> +#include <lv2/parameters/parameters.h> +#include <lv2/patch/patch.h> +#include <lv2/time/time.h> +#include <lv2/ui/ui.h> + +void +jalv_init_urids(JalvMapper* const mapper, JalvURIDs* const urids) +{ +#define MAP_URI(uri) jalv_mapper_map_uri(mapper, (uri)) + + urids->atom_Chunk = MAP_URI(LV2_ATOM__Chunk); + urids->atom_Float = MAP_URI(LV2_ATOM__Float); + urids->atom_Int = MAP_URI(LV2_ATOM__Int); + urids->atom_Object = MAP_URI(LV2_ATOM__Object); + urids->atom_Path = MAP_URI(LV2_ATOM__Path); + urids->atom_Sequence = MAP_URI(LV2_ATOM__Sequence); + urids->atom_String = MAP_URI(LV2_ATOM__String); + urids->atom_eventTransfer = MAP_URI(LV2_ATOM__eventTransfer); + urids->bufsz_maxBlockLength = MAP_URI(LV2_BUF_SIZE__maxBlockLength); + urids->bufsz_minBlockLength = MAP_URI(LV2_BUF_SIZE__minBlockLength); + urids->bufsz_sequenceSize = MAP_URI(LV2_BUF_SIZE__sequenceSize); + urids->log_Error = MAP_URI(LV2_LOG__Error); + urids->log_Trace = MAP_URI(LV2_LOG__Trace); + urids->log_Warning = MAP_URI(LV2_LOG__Warning); + urids->midi_MidiEvent = MAP_URI(LV2_MIDI__MidiEvent); + urids->param_sampleRate = MAP_URI(LV2_PARAMETERS__sampleRate); + urids->patch_Get = MAP_URI(LV2_PATCH__Get); + urids->patch_Put = MAP_URI(LV2_PATCH__Put); + urids->patch_Set = MAP_URI(LV2_PATCH__Set); + urids->patch_body = MAP_URI(LV2_PATCH__body); + urids->patch_property = MAP_URI(LV2_PATCH__property); + urids->patch_value = MAP_URI(LV2_PATCH__value); + urids->time_Position = MAP_URI(LV2_TIME__Position); + urids->time_bar = MAP_URI(LV2_TIME__bar); + urids->time_barBeat = MAP_URI(LV2_TIME__barBeat); + urids->time_beatUnit = MAP_URI(LV2_TIME__beatUnit); + urids->time_beatsPerBar = MAP_URI(LV2_TIME__beatsPerBar); + urids->time_beatsPerMinute = MAP_URI(LV2_TIME__beatsPerMinute); + urids->time_frame = MAP_URI(LV2_TIME__frame); + urids->time_speed = MAP_URI(LV2_TIME__speed); + urids->ui_scaleFactor = MAP_URI(LV2_UI__scaleFactor); + urids->ui_updateRate = MAP_URI(LV2_UI__updateRate); + +#undef MAP_URI +} diff --git a/src/urids.h b/src/urids.h index 4dcbe20..7ac1a45 100644 --- a/src/urids.h +++ b/src/urids.h @@ -5,16 +5,20 @@ #define JALV_URIDS_H #include "attributes.h" +#include "mapper.h" -#include "lv2/urid/urid.h" +#include <lv2/urid/urid.h> +// Cached LV2 URIDs JALV_BEGIN_DECLS typedef struct { + LV2_URID atom_Chunk; LV2_URID atom_Float; LV2_URID atom_Int; LV2_URID atom_Object; LV2_URID atom_Path; + LV2_URID atom_Sequence; LV2_URID atom_String; LV2_URID atom_eventTransfer; LV2_URID bufsz_maxBlockLength; @@ -43,6 +47,9 @@ typedef struct { LV2_URID ui_updateRate; } JalvURIDs; +void +jalv_init_urids(JalvMapper* mapper, JalvURIDs* urids); + JALV_END_DECLS #endif // JALV_URIDS_H diff --git a/src/worker.c b/src/worker.c index 0dca657..f03c823 100644 --- a/src/worker.c +++ b/src/worker.c @@ -3,29 +3,35 @@ #include "worker.h" -#include "lv2/core/lv2.h" -#include "lv2/worker/worker.h" -#include "zix/ring.h" -#include "zix/sem.h" -#include "zix/status.h" -#include "zix/thread.h" +#include <lv2/core/lv2.h> +#include <lv2/worker/worker.h> +#include <zix/ring.h> +#include <zix/sem.h> +#include <zix/status.h> +#include <zix/thread.h> #include <stdio.h> #include <stdlib.h> #define MAX_PACKET_SIZE 4096U +typedef enum { + STATE_SINGLE_THREADED, ///< Single-threaded worker (only state) + STATE_STOPPED, ///< Thread is exited + STATE_LAUNCHED, ///< Thread is running + STATE_MUST_EXIT, ///< Thread exit requested +} WorkerState; + struct JalvWorkerImpl { ZixRing* requests; ///< Requests to the worker ZixRing* responses; ///< Responses from the worker void* response; ///< Worker response buffer ZixSem* lock; ///< Lock for plugin work() method - bool exit; ///< Exit flag ZixSem sem; ///< Worker semaphore + WorkerState state; ///< Worker state ZixThread thread; ///< Worker thread LV2_Handle handle; ///< Plugin handle const LV2_Worker_Interface* iface; ///< Plugin worker interface - bool threaded; ///< Run work in another thread }; static LV2_Worker_Status @@ -51,7 +57,7 @@ jalv_worker_respond(LV2_Worker_Respond_Handle handle, return jalv_worker_write_packet(((JalvWorker*)handle)->responses, size, data); } -static void* +static ZixThreadResult ZIX_THREAD_FUNC worker_func(void* const data) { JalvWorker* const worker = (JalvWorker*)data; @@ -60,7 +66,7 @@ worker_func(void* const data) while (true) { // Wait for a request zix_sem_wait(&worker->sem); - if (worker->exit) { + if (worker->state == STATE_MUST_EXIT) { break; } @@ -88,88 +94,86 @@ worker_func(void* const data) } free(buf); - return NULL; -} - -static ZixStatus -jalv_worker_launch(JalvWorker* const worker) -{ - ZixStatus st = ZIX_STATUS_SUCCESS; - - if ((st = zix_sem_init(&worker->sem, 0)) || - (st = zix_thread_create(&worker->thread, 4096U, worker_func, worker))) { - return st; - } - - ZixRing* const requests = zix_ring_new(NULL, MAX_PACKET_SIZE); - if (!requests) { - zix_thread_join(worker->thread); - zix_sem_destroy(&worker->sem); - return ZIX_STATUS_NO_MEM; - } - - zix_ring_mlock(requests); - worker->requests = requests; - return ZIX_STATUS_SUCCESS; + worker->state = STATE_STOPPED; + return ZIX_THREAD_RESULT; } JalvWorker* jalv_worker_new(ZixSem* const lock, const bool threaded) { JalvWorker* const worker = (JalvWorker*)calloc(1, sizeof(JalvWorker)); + ZixRing* const requests = zix_ring_new(NULL, MAX_PACKET_SIZE); ZixRing* const responses = zix_ring_new(NULL, MAX_PACKET_SIZE); void* const response = calloc(1, MAX_PACKET_SIZE); if (worker && responses && response) { - worker->threaded = threaded; + worker->requests = requests; worker->responses = responses; worker->response = response; worker->lock = lock; - worker->exit = false; + worker->state = threaded ? STATE_STOPPED : STATE_SINGLE_THREADED; + zix_ring_mlock(requests); zix_ring_mlock(responses); - if (!threaded || !jalv_worker_launch(worker)) { - return worker; - } + return worker; } - free(worker); - zix_ring_free(responses); free(response); + zix_ring_free(responses); + zix_ring_free(requests); + free(worker); return NULL; } void -jalv_worker_start(JalvWorker* const worker, - const LV2_Worker_Interface* const iface, - LV2_Handle handle) +jalv_worker_free(JalvWorker* const worker) { if (worker) { - worker->iface = iface; - worker->handle = handle; + jalv_worker_exit(worker); + zix_ring_free(worker->requests); + zix_ring_free(worker->responses); + free(worker->response); + free(worker); } } +ZixStatus +jalv_worker_launch(JalvWorker* const worker) +{ + ZixStatus st = ZIX_STATUS_SUCCESS; + if (worker->state == STATE_STOPPED) { + if ((st = zix_sem_init(&worker->sem, 0))) { + return st; + } + + if ((st = zix_thread_create(&worker->thread, 4096U, worker_func, worker))) { + zix_sem_destroy(&worker->sem); + return st; + } + + worker->state = STATE_LAUNCHED; + } + return st; +} + void jalv_worker_exit(JalvWorker* const worker) { - if (worker && worker->threaded) { - worker->exit = true; + if (worker && worker->state == STATE_LAUNCHED) { + worker->state = STATE_MUST_EXIT; zix_sem_post(&worker->sem); zix_thread_join(worker->thread); - worker->threaded = false; } } void -jalv_worker_free(JalvWorker* const worker) +jalv_worker_attach(JalvWorker* const worker, + const LV2_Worker_Interface* const iface, + LV2_Handle handle) { if (worker) { - jalv_worker_exit(worker); - zix_ring_free(worker->requests); - zix_ring_free(worker->responses); - free(worker->response); - free(worker); + worker->iface = iface; + worker->handle = handle; } } @@ -181,17 +185,16 @@ jalv_worker_schedule(LV2_Worker_Schedule_Handle handle, JalvWorker* const worker = (JalvWorker*)handle; LV2_Worker_Status st = LV2_WORKER_SUCCESS; - if (!worker || !size) { - return LV2_WORKER_ERR_UNKNOWN; - } + if (!worker || !size || worker->state == STATE_STOPPED) { + st = LV2_WORKER_ERR_UNKNOWN; - if (worker->threaded) { + } else if (worker->state == STATE_LAUNCHED) { // Schedule a request to be executed by the worker thread if (!(st = jalv_worker_write_packet(worker->requests, size, data))) { zix_sem_post(&worker->sem); } - } else { + } else if (worker->state == STATE_SINGLE_THREADED) { // Execute work immediately in this thread zix_sem_wait(worker->lock); st = worker->iface->work( diff --git a/src/worker.h b/src/worker.h index 09d35ca..0e39894 100644 --- a/src/worker.h +++ b/src/worker.h @@ -6,14 +6,15 @@ #include "attributes.h" -#include "zix/sem.h" - -#include "lv2/core/lv2.h" -#include "lv2/worker/worker.h" +#include <lv2/core/lv2.h> +#include <lv2/worker/worker.h> +#include <zix/sem.h> +#include <zix/status.h> #include <stdbool.h> #include <stdint.h> +// Worker implementation JALV_BEGIN_DECLS /** @@ -36,17 +37,23 @@ JalvWorker* jalv_worker_new(ZixSem* lock, bool threaded); /** - Start performing work for a plugin, launching the thread if necessary. - - This must be called before scheduling any work. + Free a worker allocated with jalv_worker_new(). - @param iface Worker interface from plugin. - @param handle Handle to the LV2 plugin this worker is for. + Calls jalv_worker_exit() to terminate the running thread if necessary. */ void -jalv_worker_start(JalvWorker* worker, - const LV2_Worker_Interface* iface, - LV2_Handle handle); +jalv_worker_free(JalvWorker* worker); + +/** + Launch the worker's thread. + + For threaded workers, this launches the thread if it isn't already running. + For non-threaded workers, this does nothing. + + @return Zero on success, or a non-zero error code if launching failed. +*/ +ZixStatus +jalv_worker_launch(JalvWorker* worker); /** Terminate the worker's thread if necessary. @@ -58,12 +65,18 @@ void jalv_worker_exit(JalvWorker* worker); /** - Free a worker allocated with jalv_worker_new(). + Attach the worker to a plugin instance. - Calls jalv_worker_exit() to terminate the running thread if necessary. + This must be called before scheduling any work. + + @param worker Worker to activate and attach to a plugin. + @param iface Worker interface from plugin. + @param handle Handle to the LV2 plugin this worker is for. */ void -jalv_worker_free(JalvWorker* worker); +jalv_worker_attach(JalvWorker* worker, + const LV2_Worker_Interface* iface, + LV2_Handle handle); /** Schedule work to be performed by the worker in the audio thread. diff --git a/subprojects/lilv.wrap b/subprojects/lilv.wrap index ff82752..61b77c5 100644 --- a/subprojects/lilv.wrap +++ b/subprojects/lilv.wrap @@ -1,8 +1,14 @@ -# Copyright 2022 David Robillard <d@drobilla.net> +# Copyright 2022-2025 David Robillard <d@drobilla.net> # SPDX-License-Identifier: 0BSD OR ISC -[wrap-git] -url = https://gitlab.com/lv2/lilv.git -push-url = ssh://git@gitlab.com:lv2/lilv.git -revision = master -depth = 1 +[wrap-file] +directory = lilv-0.24.26 +source_url = https://download.drobilla.net/lilv-0.24.26.tar.xz +source_filename = lilv-0.24.26.tar.xz +source_hash = 22feed30bc0f952384a25c2f6f4b04e6d43836408798ed65a8a934c055d5d8ac + +# [wrap-git] +# url = https://gitlab.com/lv2/lilv.git +# push-url = ssh://git@gitlab.com:lv2/lilv.git +# revision = v0.24.26 +# depth = 1 diff --git a/subprojects/lv2.wrap b/subprojects/lv2.wrap index 7421eb7..ce82cf8 100644 --- a/subprojects/lv2.wrap +++ b/subprojects/lv2.wrap @@ -4,5 +4,5 @@ [wrap-git] url = https://gitlab.com/lv2/lv2.git push-url = ssh://git@gitlab.com:lv2/lv2.git -revision = master +revision = main depth = 1 diff --git a/subprojects/serd.wrap b/subprojects/serd.wrap index 4bd4109..3b84a4c 100644 --- a/subprojects/serd.wrap +++ b/subprojects/serd.wrap @@ -1,8 +1,14 @@ -# Copyright 2022 David Robillard <d@drobilla.net> +# Copyright 2022-2025 David Robillard <d@drobilla.net> # SPDX-License-Identifier: 0BSD OR ISC +# [wrap-file] +# directory = serd-0.32.4 +# source_url = https://download.drobilla.net/serd-0.32.4.tar.xz +# source_filename = serd-0.32.4.tar.xz +# source_hash = cbefb569e8db686be8c69cb3866a9538c7cb055e8f24217dd6a4471effa7d349 + [wrap-git] url = https://gitlab.com/drobilla/serd.git push-url = ssh://git@gitlab.com:drobilla/serd.git -revision = master +revision = main depth = 1 diff --git a/subprojects/sord.wrap b/subprojects/sord.wrap index 7ca49cb..89b2933 100644 --- a/subprojects/sord.wrap +++ b/subprojects/sord.wrap @@ -1,8 +1,14 @@ -# Copyright 2022 David Robillard <d@drobilla.net> +# Copyright 2022-2025 David Robillard <d@drobilla.net> # SPDX-License-Identifier: 0BSD OR ISC -[wrap-git] -url = https://gitlab.com/drobilla/sord.git -push-url = ssh://git@gitlab.com:drobilla/sord.git -revision = master -depth = 1 +[wrap-file] +directory = sord-0.16.18 +source_url = https://download.drobilla.net/sord-0.16.18.tar.xz +source_filename = sord-0.16.18.tar.xz +source_hash = 4f398b635894491a4774b1498959805a08e11734c324f13d572dea695b13d3b3 + +# [wrap-git] +# url = https://gitlab.com/drobilla/sord.git +# push-url = ssh://git@gitlab.com:drobilla/sord.git +# revision = v0.16.18 +# depth = 1 diff --git a/subprojects/sratom.wrap b/subprojects/sratom.wrap index 9bfac61..1524426 100644 --- a/subprojects/sratom.wrap +++ b/subprojects/sratom.wrap @@ -1,8 +1,14 @@ -# Copyright 2022 David Robillard <d@drobilla.net> +# Copyright 2022-2025 David Robillard <d@drobilla.net> # SPDX-License-Identifier: 0BSD OR ISC -[wrap-git] -url = https://gitlab.com/lv2/sratom.git -push-url = ssh://git@gitlab.com:lv2/sratom.git -revision = master -depth = 1 +[wrap-file] +directory = sratom-0.6.18 +source_url = https://download.drobilla.net/sratom-0.6.18.tar.xz +source_filename = sratom-0.6.18.tar.xz +source_hash = 4c6a6d9e0b4d6c01cc06a8849910feceb92e666cb38779c614dd2404a9931e92 + +# [wrap-git] +# url = https://gitlab.com/lv2/sratom.git +# push-url = ssh://git@gitlab.com:lv2/sratom.git +# revision = v0.6.18 +# depth = 1 diff --git a/subprojects/suil.wrap b/subprojects/suil.wrap index dfc1ae8..16e28d4 100644 --- a/subprojects/suil.wrap +++ b/subprojects/suil.wrap @@ -1,8 +1,14 @@ -# Copyright 2022 David Robillard <d@drobilla.net> +# Copyright 2022-2025 David Robillard <d@drobilla.net> # SPDX-License-Identifier: 0BSD OR ISC -[wrap-git] -url = https://gitlab.com/lv2/suil.git -push-url = ssh://git@gitlab.com:lv2/suil.git -revision = master -depth = 1 +[wrap-file] +directory = suil-0.10.22 +source_url = https://download.drobilla.net/suil-0.10.22.tar.xz +source_filename = suil-0.10.22.tar.xz +source_hash = d720969e0f44a99d5fba35c733a43ed63a16b0dab867970777efca4b25387eb7 + +# [wrap-git] +# url = https://gitlab.com/lv2/suil.git +# push-url = ssh://git@gitlab.com:lv2/suil.git +# revision = v0.10.22 +# depth = 1 diff --git a/subprojects/zix.wrap b/subprojects/zix.wrap index 7a50ea2..531d29c 100644 --- a/subprojects/zix.wrap +++ b/subprojects/zix.wrap @@ -1,14 +1,14 @@ -# Copyright 2022-2023 David Robillard <d@drobilla.net> +# Copyright 2022-2025 David Robillard <d@drobilla.net> # SPDX-License-Identifier: 0BSD OR ISC [wrap-file] -directory = zix-0.4.0 -source_url = https://download.drobilla.net/zix-0.4.0.tar.xz -source_filename = zix-0.4.0.tar.xz -source_hash = ac88dbefd9d29bfdce1532165c957d19d474a8367b727edb7be7d524e6cf9a14 +directory = zix-0.6.2 +source_url = https://download.drobilla.net/zix-0.6.2.tar.xz +source_filename = zix-0.6.2.tar.xz +source_hash = 4bc771abf4fcf399ea969a1da6b375f0117784f8fd0e2db356a859f635f616a7 # [wrap-git] # url = https://gitlab.com/drobilla/zix.git # push-url = ssh://git@gitlab.com:drobilla/zix.git -# revision = v0.4.0 +# revision = v0.6.2 # depth = 1 diff --git a/test/meson.build b/test/meson.build index f26b819..5e89d48 100644 --- a/test/meson.build +++ b/test/meson.build @@ -1,28 +1,92 @@ -# Copyright 2019-2022 David Robillard <d@drobilla.net> +# Copyright 2019-2024 David Robillard <d@drobilla.net> # SPDX-License-Identifier: 0BSD OR ISC +######## +# Lint # +######## + +all_sources = ( + common_sources + files( + '../src/attributes.h', + '../src/backend.h', + '../src/control.h', + '../src/frontend.h', + '../src/jack.c', + '../src/jalv.h', + '../src/jalv_config.h', + '../src/jalv_console.c', + '../src/jalv_gtk.c', + '../src/jalv_qt.cpp', + '../src/jalv_qt.hpp', + '../src/log.h', + '../src/lv2_evbuf.h', + '../src/nodes.h', + '../src/options.h', + '../src/port.h', + '../src/portaudio.c', + '../src/state.h', + '../src/symap.h', + '../src/types.h', + '../src/urids.h', + '../src/worker.h', + ) +) + if get_option('lint') if not meson.is_subproject() # Check release metadata autoship = find_program('autoship', required: get_option('tests')) if autoship.found() test( - 'autoship', autoship, + 'autoship', + autoship, args: ['test', jalv_src_root], suite: 'data', ) endif + + # Check code with cppcheck + cppcheck = find_program('cppcheck', required: false) + if cppcheck.found() + compdb_path = join_paths(jalv_build_root, 'compile_commands.json') + suppress_path = join_paths(jalv_src_root, '.suppress.cppcheck') + test( + 'cppcheck', + cppcheck, + args: [ + '--enable=warning,style,performance,portability', + '--error-exitcode=1', + '--project=' + compdb_path, + '--suppressions-list=' + suppress_path, + '-q', + ['-i', jalv_build_root], + ], + suite: 'code', + ) + endif endif # Check licensing metadata reuse = find_program('reuse', required: false) if reuse.found() test( - 'REUSE', reuse, + 'REUSE', + reuse, args: ['--root', jalv_src_root, 'lint'], suite: 'data', ) endif + + # Check code formatting + clang_format = find_program('clang-format', required: false) + if clang_format.found() + test( + 'format', + clang_format, + args: ['--Werror', '--dry-run'] + all_sources, + suite: 'code', + ) + endif endif ############## @@ -34,7 +98,7 @@ test( executable( 'test_symap', files('../src/symap.c'), - c_args: ['-DSYMAP_STANDALONE'], + c_args: c_suppressions + ['-DSYMAP_STANDALONE'], dependencies: [zix_dep], ), ) |