diff --git a/.github/workflows/makefile.yml b/.github/workflows/makefile.yml index c5c4c83..1e198dd 100644 --- a/.github/workflows/makefile.yml +++ b/.github/workflows/makefile.yml @@ -97,7 +97,7 @@ jobs: # Big thanks to @tomara-x and @timothyschoen for showing me how to do this! -ag github-release: - if: startsWith(github.ref, 'refs/tags/') + if: github.ref_type == 'tag' runs-on: ubuntu-latest needs: [ubuntu-build, macos-build, windows-build] @@ -157,13 +157,11 @@ jobs: - name: check deken package shell: bash run: | - SHORT=${GITHUB_REF:10} - VERSION=${SHORT//\//_} echo "## ${{ matrix.os }}" | tee -a $GITHUB_STEP_SUMMARY mkdir -p package-${{ matrix.os }} docker run --rm --user $(id -u) --volume ./pdlua-${{ matrix.os }}:/pdlua \ --volume ./package-${{ matrix.os }}:/package registry.git.iem.at/pd/deken \ - deken package --output-dir /package -v "${VERSION}" /pdlua + deken package --output-dir /package -v "${{ github.ref_name }}" /pdlua dek_files=$(ls package-${{ matrix.os }}/*.dek) for dek_file in $dek_files; do @@ -204,13 +202,11 @@ jobs: DEKEN_USERNAME: ${{ secrets.DEKEN_USERNAME }} DEKEN_PASSWORD: ${{ secrets.DEKEN_PASSWORD }} run: | - SHORT=${GITHUB_REF:10} - VERSION=${SHORT//\//_} for os in ubuntu macos windows; do docker run --rm -e DEKEN_USERNAME -e DEKEN_PASSWORD \ --volume ./pdlua-${os}:/pdlua registry.git.iem.at/pd/deken \ - deken upload --no-source-error -v "${VERSION}" /pdlua + deken upload --no-source-error -v "${{ github.ref_name }}" /pdlua done docker run --rm -e DEKEN_USERNAME -e DEKEN_PASSWORD \ --volume ./pdlua-src:/pdlua registry.git.iem.at/pd/deken \ - deken upload -v "${VERSION}" /pdlua + deken upload -v "${{ github.ref_name }}" /pdlua diff --git a/Makefile b/Makefile index 739aeb1..791024e 100644 --- a/Makefile +++ b/Makefile @@ -56,4 +56,5 @@ pdx_files = $(addprefix ./pdlua/tutorial/examples/, pdx.lua pd-remote.el pd-remo installplus: $(INSTALL_DIR) -v "$(installpath)" cp -r ./pdlua/ "${installpath}"/pdlua + cp pdlua-meta.pd "${installpath}" cp $(pdx_files) "${installpath}" diff --git a/README b/README index dcbbd5f..9216d33 100644 --- a/README +++ b/README @@ -27,8 +27,8 @@ corresponding subdirectories). Originally written by Claude Heiland-Allen, pdlua has gone through the hands of a few people over the years, including mrpeach (maintainer since 2011), zmoelnig a.k.a. umlaeute (loader update, Debian package), and myself (Arch package, Lua 5.3+ support, Purr Data and -plugdata support, tutorial). Please also check my brief account on the history -of pd-lua below. +plugdata support, pdx.lua live-coding extension, tutorial). Please also check +my brief account on the history of pd-lua below. Lua 5.4 is highly recommended with the latest version, Lua 5.3 works as well. Reportedly, Lua 5.2 and even 5.1 still work (at least to some extent), @@ -85,19 +85,16 @@ on https://github.com/agraef/pd-lua (but see below for the Deken package). Also, both Purr Data and plugdata ship with pdlua, and have it enabled by default, so no 3rd party package is needed in those environments. -For vanilla, we recommend installing the Deken package (named `pdlua`), +For vanilla Pd, we recommend installing the Deken package (named `pdlua`), readily available using Pd's `Find externals` menu option. This package was originally uploaded by Alexandre Porres, but Ben Wesch recently added Deken tests and uploads to our GitHub workflow, so that the Deken releases are now automatized and always in sync with the GitHub releases. Thanks, Ben! -The latest release is also in the Arch repositories (maintained by dvzrv, -thanks David!), and in the Debian repositories (maintained by IOhannes -Zmölnig, thanks umlaeute!). - -To enable the pdlua loader in Pd after installation, just add `pdlua` to your -startup libraries (after adding its parent directory to Pd's search path if -necessary) and you should be set. +pd-lua is also in the Arch repositories (maintained by dvzrv, thanks David!), +and in the Debian repositories (maintained by IOhannes Zmölnig, thanks +umlaeute!). (During busy times, these may trail our releases for a little bit, +but most of the time they will the latest version.) Mac users please note that the packages I distribute aren't notarized, so on recent macOS versions you'll have to jump through the usual hoops to make them @@ -106,6 +103,21 @@ usually do the trick, but check the internet for up-to-date information on Gatekeeper for details. +Enabling pdlua: + +Again, this step is only necessary with vanilla Pd; both Purr Data and +plugdata have the pdlua loader enabled by default, so it should be ready to go +immediately. + +With vanilla Pd, after installing pdlua, the pdlua loader also needs to be +added to your startup libraries, before Pd will recognize any Lua object in +your patches. This only needs to be done once. (Alternatively, you can also +use the `-lib pdlua` option on the Pd command line, or a `declare -lib pdlua` +object in a patch with Lua objects.) To make any of this work, you may also +have to add the parent directory of the `pdlua` folder to Pd's search path if +pdlua was installed in an unusual location. + + Compilation Instructions: You can also compile pdlua yourself from source, which isn't hard to do. diff --git a/doc/graphics.txt b/doc/graphics.txt index 37e5066..20928ef 100644 --- a/doc/graphics.txt +++ b/doc/graphics.txt @@ -28,47 +28,54 @@ end API overview -------------- --- Callback functions you can define -pd:Class:mouse_down(x, y) -- Mouse down callback, called when the mouse is clicked -pd:Class:mouse_up(x, y) -- Mouse up callback, called when the mouse button is released -pd:Class:mouse_move(x, y) -- Mouse move callback, called when the mouse is moved while not being down -pd:Class:mouse_drag(x, y) -- Mouse drag callback, called when the mouse is moved while also being down +-- Callback functions you can define +pd:Class:mouse_down(x, y) -- Mouse down callback, called when the mouse is clicked +pd:Class:mouse_up(x, y) -- Mouse up callback, called when the mouse button is released +pd:Class:mouse_move(x, y) -- Mouse move callback, called when the mouse is moved while not being down +pd:Class:mouse_drag(x, y) -- Mouse drag callback, called when the mouse is moved while also being down -- Functions you can call -pd:Class:repaint() -- Request a repaint, after this the "paint" callback will occur -pd:Class:paint(g) -- Paint callback, returns a graphics_context object (commonly called g) that you can call these drawing functions on: -g:set_size(w, h) -- Sets the size of the object. -width, height = g:get_size(w, h) -- Gets the size of the object. +pd:Class:repaint() -- Request a repaint, after this the "paint" callback will occur +pd:Class:paint(g) -- Paint callback, returns a graphics_context object (commonly called g) that you can call these drawing functions on: + +g:set_size(w, h) -- Sets the size of the object +width, height = g:get_size(w, h) -- Gets the size of the object -g:set_color(r, g, b, a=1.0) -- Sets the color for the next drawing operation. +g:set_color(r, g, b, a=1.0) -- Sets the color for the next drawing operation -g:fill_ellipse(x, y, w, h) -- Draws a filled ellipse at the specified position and size. -g:stroke_ellipse(x, y, w, h, line_width) -- Draws the outline of an ellipse at the specified position and size. +g:fill_ellipse(x, y, w, h) -- Draws a filled ellipse at the specified position and size +g:stroke_ellipse(x, y, w, h, line_width) -- Draws the outline of an ellipse at the specified position and size -g:fill_rect(x, y, w, h) -- Draws a filled rectangle at the specified position and size. -g:stroke_rect(x, y, w, h, line_width) -- Draws the outline of a rectangle at the specified position and size. +g:fill_rect(x, y, w, h) -- Draws a filled rectangle at the specified position and size +g:stroke_rect(x, y, w, h, line_width) -- Draws the outline of a rectangle at the specified position and size -g:fill_rounded_rect(x, y, w, h, corner_radius) -- Draws a filled rounded rectangle at the specified position and size. -g:stroke_rounded_rect(x, y, w, h, corner_radius, line_width) -- Draws the outline of a rounded rectangle at the specified position and size. +g:fill_rounded_rect(x, y, w, h, corner_radius) -- Draws a filled rounded rectangle at the specified position and size +g:stroke_rounded_rect(x, y, w, h, corner_radius, line_width) -- Draws the outline of a rounded rectangle at the specified position and size -g:draw_line(x1, y1, x2, y2) -- Draws a line between two points. -g:draw_text(text, x, y, w, fontsize) -- Draws text at the specified position and size. +g:draw_line(x1, y1, x2, y2) -- Draws a line between two points +g:draw_text(text, x, y, w, fontsize) -- Draws text at the specified position and size -g:fill_all() -- Fills the entire drawing area with the current color. Also will draw an object outline in the style of the host (ie. pure-data or plugdata) +g:fill_all() -- Fills the entire drawing area with the current color. Also will draw an object outline in the style of the host (ie. pure-data or plugdata) -g:translate(tx, ty) -- Translates the coordinate system by the specified amounts. -g:scale(sx, sy) -- Scales the coordinate system by the specified factors. This will always happen after the translation -g:reset_transform() -- Resets current scale and translation +g:translate(tx, ty) -- Translates the coordinate system by the specified amounts +g:scale(sx, sy) -- Scales the coordinate system by the specified factors. This will always happen after the translation +g:reset_transform() -- Resets current scale and translation -p = Path(x, y) -- Initiates a new path at the specified point -p:line_to(x, y) -- Adds a line segment to the path. -p:quad_to(x1, y1, x2, y2) -- Adds a quadratic Bezier curve to the path. -p:cubic_to(x1, y1, x2, y2, x3, y) -- Adds a cubic Bezier curve to the path. -p:close_path() -- Closes the path. +p = Path(x, y) -- Initiates a new path at the specified point +p:line_to(x, y) -- Adds a line segment to the path +p:quad_to(x1, y1, x2, y2) -- Adds a quadratic Bezier curve to the path +p:cubic_to(x1, y1, x2, y2, x3, y) -- Adds a cubic Bezier curve to the path +p:close_path() -- Closes the path + +g:stroke_path(p, line_width) -- Draws the outline of the path with the specified line width +g:fill_path(p) -- Fills the current path + +-- Additional functions +expandedsymbol = pd:Class:canvas_realizedollar(s) -- Expand dollar symbols in patch canvas context +pd:Class:set_args(args) -- Set the object arguments to be saved in the patch file +pd:Class:get_args() -- Get the object arguments to be saved in the patch file -g:stroke_path(p, line_width) -- Draws the outline of the path with the specified line width. -g:fill_path(p) -- Fills the current path. Basic example --------------------- @@ -94,12 +101,12 @@ function example:mouse_drag(x, y) self:repaint() end -function example:paint() +function example:paint(g) -- Fill background with color - gfx.set_color(255, 0, 0, 1) - gfx.fill_all() + g:set_color(255, 0, 0, 1) + g:fill_all() -- Draw an ellipse - gfx.set_color(0, 255, 0, 1) - gfx.fill_ellipse(self.circle_x, self.circle_y, 30, 30) + g:set_color(0, 255, 0, 1) + g:fill_ellipse(self.circle_x, self.circle_y, 30, 30) end diff --git a/pd.lua b/pd.lua index 4e3c581..a577cd1 100644 --- a/pd.lua +++ b/pd.lua @@ -20,24 +20,22 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -- storage for Pd C<->Lua interaction pd._classes = { } -- take absolute paths and turn them into classes -pd._fullpaths = { } pd._pathnames = { } -- look up absolute path by creation name pd._objects = { } pd._clocks = { } pd._receives = { } pd._loadpath = "" -pd._currentpath = "" -- add a path to Lua's "require" search paths -pd._setrequirepath = function(path, pdlua_path) +pd._setrequirepath = function(path) pd._packagepath = package.path pd._packagecpath = package.cpath if (pd._iswindows) then - package.path = path .. "\\?;" .. path .. "\\?.lua;" .. pdlua_path .. "\\?;" .. pdlua_path .. "\\?.lua;" .. package.path - package.cpath = path .. "\\?.dll;" .. pdlua_path .. "\\?.dll;" .. package.cpath + package.path = path .. "\\?.lua;" .. path .. "\\?\\init.lua;" .. package.path + package.cpath = path .. "\\?.dll;" .. package.cpath else - package.path = path .. "/?;" .. path .. "/?.lua;" .. pdlua_path .. "/?;" .. pdlua_path .. "/?.lua;" .. package.path - package.cpath = path .. "/?.so;" .. pdlua_path .. "/?.so;" .. package.cpath + package.path = path .. "/?.lua;" .. path .. "/?/init.lua;" .. package.path + package.cpath = path .. "/?.so;" .. package.cpath end end @@ -135,22 +133,6 @@ pd._whoami = function (object) end end ---whereami method dispatcher -pd._whereami = function(name) - if nil ~= pd._fullpaths[name] then - return pd._fullpaths[name] - end - - return nil -end - ---class method dispatcher -pd._get_class = function (object) - if nil ~= pd._objects[object] then - return pd._objects[object]:get_class() - end -end - -- prototypical OO system pd.Prototype = { } @@ -171,6 +153,12 @@ function pd.Clock:register(object, method) self._target = object self._method = method pd._clocks[self._clock] = self + -- ag 20240906: record the clock in the target's _clocks table so that it + -- can be destroyed automatically with the object + if not object._clocks then + object._clocks = { } + end + object._clocks[self._clock] = self return self end end @@ -178,9 +166,16 @@ function pd.Clock:register(object, method) end function pd.Clock:destruct() - pd._clocks[self._clock] = nil - pd._clockfree(self._clock) - self._clock = nil + -- ag 20240906: remove the clock from the target's _clocks table if any + if self._target and self._target._clocks then + --pd.post(string.format("%s: destroying clock %s", self._target._name, self._method)) + self._target._clocks[self._clock] = nil + end + if self._clock then + pd._clocks[self._clock] = nil + pd._clockfree(self._clock) + self._clock = nil + end end function pd.Clock:dispatch() @@ -272,6 +267,12 @@ function pd.Receive:register(object, name, method) self._target = object self._method = method pd._receives[self._receive] = self + -- ag 20240906: record the receiver in the target's _receives table so + -- that it can be destroyed automatically with the object + if not object._receives then + object._receives = { } + end + object._receives[self._receive] = self return self end end @@ -279,12 +280,19 @@ function pd.Receive:register(object, name, method) end function pd.Receive:destruct() - pd._receives[self._receive] = nil - pd._receivefree(self._receive) - self._receive = nil - self._name = nil - self._target = nil - self._method = nil + -- ag 20240906: remove the receiver from the target's _receives table if any + if self._target and self._target._receives then + --pd.post(string.format("%s: destroying receiver %s", self._target._name, self._method)) + self._target._receives[self._receive] = nil + end + if self._receive then + pd._receives[self._receive] = nil + pd._receivefree(self._receive) + self._receive = nil + self._name = nil + self._target = nil + self._method = nil + end end function pd.Receive:dispatch(sel, atoms) @@ -300,8 +308,19 @@ function pd.Class:register(name) -- Those trailing slashes keep piling up, thus we need to check whether -- pd._loadpath already has one. This is only a temporary kludge until a -- proper fix is deployed. -ag 2023-02-02 + --[[ + Oh well, it appears that the "temporary kludge" is here to stay, so + let's at least fix it up to preserve an empty _loadpath, which is what we + get with pdluax. Which makes sense, since pdluax's _scriptname will be an + absolute path. But then an empty _loadpath should stay empty so that + self._loadpath .. self._scriptname returns a proper path. + + This whole code has been touched so many times during sebshader's quest + to get relative paths in object names right, that I don't really dare to + touch it anymore, but this much we can do. -ag 20240905 + ]] local fullpath = string.sub(pd._loadpath, -1) == "/" and pd._loadpath or - (pd._loadpath .. '/') + pd._loadpath ~= "" and pd._loadpath .. '/' or "" local fullname = fullpath .. name if nil ~= pd._classes[fullname] then @@ -317,19 +336,10 @@ function pd.Class:register(name) else regname = name end - - --pd._fullpaths[regname] = pd._currentpath or (fullname .. ".pd_lua") - if pd._currentpath == nil or pd._currentpath == '' then - pd._fullpaths[regname] = fullname .. ".pd_lua" - else - pd._fullpaths[regname] = pd._currentpath - end - pd._pathnames[regname] = fullname pd._classes[fullname] = self -- record registration - self._class = pd._register(name) -- register new class + self._class, self._class_gfx = pd._register(name) -- register new class self._name = name - self._path = pd._fullpaths[regname] self._loadpath = fullpath if name == "pdlua" then self._scriptname = "pd.lua" @@ -340,10 +350,13 @@ function pd.Class:register(name) end function pd.Class:construct(sel, atoms) - self._object = pd._create(self._class) + self._object = pd._create(self._class, self._class_gfx) self.inlets = 0 self.outlets = 0 self._canvaspath = pd._canvaspath(self._object) .. "/" + if pdx then + pdx.reload(self) + end if self:initialize(sel, atoms) then pd._createinlets(self._object, self.inlets) pd._createoutlets(self._object, self.outlets) @@ -353,11 +366,41 @@ function pd.Class:construct(sel, atoms) self:postinitialize() return self else + if pdx then + pdx.unreload(self) + end return nil end end function pd.Class:destruct() + -- ag 20240906: get rid of all clocks and receivers registered for us + if self._clocks then + local clocks = { } + -- since the destruct() method destructively updates our _clocks table, + -- record all registered clock objects in a new table + for _, c in pairs(self._clocks) do + table.insert(clocks, c) + end + -- now destroy them + for _, c in ipairs(clocks) do + c:destruct() + end + self._clocks = nil + end + if self._receives then + local receives = { } + -- since the destruct() method destructively updates our _receives table, + -- record all registered receivers in a new table + for _, r in pairs(self._receives) do + table.insert(receives, r) + end + -- now destroy them + for _, r in ipairs(receives) do + r:destruct() + end + self._receives = nil + end pd._objects[self] = nil self:finalize() pd._destroy(self._object) @@ -406,10 +449,17 @@ function pd.Class:postinitialize() end function pd.Class:finalize() end +function pd.Class:get_args() + return pd._get_args(self._object) +end + function pd.Class:set_args(args) pd._set_args(self._object, args) end +function pd.Class:canvas_realizedollar(s) + return pd._canvas_realizedollar(self._object, s) +end function pd.Class:repaint(layer) if layer == nil or layer <= 1 then @@ -465,7 +515,6 @@ function pd.Class:dofilex(file) local pathsave = pd._loadpath pd._loadname = nil pd._loadpath = self._loadpath - pd._currentpath = file local f, path = pd._dofilex(self._class, file) pd._loadname = namesave pd._loadpath = pathsave @@ -480,7 +529,6 @@ function pd.Class:dofile(file) local pathsave = pd._loadpath pd._loadname = nil pd._loadpath = self._loadpath - pd._currentpath = file local f, path = pd._dofile(self._object, file) pd._loadname = namesave pd._loadpath = pathsave @@ -495,12 +543,11 @@ function pd.Class:whoami() return self._scriptname or self._name end -function pd.Class:in_1__reload() - self:dofile(self._path) -end - function pd.Class:get_class() -- accessor for t_class* - return self._class or nil + -- ag 20240905: this is now implemented on the C side (you probably + -- shouldn't use this any more, we only keep this here for backward + -- compatibility) + return pd._get_class(self) or nil end local lua = pd.Class:new():register("pdlua") -- global controls (the [pdlua] object only) @@ -515,6 +562,7 @@ function lua:in_1_load(atoms) -- execute a script self:dofile(atoms[1]) end + local luax = pd.Class:new():register("pdluax") -- classless lua externals (like [pdluax foo]) function luax:initialize(sel, atoms) -- motivation: pd-list 2007-09-23 @@ -543,7 +591,31 @@ function luax:initialize(sel, atoms) -- motivation: pd-list 2007-09-23 end end +-- convenience creation functions for classes, arrays, clocks, receivers + +function pd.class(...) + return pd.Class:new():register(...) +end + +function pd.table(...) + return pd.Table:new():sync(...) +end + +function pd.clock(...) + return pd.Clock:new():register(...) +end + +function pd.receive(...) + return pd.Receive:new():register(...) +end + +-- constants used in the signal and graphics API DATA = 0 SIGNAL = 1 Colors = {background = 0, foreground = 1, outline = 2} + +-- pre-load pdx.lua (advanced live coding support); if you don't want this, +-- just comment out the line below +pdx = require 'pdx' + -- fin pd.lua diff --git a/pdlua-help.pd b/pdlua-help.pd index dcb133e..d6b3a6c 100644 --- a/pdlua-help.pd +++ b/pdlua-help.pd @@ -12,7 +12,7 @@ #X text 245 491 NONE; #X text 245 526 NONE; #X text 179 456 load ; -#X text 151 226 <-- load and run a Lua file; +#X text 191 226 <-- load and run a Lua file; #X text 91 257 <-- global interface to pdlua; #X obj 306 4 cnv 15 250 40 empty empty empty 12 13 0 18 #7c7c7c #e0e4dc 0; #N canvas 382 141 749 319 (subpatch) 0; @@ -23,13 +23,13 @@ #N canvas 0 22 450 278 (subpatch) 0; #X coords 0 1 100 -1 299 39 1 0 0; #X restore 3 3 graph; -#X text 390 233 <- right click for help or open, f 16; -#X text 22 60 [pdlua] registers a loader that allows Pd externals written in Lua (with the "*.pd_lua" extension) to be loaded. To guarantee Pd will load these externals \, you should load [pdlua] as a library \, either at startup or with [delcare]:, f 80; +#X text 390 243 <- right click for help or open, f 16; +#X text 22 60 [pdlua] registers a loader that allows Pd externals written in Lua (with the .pd_lua extension) to be loaded. To guarantee Pd will load these externals \, you should load [pdlua] as a library \, either at startup or with [declare]:, f 80; #X text 75 114 loading [pdlua] from [declare] -->; #X text 263 456 - load and run a '*.lua' file; #X text 324 344 Details on writting lua externals ----->, f 23; #X text 23 291 The [hello] object above is loaded from a 'hello.pd_lua' file. You can also provide help files for it and right click on it to ask for them. Right click also allows you to open the .pd_lua file if your system has a known application for it., f 80; -#X text 164 353 (this version loads files as arguments), f 20; +#X text 164 353 (this version creates objects from .pd_luax files), f 20; #X obj 290 115 declare -lib pdlua -path pdlua; #X text 23 146 If you also create [pdlua] as an object as below \, a global interface is created to load and run "*.lua" files via the 'load' message. Make sure that Pd is aware of the paths where your .pd_lua externals and .lua files are - you can also use declare for this (see that above we also include '-path pdlua' so it finds the 'hello' external and its help file)., f 80; #N canvas 567 120 718 519 quickstart 0; @@ -40,25 +40,23 @@ #X obj 47 787 cnv 15 450 170 empty empty empty 20 12 0 14 #dfdfdf #404040 0; #X obj 47 1661 cnv 15 450 100 empty empty -\ Sending\ To\ Receivers: 20 12 0 14 #dfdfdf #000000 0; #X obj 47 1762 cnv 15 450 120 empty empty -\ Receivers: 20 12 0 14 #cccccc #000000 0; -#X text 65 113 The Lua loader included in -lib pdlua allows externals for Pd to be written in the Lua programming language. (http://www.lua.org/), f 65; +#X text 65 113 The Lua loader included in -lib pdlua allows externals for Pd to be written in the Lua programming language. (www.lua.org), f 65; #X text 65 145 If you try to create an object [foo] in Pd \, Pd checks if the class "foo" exists. If it doesn't \, it tries to load an external file that "probably" will contain code for "foo". The Lua loader adds support for loading "foo.pd_lua" when you try to create [foo]., f 65; #X obj 47 958 cnv 15 450 70 empty empty -\ Object\ Finalization: 20 12 0 14 #cccccc #000000 0; #X text 76 309 This creates a new Pd class called "foo". The 'local' declaration is optional \, but recommended -- without it \, 'foo' is global \, which means any Lua code can modify it (possibly by accident)., f 61; #X text 75 251 The first expression/statement in the text file "foo.pd_lua" should be of the form:; #X obj 47 1029 cnv 15 450 400 empty empty -\ Inlet\ Methods: 20 12 0 14 #dfdfdf #000000 0; -#X text 65 476 or equivalently:; +#X text 65 486 or equivalently:; #X obj 47 1430 cnv 15 450 230 empty empty -\ Sending\ To\ Outlets: 20 12 0 14 #cccccc #000000 0; #X text 65 390 Then you can add methods to the Pd class. The most important one is 'initialize' \, which is executed when a new object is created:, f 65; -#X text 81 578 [foo a b 1 2 3 c]; -#X text 65 549 'sel' is usually (always?) the class name \, 'atoms' are the creation arguments in a Lua table. For example a Pd object, f 61; -#X text 66 596 would have sel equal to "foo" and the atoms:; +#X text 81 598 [foo a b 1 2 3 c]; +#X text 65 569 'sel' is the class name \, 'atoms' are the creation arguments in a Lua table. For example a Pd object, f 61; +#X text 66 616 would have sel equal to "foo" and the atoms:; #X text 65 670 Being a method \, 'initialize' has a 'self' variable (which is the object to be created) \, and if you want your objects to have inlets or outlets you need need to set those fields in this method (Pd doesn't support changing the number of inlets or outlets after an object is created):; #X text 65 769 The default inlet/outlet counts are 0; #X text 65 792 The return value of 'initialize' is used to allow objects to fail to create (for example \, if the creation arguments are bad). Most of the time you will 'return true' \, but if you really can't create then you can 'return false'.; #X text 65 980 The 'finalize' method is called when the object is deleted by Pd. You can clean up stuff here if needed. The default implementation does nothing.; -#X text 65 1680 You can send messages to receivers like this:; -#X text 65 631 where <> should be curly brackets \, but Pd won't print them in a comment., f 60; -#X text 79 1722 (again the <> represent curly brackets); +#X text 65 1690 You can send messages to receivers like this:; #X text 66 1842 Remember to clean up your receivers in object:finalize() \, or weird things will happen., f 57; #X obj 47 1883 cnv 15 450 120 empty empty -\ Clocks: 20 12 0 14 #dfdfdf #000000 0; #X obj 47 2004 cnv 15 450 140 empty empty -\ Miscellaneous\ Object\ Methods: 20 12 0 14 #cccccc #000000 0; @@ -69,19 +67,19 @@ #X obj 47 2145 cnv 15 450 80 empty empty -\ Miscellaneous\ Functions: 20 12 0 14 #dfdfdf #000000 0; #X text 65 2169 Print a string to Pd's console:; #X text 65 2206 Note that pd.post() should not really be used for errors.; -#X text 65 1784 You can bind methods to receivers \, to get messages from [send receiver] and " \; receiver message"., f 52; +#X text 65 1784 You can bind methods to receivers \, to get messages from [send receiver]., f 52; #X obj 83 284 cnv 15 300 20 empty empty empty 15 10 0 14 #ffffff #202020 0; -#X obj 82 422 cnv 15 300 50 empty empty empty 20 12 0 14 #ffffff #404040 0; -#X obj 82 495 cnv 15 300 50 empty empty empty 20 12 0 14 #ffffff #404040 0; -#X text 89 424 function foo:initialize(sel \, atoms); -#X text 90 437 -- code; -#X text 90 451 end; -#X text 90 499 foo.initialize = function (self \, sel \, atoms); -#X text 89 513 -- code; -#X text 90 526 end; +#X obj 82 427 cnv 15 300 50 empty empty empty 20 12 0 14 #ffffff #404040 0; +#X obj 82 505 cnv 15 300 50 empty empty empty 20 12 0 14 #ffffff #404040 0; +#X text 89 429 function foo:initialize(sel \, atoms); +#X text 90 442 -- code; +#X text 90 456 end; +#X text 90 509 foo.initialize = function (self \, sel \, atoms); +#X text 89 523 -- code; +#X text 90 536 end; #X obj 122 894 cnv 15 300 50 empty empty empty 20 12 0 14 #ffffff #404040 0; #X obj 82 736 cnv 15 300 30 empty empty empty 20 12 0 14 #ffffff #404040 0; -#X text 82 615 <"a" \, "b" \, 1 \, 2 \, 3 \, "c">; +#X text 82 635 {"a" \, "b" \, 1 \, 2 \, 3 \, "c"}; #X text 90 736 self.inlets = 1; #X text 129 896 function foo:postinitialize(); #X text 129 910 -- code; @@ -95,7 +93,7 @@ #X obj 514 1698 bng 20 250 50 0 empty empty empty 17 7 0 10 #dfdfdf #000000 #000000; #X floatatom 632 1814 3 0 0 0 - - - 0; #X text 66 1821 See doc/examples/lreceive.pd_lua for details.; -#X text 81 1743 See doc/examples/lsend.pd_lua for details.; +#X text 81 1738 See doc/examples/lsend.pd_lua for details.; #X text 66 1937 See doc/examples/ldelay.pd_lua for details.; #X obj 514 1730 lsend splat-1; #X obj 549 1705 lsend splat-2; @@ -108,20 +106,19 @@ #X text 99 1169 end; #X text 75 1273 A "gimme" method for [foo] accepts any input:; #X obj 92 1488 cnv 15 300 20 empty empty empty 20 12 0 14 #ffffff #404040 0; -#X text 99 1492 self:outlet(2 \, "bang" \, <>); -#X text 63 1511 (as usual <> should be curly brackets); +#X text 99 1492 self:outlet(2 \, "bang" \, {}); #X text 65 1467 This will cause the second outlet to emit a bang:; -#X obj 92 1558 cnv 15 300 20 empty empty empty 20 12 0 14 #ffffff #404040 0; -#X text 65 1537 This will cause the second outlet to emit a float:; -#X text 99 1562 self:outlet(2 \, "float" \, <123>); +#X obj 92 1548 cnv 15 300 20 empty empty empty 20 12 0 14 #ffffff #404040 0; +#X text 65 1527 This will cause the second outlet to emit a float:; +#X text 99 1552 self:outlet(2 \, "float" \, {123}); #X obj 92 1608 cnv 15 300 40 empty empty empty 20 12 0 14 #ffffff #404040 0; #X text 65 1586 This will cause the first outlet to emit a list:; #X text 99 1629 self:outlet(1 \, "list" \, somelist); -#X text 99 1612 self.somelist = ; +#X text 99 1612 self.somelist = {some items in a list}; #X text 75 1195 A "stop" method for inlet 2 of [foo]:; #X text 65 1084 The name of the method is constructed as "in_n_selector" where n is the inlet number (starting from 1) and selector is a type such as "float" or "bang" \, or a selector name such as "start". Here is a float method for [foo] inlet 1:; -#X obj 72 1697 cnv 15 360 20 empty empty empty 20 12 0 14 #ffffff #404040 0; -#X text 78 1699 pd.send("receiver" \, "selector" \, <"a" \, "message" \, 1 \, 2 \, 3>; +#X obj 72 1707 cnv 15 360 20 empty empty empty 20 12 0 14 #ffffff #404040 0; +#X text 78 1709 pd.send("receiver" \, "selector" \, {"a" \, "message" \, 1 \, 2 \, 3}); #X text 75 1350 A method for symbols on any input:; #X text 65 846 If you need to do things after the Pd object is created \, but before control is returned to Pd \, (such as registering receivers or clocks) you can use the 'postinitialize' method:; #X text 65 2106 This will allow the object to be highlighted from Pd's menu using Find->Find Last Error.; @@ -161,18 +158,11 @@ #N canvas 0 22 450 278 (subpatch) 0; #X coords 0 1 100 -1 451 2163 1 0 0; #X restore 46 82 graph; -#X text 69 21 Find basic instructions/examples below on how to write externals in Lua. For a detailed tutorial \, check the 'pdlua/tutorial' folder for a PDF documente. You can also find this tutorial online at: https://agraef.github.io/pd-lua/tutorial/pd-lua-intro.html, f 66; +#X text 69 16 Find basic instructions/examples below on how to write externals in Lua. For further details \, see 'pd-lua-intro.pdf' in the 'pdlua/tutorial' folder. You can also find this tutorial online at: https://agraef.github.io/pd-lua/tutorial/pd-lua-intro.html, f 66; #X obj 515 1624 declare -path pdlua/examples; #X text 549 1602 Examples from:; -#X connect 59 0 64 0; -#X connect 66 0 107 0; -#X connect 66 1 60 0; -#X connect 67 0 113 0; #X connect 107 0 108 0; -#X connect 107 1 109 0; #X connect 110 0 65 0; -#X connect 111 0 65 0; -#X connect 112 0 67 0; #X restore 446 359 pd quickstart; #N canvas 106 35 1079 707 graphics 0; #X obj 8 77 hello-gui; @@ -199,6 +189,6 @@ #X text 373 818 -- Mouse down callback \, called when the mouse is clicked \; -- Mouse up callback \, called when the mouse button is released \; -- Mouse move callback \, called when the mouse is moved while not being down \; -- Mouse drag callback \, called when the mouse is moved while also being down \; \; -- Set object size \; -- Get object size \; \; -- Request a repaint \, after this the "paint" callback will occur \; -- Paint callback The argument "g" is the graphics context that you can call these drawing functions on: \; -- Sets the color for the next drawing operation. Colors are in range 0-255 \, optional alpha is in range 0-1 \; \; -- Draws a filled ellipse at the specified position and size. \; -- Draws the outline of an ellipse at the specified position and size. \; -- Draws a filled rectangle at the specified position and size. \; -- Draws the outline of a rectangle at the specified position and size. \; -- Draws a filled rounded rectangle at the specified position and size. \; -- Draws the outline of a rounded rectangle at the specified position and size. \; \; \; -- Draws a line between two points. \; -- Draws text at the specified position and size. \; \; -- Fills the entire drawing area with the current color. Also will draw an object outline in the style of the host (ie. pure-data or plugdata) \; \; -- Translates the coordinate system by the specified amounts. \; -- Scales the coordinate system by the specified factors. This will always happen after the translation \; -- Resets current scale and translation \; \; -- Initiates a new path at the specified point. \; -- Adds a line segment to the current path. \; -- Adds a quadratic Bezier curve to the current path. \; -- Adds a cubic Bezier curve to the current path. \; -- Closes the current path. \; \; -- Draws the outline of the current path with the specified line width. \; -- Fills the current path. \;, f 115; #X restore 446 409 pd graphics; #X text 324 384 Details on how to create GUI objects ------->, f 18; -#X obj 342 227 hello; +#X obj 342 247 hello; #X msg 55 227 load hello-gui.pd_lua; #X connect 31 0 1 0; diff --git a/pdlua-meta.pd b/pdlua-meta.pd index 7854419..70a5292 100644 --- a/pdlua-meta.pd +++ b/pdlua-meta.pd @@ -1,10 +1,10 @@ -#N canvas 10 38 200 201 10; -#N canvas 359 186 421 302 META 0; -#X text 10 10 META this is a prototype of a libdir meta file; -#X text 10 30 NAME pdlua; -#X text 10 50 LICENSE GNU GPL; -#X text 10 70 DESCRIPTION lua loader for pd; +#N canvas 2 51 320 90 10; +#N canvas 2 51 382 154 META 0; +#X text 10 10 NAME pdlua; +#X text 10 30 LICENSE GPL-2+; +#X text 10 50 DESCRIPTION write your own Pd objects in the Lua programming language; #X text 10 90 AUTHOR Claude Heiland-Allen \, Frank Barknecht \, Martin -Peach \, IOhannes m zmölnig \, Albert Gräf; -#X text 10 123 VERSION 0.12.6; -#X restore 10 20 pd META; +Peach \, IOhannes m zmölnig \, Albert Gräf \, et al; +#X text 10 123 VERSION 0.12.18; +#X restore 43 36 pd META; +#X text 39 15 pdlua: Loader for Lua objects; diff --git a/pdlua.c b/pdlua.c index 376975e..df2d07b 100644 --- a/pdlua.c +++ b/pdlua.c @@ -56,6 +56,14 @@ #include "pdlua_gfx.h" +// This used to be in s_stuff.h, but not anymore since 0.55.1test1. +int sys_trytoopenone(const char *dir, const char *name, const char* ext, + char *dirresult, char **nameresult, unsigned int size, int bin); + +// Check for absolute filenames in the second argument. Otherwise, +// sys_trytoopenone will happily prepend the given path anyway. +#define trytoopenone(dir, name, ...) sys_trytoopenone(sys_isabsolutepath(name) ? "" : dir, name, __VA_ARGS__) + #ifdef PDINSTANCE typedef struct _lua_Instance { @@ -178,12 +186,13 @@ EXTERN int sys_trytoopenone(const char *dir, const char *name, const char* ext, // In plugdata we're linked statically and thus c_externdir is empty. // So we pass a data directory to the setup function instead and store it here. // ag: Renamed to pdlua_datadir since we also need this in vanilla when -// setting up the Lua search path when loading a pd_lua file. +// setting up Lua's package.path. char pdlua_datadir[MAXPDSTRING]; #if PLUGDATA // Hook to inform plugdata which class names are lua objects void(*plugdata_register_class)(const char*); #endif +static char pdlua_cwd[MAXPDSTRING]; /** State for the Lua file reader. */ typedef struct pdlua_readerdata @@ -520,6 +529,59 @@ static const char *basename(const char *name) return basenamep; } +// ag 20240907: Improved Lua error reporting. Go to some lengths to get +// prettier error messages than what the Lua runtime system provides. + +// This lets us report source locations in case there's no real Lua error, +// but we still want a well-formatted custom error message. +static char *src_info(lua_State *L, char *msg) +{ + // fill in some Lua source location information + lua_Debug ar; + // locate the Lua stack frame with our function; we're looking for a Lua + // source which is *not* pd.lua + for (int i = 1; i < 10 && lua_getstack(L, i, &ar) && lua_getinfo(L, "S", &ar); ++i) { + const char *src = ar.source; + // cf. "The Debug Interface" in the Lua reference manual + if (*src == '@') src = basename(src+1); + if (strcmp(ar.what, "Lua") == 0 && strcmp(src, "pd.lua") != 0) { + snprintf(msg, MAXPDSTRING-1, "%s: %d", src, ar.linedefined); + return msg; + } + } + // fall back to just a bland 'lua' if we couldn't find any information + strcpy(msg, "lua"); + return msg; +} + +// Drop-in replacement for lua_error() which outputs directly to the Pd +// console (instead of taking the detour via stderr), and also fixes up the +// error message itself; in particular, it replaces the [string "filename"] +// source designations with something nice. +static void mylua_error (lua_State *L, t_pdlua *o, const char *descr) +// o may indicate the object which is the source of the error, if available, +// otherwise it must be NULL; descr, if not NULL, is to be added to the message +{ + char *err = lua_isstring(L, -1) ? lua_tostring(L, -1) : "unknown error"; + // some sscanf magic to extract the real source name + char s[MAXPDSTRING]; int i; + if (sscanf(err, "[string \"%[^\"]\"]:%n", s, &i) <= 0) strcpy(s, ""); + // send the message directly to the Pd console + if (descr) { + if (*s) + pd_error(o, "lua: %s: %s: %s", descr, s, err+i); + else + pd_error(o, "lua: %s: %s", descr, err); + } else { + if (*s) + pd_error(o, "lua: %s: %s", s, err+i); + else + pd_error(o, "lua: %s", err); + } + // we've handled the error, pop the error string + lua_pop(L, 1); +} + /** Pd object constructor. */ static t_pdlua *pdlua_new ( @@ -591,14 +653,13 @@ static t_pdlua *pdlua_new { close(fd); pdlua_clearrequirepath(__L()); - lua_error(__L()); + mylua_error(__L(), NULL, NULL); } else { if (lua_pcall(__L(), 0, LUA_MULTRET, 0)) { - pd_error(NULL, "lua: error running `%s':\n%s", buf, lua_tostring(__L(), -1)); - lua_pop(__L(), 1); + mylua_error(__L(), NULL, NULL); close(fd); pdlua_clearrequirepath(__L()); } @@ -618,7 +679,7 @@ static t_pdlua *pdlua_new lua_setfield(__L(), -2, "_loadname"); luaL_unref(__L(), LUA_REGISTRYINDEX, load_name_save); } - else pd_error(NULL, "lua: error loading `%s': canvas_open() failed", buf); + else pd_error(NULL, "lua: constructor: couldn't locate `%s'", buf); } PDLUA_DEBUG("pdlua_new: after load script. stack top %d", lua_gettop(__L())); @@ -628,8 +689,8 @@ static t_pdlua *pdlua_new PDLUA_DEBUG("pdlua_new: before lua_pcall(L, 2, 1, 0) stack top %d", lua_gettop(__L())); if (lua_pcall(__L(), 2, 1, 0)) { - pd_error(NULL, "pdlua_new: error in constructor for `%s':\n%s", s->s_name, lua_tostring(__L(), -1)); - lua_pop(__L(), 2); /* pop the error string and the global "pd" */ + mylua_error(__L(), NULL, "constructor"); + lua_pop(__L(), 1); /* pop the global "pd" */ return NULL; } else @@ -661,8 +722,7 @@ static void pdlua_free( t_pdlua *o /**< The object to destruct. */) lua_pushlightuserdata(__L(), o); if (lua_pcall(__L(), 1, 0, 0)) { - pd_error(NULL, "lua: error in destructor:\n%s", lua_tostring(__L(), -1)); - lua_pop(__L(), 1); /* pop the error string */ + mylua_error(__L(), NULL, "destructor"); } lua_pop(__L(), 1); /* pop the global "pd" */ PDLUA_DEBUG("pdlua_free: end. stack top %d", lua_gettop(__L())); @@ -760,18 +820,11 @@ static int pdlua_click(t_gobj *z, t_glist *gl, int xpos, int ypos, int shift, in x->gfx.mouse_down = doit; return 1; - } + } else #endif return text_widgetbehavior.w_clickfn(z, gl, xpos, ypos, shift, alt, dbl, doit); } -// The _reload method will tell the pdlua object to reload the original script that spawned it -// This is used in plugdata for dynamic reloading, but can be useful in other environments too -// Prefixed with _ to prevent namespace pollution -static void pdlua_reload(t_gobj* z) -{ - pdlua_dispatch((t_pdlua *)z, 0, gensym("_reload"), 0, NULL); -} static void pdlua_displace(t_gobj *z, t_glist *glist, int dx, int dy){ t_pdlua *x = (t_pdlua *)z; @@ -893,6 +946,10 @@ static void pdlua_stack_dump (lua_State *L) } #endif +#ifdef WIN32 +#define realpath(N,R) _fullpath((R),(N),PATH_MAX) +#endif + /* nw.js support. If this is non-NULL then we're running inside Jonathan Wilkes' Pd-L2Ork variant and access to the GUI uses JavaScript. */ static void (*nw_gui_vmess)(const char *sel, char *fmt, ...) = NULL; @@ -907,35 +964,17 @@ void plugdata_forward_message(void* x, t_symbol *s, int argc, t_atom *argv); /** Here we find the lua code for the object and open it in an editor */ static void pdlua_menu_open(t_pdlua *o) { - #if PLUGDATA - // This is a more reliable method of finding out what file an object came from - // TODO: we might also want to use something like this for pd-vanilla? - lua_getglobal(__L(), "pd"); - lua_getfield(__L(), -1, "_whereami"); - lua_pushstring(__L(), o->pd.te_pd->c_name->s_name); - - if (lua_pcall(__L(), 1, 1, 0)) - { - pd_error(NULL, "lua: error in whereami:\n%s", lua_tostring(__L(), -1)); - lua_pop(__L(), 2); /* pop the error string and the global "pd" */ - return; - } - if(lua_isstring(__L(), -1)) { - const char* fullpath = luaL_checkstring(__L(), -1); - if(fullpath) { - t_atom arg; - SETSYMBOL(&arg, gensym(fullpath)); - plugdata_forward_message(o, gensym("open_textfile"), 1, &arg); - } - return; - } -#endif - const char *name; const char *path; char pathname[FILENAME_MAX]; t_class *class; + /* 20240903 ag: This is surpringly complicated, because there are various + cases to consider. First, whoami gives us the script name, and + externdir its path. In rare cases the path may be relative to + pdlua_cwd, we account for that here. In most cases this will give us + the absolute pathname of the script. The only exception is pdluax, + where the name already includes the full path, so we just use that. */ PDLUA_DEBUG("pdlua_menu_open stack top is %d", lua_gettop(__L())); /** Get the scriptname of the object */ lua_getglobal(__L(), "pd"); @@ -943,32 +982,47 @@ static void pdlua_menu_open(t_pdlua *o) lua_pushlightuserdata(__L(), o); if (lua_pcall(__L(), 1, 1, 0)) { - pd_error(NULL, "lua: error in whoami:\n%s", lua_tostring(__L(), -1)); - lua_pop(__L(), 2); /* pop the error string and the global "pd" */ + mylua_error(__L(), NULL, "whoami"); + lua_pop(__L(), 1); /* pop the global "pd" */ return; } name = luaL_checkstring(__L(), -1); PDLUA_DEBUG3("pdlua_menu_open: L is %p, name is %s stack top is %d", __L(), name, lua_gettop(__L())); - if (name) + if (name && *name) // `pdluax` without argument gives empty script name { - lua_getglobal(__L(), "pd"); - lua_getfield(__L(), -1, "_get_class"); - lua_pushlightuserdata(__L(), o); - if (lua_pcall(__L(), 1, 1, 0)) - { - pd_error(NULL, "lua: error in get_class:\n%s", lua_tostring(__L(), -1)); - lua_pop(__L(), 4); /* pop the error string, global "pd", name, global "pd"*/ + class = o->class; + if (!class) { + lua_pop(__L(), 2); /* pop name, global "pd"*/ return; } - class = (t_class *)lua_touserdata(__L(), -1); #if PLUGDATA if (!*class->c_externdir->s_name) path = pdlua_datadir; else #endif path = class->c_externdir->s_name; - snprintf(pathname, FILENAME_MAX-1, "%s/%s", path, name); - lua_pop(__L(), 4); /* pop class, global "pd", name, global "pd"*/ + if (sys_isabsolutepath(name)) { + // pdluax returns an absolute path for its script, just use that. + snprintf(pathname, FILENAME_MAX-1, "%s", name); + } else if (sys_isabsolutepath(path)) { + // If the externdir is an absolute path, just use it, no questions + // asked. This should cover most cases. + snprintf(pathname, FILENAME_MAX-1, "%s/%s", path, name); + } else { + // Normally, the externdir of an object should be absolute, but if + // it isn't, it should be relative to the cwd we recorded at + // startup time. + char buf[PATH_MAX+1], real_path[PATH_MAX+1], *s = buf; + if (*path) + snprintf(s, PATH_MAX, "%s/%s/%s", pdlua_cwd, path, name); + else + snprintf(s, PATH_MAX, "%s/%s", pdlua_cwd, name); + // canonicalize + if (realpath(s, real_path)) s = real_path; + snprintf(pathname, FILENAME_MAX-1, "%s", s); + } + //post("path = %s, name = %s, pathname = %s", path, name, pathname); + lua_pop(__L(), 2); /* pop name, global "pd"*/ #if PD_MAJOR_VERSION==0 && PD_MINOR_VERSION<43 post("Opening %s for editing", pathname); @@ -985,6 +1039,8 @@ static void pdlua_menu_open(t_pdlua *o) else sys_vgui("::pd_menucommands::menu_openfile {%s}\n", pathname); #endif + } else { + lua_pop(__L(), 2); /* pop name, global "pd"*/ } PDLUA_DEBUG("pdlua_menu_open end. stack top is %d", lua_gettop(__L())); } @@ -1012,18 +1068,25 @@ static t_int *pdlua_perform(t_int *w){ if (lua_pcall(__L(), 1 + o->siginlets, o->sigoutlets, 0)) { - pd_error(o, "pdlua: error in perform:\n%s", lua_tostring(__L(), -1)); - lua_pop(__L(), 2); /* pop the error string and global pd */ + mylua_error(__L(), o, "perform"); + lua_pop(__L(), 1); /* pop the global pd */ return w + o->siginlets + o->sigoutlets + 3; } if (!lua_istable(__L(), -1)) { - const char *s = "pdlua: 'perform' function should return"; - if (o->sigoutlets == 1) - pd_error(o, "%s %s", s, "a table"); - else if (o->sigoutlets > 1) - pd_error(o, "%s %d %s", s, o->sigoutlets, "tables"); + const char *s = "lua: perform: function should return"; + if (o->sigoutlets == 1) { + if (!o->sig_warned) { + pd_error(o, "%s %s", s, "a table"); + o->sig_warned = 1; + } + } else if (o->sigoutlets > 1) { + if (!o->sig_warned) { + pd_error(o, "%s %d %s", s, o->sigoutlets, "tables"); + o->sig_warned = 1; + } + } lua_pop(__L(), 1 + o->sigoutlets); return w + o->siginlets + o->sigoutlets + 3; } @@ -1056,6 +1119,7 @@ static t_int *pdlua_perform(t_int *w){ static void pdlua_dsp(t_pdlua *x, t_signal **sp){ int sum = x->siginlets + x->sigoutlets; if(sum == 0) return; + x->sig_warned = 0; PDLUA_DEBUG("pdlua_dsp: stack top %d", lua_gettop(__L())); lua_getglobal(__L(), "pd"); @@ -1066,8 +1130,7 @@ static void pdlua_dsp(t_pdlua *x, t_signal **sp){ if (lua_pcall(__L(), 3, 0, 0)) { - pd_error(x, "pdlua: error in dsp:\n%s", lua_tostring(__L(), -1)); - lua_pop(__L(), 1); /* pop the error string */ + mylua_error(__L(), x, "dsp"); } lua_pop(__L(), 1); /* pop the global "pd" */ @@ -1086,18 +1149,69 @@ static void pdlua_dsp(t_pdlua *x, t_signal **sp){ freebytes(sigvec, sigvecsize * sizeof(t_int)); } +static int pdlua_get_arguments(lua_State *L) +{ + // Check if the first argument is a valid user data pointer + char msg[MAXPDSTRING]; + if (lua_islightuserdata(L, 1)) + { + // Retrieve the userdata pointer + t_pdlua *o = lua_touserdata(L, 1); + if (!o) { + pd_error(NULL, "%s: get_args: null object", src_info(L, msg)); + return 0; + } + + // Retrieve the binbuf + t_binbuf* b = o->pd.te_binbuf; + + if (!b) { + pd_error(o, "%s: get_args: null arguments", src_info(L, msg)); + return 0; + } + lua_newtable(L); + char buf[MAXPDSTRING]; + const t_atom *ap; + int indx = binbuf_getnatom(b), i = 0; + for (ap = binbuf_getvec(b); indx--; ap++, i++) { + if (i == 0) continue; // skip 1st atom = object name + lua_pushnumber(L, i); + if (ap->a_type == A_FLOAT) + lua_pushnumber(L, ap->a_w.w_float); + else { + atom_string(ap, buf, MAXPDSTRING); + lua_pushstring(L, buf); + } + lua_settable(L, -3); + } + return 1; + } else { + pd_error(NULL, "%s: get_args: missing object", src_info(L, msg)); + } + + return 0; +} + static int pdlua_set_arguments(lua_State *L) { // Check if the first argument is a valid user data pointer + char msg[MAXPDSTRING]; if (lua_islightuserdata(L, 1)) { // Retrieve the userdata pointer t_pdlua *o = lua_touserdata(L, 1); + if (!o) { + pd_error(NULL, "%s: set_args: null object", src_info(L, msg)); + return 0; + } // Retrieve the binbuf t_binbuf* b = o->pd.te_binbuf; - if (!b) return 0; + if (!b) { + pd_error(o, "%s: set_args: null arguments", src_info(L, msg)); + return 0; + } t_atom name; SETSYMBOL(&name, atom_getsymbol(binbuf_getvec(b))); @@ -1107,6 +1221,11 @@ static int pdlua_set_arguments(lua_State *L) // Check if the second argument is a table if (lua_istable(L, 2)) { + // check whether we need to redraw the object + t_text *x = (t_text*)o; + int redraw = gobj_shouldvis(&o->pd.te_g, o->canvas) && + glist_isvisible(o->canvas); + // Get the number of elements in the table int argc = lua_rawlen(L, 2); @@ -1124,24 +1243,43 @@ static int pdlua_set_arguments(lua_State *L) binbuf_add(b, 1, &atom); } else if (lua_isstring(L, -1)) { - // If it's a string, convert it to a symbol and add to binbuf + // If it's a string, parse it using binbuf_text and add the resulting atoms to the binbuf const char* str = lua_tostring(L, -1); - t_atom atom; - SETSYMBOL(&atom, gensym(str)); - binbuf_add(b, 1, &atom); + t_binbuf *temp = binbuf_new(); + binbuf_text(temp, str, strlen(str)); + binbuf_add(b, binbuf_getnatom(temp), binbuf_getvec(temp)); + binbuf_free(temp); + } else { + pd_error(o, "%s: set_args: atom #%d is neither float nor string", src_info(L, msg), i); } // Pop the value from the stack lua_pop(L, 1); + + if (redraw) { + // update the text in the object box; this makes sure that + // the arguments in the display are what we just set + t_rtext *y = glist_findrtext(o->canvas, x); + rtext_retext(y); + // redraw the object and its iolets (including incident + // cord lines), in case the object box size has changed + gobj_vis(&o->pd.te_g, o->canvas, 0); + gobj_vis(&o->pd.te_g, o->canvas, 1); + canvas_fixlinesfor(o->canvas, x); + } } + } else { + pd_error(o, "%s: set_args: argument must be a table", src_info(L, msg)); } + } else { + pd_error(NULL, "%s: set_args: missing object", src_info(L, msg)); } return 0; } -t_widgetbehavior pdlua_widgetbehavior; +static t_widgetbehavior pdlua_widgetbehavior; /** Lua class registration. This is equivalent to the "setup" method for an ordinary Pd class */ static int pdlua_class_new(lua_State *L) @@ -1152,50 +1290,70 @@ static int pdlua_class_new(lua_State *L) * \li \c 1 Pd class pointer. * */ { - const char *name; - t_class *c; + const char *name, name_gfx[MAXPDSTRING]; + t_class *c, *c_gfx = NULL; name = luaL_checkstring(L, 1); + if (!name || !*name) { + // fail silently, return nothing + return 0; + } + snprintf(name_gfx, MAXPDSTRING-1, "%s:gfx", name); PDLUA_DEBUG3("pdlua_class_new: L is %p, name is %s stack top is %d", L, name, lua_gettop(L)); c = class_new(gensym((char *) name), (t_newmethod) pdlua_new, (t_method) pdlua_free, sizeof(t_pdlua), CLASS_NOINLET, A_GIMME, 0); + if (strcmp(name, "pdlua") && strcmp(name, "pdluax")) { + // Shadow class for graphics objects. This is an exact clone of the + // regular (non-gui) class, except that it has a different + // widgetbehavior. We only need this for the regular Lua objects, the + // pdlua and pdluax built-ins don't have this. + c_gfx = class_new(gensym((char *) name_gfx), (t_newmethod) pdlua_new, + (t_method) pdlua_free, sizeof(t_pdlua), CLASS_NOINLET, A_GIMME, 0); + class_sethelpsymbol(c_gfx, gensym((char *) name)); + } // Let plugdata know this class is a lua object #if PLUGDATA + // XXXFIXME: @timothyschoen: Not sure whether plugdata needs to know about + // name_gfx, too? plugdata_register_class(name); #endif - // Set custom widgetbehaviour for GUIs - pdlua_widgetbehavior.w_getrectfn = pdlua_getrect; - pdlua_widgetbehavior.w_displacefn = pdlua_displace; -#ifndef PURR_DATA - pdlua_widgetbehavior.w_selectfn = text_widgetbehavior.w_selectfn; -#else - // Purr Data only, this seems to be preferred over w_displacefn and is - // actually needed to make text_widgetbehavior.w_selectfn happy. - pdlua_widgetbehavior.w_displacefnwtag = pdlua_displace_wtag; - // We also do our own variant of text_widgetbehavior.w_selectfn, as the - // text_widgetbehavior won't give the right object tag with a freshly - // created gop for some reason. - pdlua_widgetbehavior.w_selectfn = pdlua_select; -#endif - pdlua_widgetbehavior.w_deletefn = pdlua_delete; - pdlua_widgetbehavior.w_clickfn = pdlua_click; - pdlua_widgetbehavior.w_visfn = pdlua_vis; - pdlua_widgetbehavior.w_activatefn = pdlua_activate; - class_setwidget(c, &pdlua_widgetbehavior); - if (c) { /* a class with a "menu-open" method will have the "Open" item highlighted in the right-click menu */ class_addmethod(c, (t_method)pdlua_menu_open, gensym("menu-open"), A_NULL);/* (mrpeach 20111025) */ - class_addmethod(c, (t_method)pdlua_reload, gensym("_reload"), A_NULL);/* (mrpeach 20111025) */ class_addmethod(c, (t_method)pdlua_dsp, gensym("dsp"), A_CANT, 0); /* timschoen 20240226 */ } -/**/ + + if (c_gfx) { + class_addmethod(c_gfx, (t_method)pdlua_menu_open, gensym("menu-open"), A_NULL); + class_addmethod(c_gfx, (t_method)pdlua_dsp, gensym("dsp"), A_CANT, 0); + + // Set custom widgetbehaviour for GUIs + pdlua_widgetbehavior.w_getrectfn = pdlua_getrect; + pdlua_widgetbehavior.w_displacefn = pdlua_displace; +#ifndef PURR_DATA + pdlua_widgetbehavior.w_selectfn = text_widgetbehavior.w_selectfn; +#else + // Purr Data only, this seems to be preferred over w_displacefn and is + // actually needed to make text_widgetbehavior.w_selectfn happy. + pdlua_widgetbehavior.w_displacefnwtag = pdlua_displace_wtag; + // We also do our own variant of text_widgetbehavior.w_selectfn, as the + // text_widgetbehavior won't give the right object tag with a freshly + // created gop for some reason. + pdlua_widgetbehavior.w_selectfn = pdlua_select; +#endif + pdlua_widgetbehavior.w_deletefn = pdlua_delete; + pdlua_widgetbehavior.w_clickfn = pdlua_click; + pdlua_widgetbehavior.w_visfn = pdlua_vis; + pdlua_widgetbehavior.w_activatefn = pdlua_activate; + class_setwidget(c_gfx, &pdlua_widgetbehavior); + } lua_pushlightuserdata(L, c); + lua_pushlightuserdata(L, c_gfx); PDLUA_DEBUG("pdlua_class_new: end stack top is %d", lua_gettop(L)); - return 1; + return 2; } /** Lua object creation. */ @@ -1211,6 +1369,7 @@ static int pdlua_object_new(lua_State *L) if (lua_islightuserdata(L, 1)) { t_class *c = lua_touserdata(L, 1); + t_class *c_gfx = lua_touserdata(L, 2); if(c) { PDLUA_DEBUG("pdlua_object_new: path is %s", c->c_externdir->s_name); @@ -1224,7 +1383,10 @@ static int pdlua_object_new(lua_State *L) o->out = NULL; o->siginlets = 0; o->sigoutlets = 0; + o->sig_warned = 0; o->canvas = canvas_getcurrent(); + o->class = c; + o->class_gfx = c_gfx; o->gfx.width = 80; o->gfx.height = 80; @@ -1252,11 +1414,82 @@ static int pdlua_object_new(lua_State *L) static int pdlua_object_creategui(lua_State *L) { t_pdlua *o = lua_touserdata(L, 1); + t_text *x = (t_text*)o; + int reinit = lua_tonumber(L, 2); + if (!o->class_gfx) return 0; // we're not supposed to be here... + // We may need to redraw the object in case it's been reloaded, to get the + // iolets and patch cords fixed. + int redraw = reinit && o->pd.te_binbuf && gobj_shouldvis(&o->pd.te_g, o->canvas) && glist_isvisible(o->canvas); + if (redraw) { + gobj_vis(&o->pd.te_g, o->canvas, 0); + } o->has_gui = 1; + // We need to switch classes mid-flight here. This is a bit of a hack, but + // we want to retain the standard text widgetbehavior for regular + // (non-gui) objects. As soon as we create the gui here, we switch over to + // o->class_gfx, which is an exact clone of o->class, except that it has + // our custom widgetbehavior for gui objects. + x->te_pd = o->class_gfx; gfx_initialize(o); + if (redraw) { + // force object and its iolets to be redrawn + gobj_vis(&o->pd.te_g, o->canvas, 1); + canvas_fixlinesfor(o->canvas, x); + } return 0; } +// ag 20240905: This used to be implemented on the Lua side, along with a +// corresponding dispatch method so that the Lua method could be called from +// C. But we don't use that method here anymore, so instead the Lua method +// (which we want to keep around for backward compatibility) now calls this C +// function, where we get the current class (which might be either the regular +// a.k.a. text-based or the gfx shadow class) directly from the horse's mouth. +static int pdlua_get_class(lua_State *L) +{ + t_text *x = lua_touserdata(L, 1); + if (x) { + lua_pushlightuserdata(L, x->te_pd); + return 1; + } + return 0; +} + +/* ag 20240902: We shouldn't use these private data structures, but we have + to, since Pd's public API doesn't provide any means to change an existing + iolet in-place. Which we need to do in order to update an existing pdlua + object in-place. Just recreating the object would loose all existing + connections, which would be much too disruptive for live-coding. */ + +/* Fortunately, the _inlet and _outlet structs have remained stable for a very + VERY long time, and are the same across all existing Pd flavors, so we just + replicate their private declarations here. */ + +union inletunion +{ + // we only need these two variants here + t_symbol *iu_symto; + t_float iu_floatsignalvalue; +}; + +struct _inlet +{ + t_pd i_pd; + struct _inlet *i_next; + t_object *i_owner; + t_pd *i_dest; + t_symbol *i_symfrom; + union inletunion i_un; +}; + +struct _outlet +{ + t_object *o_owner; + struct _outlet *o_next; + t_outconnect *o_connections; + t_symbol *o_sym; +}; + /** Lua object inlet creation. */ static int pdlua_object_createinlets(lua_State *L) /**< Lua interpreter state. @@ -1271,37 +1504,76 @@ static int pdlua_object_createinlets(lua_State *L) { t_pdlua *o = lua_touserdata(L, 1); if(o) { - if (lua_isnumber(L, 2)) { - // If it's a number, it means the number of data inlets - o->inlets = luaL_checknumber(L, 2); - o->proxy_in = malloc(o->inlets * sizeof(t_pdlua_proxyinlet)); - o->in = malloc(o->inlets * sizeof(t_inlet*)); - for (int i = 0; i < o->inlets; ++i) - { - pdlua_proxyinlet_init(&o->proxy_in[i], o, i); - o->in[i] = inlet_new(&o->pd, &o->proxy_in[i].pd, 0, 0); - } - } else if (lua_istable(L, 2)) { - // If it's a table, it means a list of inlet types (data or signal) - o->inlets = lua_rawlen(L, 2); - o->proxy_in = malloc(o->inlets * sizeof(t_pdlua_proxyinlet)); - o->in = malloc(o->inlets * sizeof(t_inlet*)); - for (int i = 0; i < o->inlets; ++i) - { + /* Note that we update the inlets of an existing object in-place + here, without recreating the object. I learned this neat little + trick from Pierre Guillot's pd-faustgen, thanks Pierre! */ + // record the number of old inlets + int old_inlets = o->inlets; + // determine the new number of inlets + int have_number = lua_isnumber(L, 2); + int have_table = lua_istable(L, 2); + int new_inlets = have_number ? luaL_checknumber(L, 2) : + have_table ? lua_rawlen(L, 2) : 0; + if (!have_number && !have_table) return luaL_error(L, "inlets must be a number or a table"); + // need to suspend dsp state here, in case any signal inlets are + // created or destroyed + int dspstate = canvas_suspend_dsp(); + // check whether we need to redraw iolets and cords + int redraw = o->pd.te_binbuf && gobj_shouldvis(&o->pd.te_g, o->canvas) && glist_isvisible(o->canvas); + if (redraw) { + gobj_vis(&o->pd.te_g, o->canvas, 0); + } + // remove all cords of excess inlets, then the inlets themselves + for (int i = new_inlets; i < old_inlets; ++i) { + canvas_deletelinesforio(o->canvas, (t_text*)o, o->in[i], 0); + inlet_free(o->in[i]); + } + // reallocate tables + o->inlets = new_inlets; + o->proxy_in = realloc(o->proxy_in, new_inlets * sizeof(t_pdlua_proxyinlet)); + o->in = realloc(o->in, new_inlets * sizeof(t_inlet*)); + o->siginlets = 0; + // update existing and create new inlets + for (int i = 0; i < new_inlets; ++i) { + int is_signal = 0; + if (have_table) { lua_rawgeti(L, 2, i + 1); // Get element at index i+1 - if (lua_isnumber(L, -1)) { - int is_signal = lua_tonumber(L, -1); - o->siginlets += is_signal; - - pdlua_proxyinlet_init(&o->proxy_in[i], o, i); - o->in[i] = inlet_new(&o->pd, &o->proxy_in[i].pd, is_signal ? &s_signal : 0, is_signal ? &s_signal : 0); - } + if (lua_isnumber(L, -1)) + // this should be a 0-1 flag, convert from a Lua + // number which is some kind of float + is_signal = !!(int)lua_tonumber(L, -1); lua_pop(L, 1); // Pop the value from the stack } - } else { - // Invalid argument type - return luaL_error(L, "inlets must be a number or a table"); + // remove all existing cords with a signature mismatch + int sig_changed = i < old_inlets && is_signal != obj_issignalinlet(&o->pd, i); + if (sig_changed) + canvas_deletelinesforio(o->canvas, (t_text*)o, o->in[i], 0); + o->siginlets += is_signal; + t_symbol *sym = is_signal ? &s_signal : 0; + if (i >= old_inlets) { + // add a new inlet + pdlua_proxyinlet_init(&o->proxy_in[i], o, i); + o->in[i] = inlet_new(&o->pd, &o->proxy_in[i].pd, sym, sym); + } else { + // here's the hacky bit: update an existing inlet in-place + struct _inlet *x = (struct _inlet *)o->in[i]; + // code adapted from inlet_new() + x->i_dest = &o->proxy_in[i].pd; + if (sig_changed) { + if (sym) + x->i_un.iu_floatsignalvalue = 0; + else + x->i_un.iu_symto = sym; + x->i_symfrom = sym; + } + } } + if (redraw) { + // force object and its iolets to be redrawn + gobj_vis(&o->pd.te_g, o->canvas, 1); + canvas_fixlinesfor(o->canvas, (t_text*)o); + } + canvas_resume_dsp(dspstate); } } @@ -1323,37 +1595,52 @@ static int pdlua_object_createoutlets(lua_State *L) t_pdlua *o = lua_touserdata(L, 1); if (o) { - if (lua_isnumber(L, 2)) { - // If it's a number, it means the number of data outlets - o->outlets = luaL_checknumber(L, 2); - if (o->outlets > 0) - { - o->out = malloc(o->outlets * sizeof(t_outlet *)); - for (int i = 0; i < o->outlets; ++i) o->out[i] = outlet_new(&o->pd, 0); + // this is basically the same as above, but for outlets + int old_outlets = o->outlets; + int have_number = lua_isnumber(L, 2); + int have_table = lua_istable(L, 2); + int new_outlets = have_number ? luaL_checknumber(L, 2) : + have_table ? lua_rawlen(L, 2) : 0; + if (!have_number && !have_table) return luaL_error(L, "outlets must be a number or a table"); + int dspstate = canvas_suspend_dsp(); + int redraw = o->pd.te_binbuf && gobj_shouldvis(&o->pd.te_g, o->canvas) && glist_isvisible(o->canvas); + if (redraw) { + gobj_vis(&o->pd.te_g, o->canvas, 0); + } + for (int i = new_outlets; i < old_outlets; ++i) { + canvas_deletelinesforio(o->canvas, (t_text*)o, 0, o->out[i]); + outlet_free(o->out[i]); + } + o->outlets = new_outlets; + o->out = realloc(o->out, new_outlets * sizeof(t_outlet*)); + o->sigoutlets = 0; + for (int i = 0; i < new_outlets; ++i) { + int is_signal = 0; + if (have_table) { + lua_rawgeti(L, 2, i + 1); // Get element at index i+1 + if (lua_isnumber(L, -1)) + is_signal = !!(int)lua_tonumber(L, -1); + lua_pop(L, 1); // Pop the value from the stack } - else o->out = NULL; - } else if (lua_istable(L, 2)) { - // If it's a table, it means a list of outlet types (data or signal) - o->outlets = lua_rawlen(L, 2); - if (o->outlets > 0) - { - o->out = malloc(o->outlets * sizeof(t_outlet *)); - for (int i = 0; i < o->outlets; ++i) - { - lua_rawgeti(L, 2, i + 1); // Get element at index i+1 - if (lua_isnumber(L, -1)) { - int is_signal = lua_tonumber(L, -1); - o->sigoutlets += is_signal; - o->out[i] = outlet_new(&o->pd, is_signal ? &s_signal : 0); - } - lua_pop(L, 1); // Pop the value from the stack - } + int sig_changed = i < old_outlets && is_signal != obj_issignaloutlet(&o->pd, i); + if (sig_changed) + canvas_deletelinesforio(o->canvas, (t_text*)o, 0, o->out[i]); + o->sigoutlets += is_signal; + t_symbol *sym = is_signal ? &s_signal : 0; + if (i >= old_outlets) { + o->out[i] = outlet_new(&o->pd, sym); + } else if (sig_changed) { + struct _outlet *x = (struct _outlet *)o->out[i]; + // code adapted from outlet_new() + x->o_connections = 0; + x->o_sym = sym; } - else o->out = NULL; - } else { - // Invalid argument type - return luaL_error(L, "outlets must be a number or a table"); } + if (redraw) { + gobj_vis(&o->pd.te_g, o->canvas, 1); + canvas_fixlinesfor(o->canvas, (t_text*)o); + } + canvas_resume_dsp(dspstate); } } @@ -1537,6 +1824,21 @@ static int pdlua_clock_free(lua_State *L) return 0; } +// 20240906 ag: Some time utility functions to support the clock functions. + +static int pdlua_systime(lua_State *L) +{ + lua_pushnumber(L, clock_getsystime()); + return 1; +} + +static int pdlua_timesince(lua_State *L) +{ + double systime = luaL_checknumber(L, 1); + lua_pushnumber(L, clock_gettimesince(systime)); + return 1; +} + /** Lua object destruction. */ static int pdlua_object_free(lua_State *L) /**< Lua interpreter state. @@ -1560,7 +1862,7 @@ static int pdlua_object_free(lua_State *L) o->in = NULL; } - if (o->proxy_in) freebytes(o->proxy_in, sizeof(struct pdlua_proxyinlet) * o->inlets); + if (o->proxy_in) free(o->proxy_in); if(o->out) { @@ -1594,8 +1896,7 @@ static void pdlua_dispatch if (lua_pcall(__L(), 4, 0, 0)) { - pd_error(o, "lua: error in dispatcher:\n%s", lua_tostring(__L(), -1)); - lua_pop(__L(), 1); /* pop the error string */ + mylua_error(__L(), o, "dispatcher"); } lua_pop(__L(), 1); /* pop the global "pd" */ @@ -1623,8 +1924,7 @@ static void pdlua_receivedispatch if (lua_pcall(__L(), 3, 0, 0)) { - pd_error(r->owner, "lua: error in receive dispatcher:\n%s", lua_tostring(__L(), -1)); - lua_pop(__L(), 1); /* pop the error string */ + mylua_error(__L(), r->owner, "receive dispatcher"); } lua_pop(__L(), 1); /* pop the global "pd" */ PDLUA_DEBUG("pdlua_receivedispatch: end. stack top %d", lua_gettop(__L())); @@ -1642,8 +1942,7 @@ static void pdlua_clockdispatch( t_pdlua_proxyclock *clock) if (lua_pcall(__L(), 1, 0, 0)) { - pd_error(clock->owner, "lua: error in clock dispatcher:\n%s", lua_tostring(__L(), -1)); - lua_pop(__L(), 1); /* pop the error string */ + mylua_error(__L(), clock->owner, "clock dispatcher"); } lua_pop(__L(), 1); /* pop the global "pd" */ PDLUA_DEBUG("pdlua_clockdispatch: end. stack top %d", lua_gettop(__L())); @@ -1727,7 +2026,13 @@ static t_atom *pdlua_popatomtable } else { - pd_error(o, "lua: error: not a table"); + /* ag 20240907: We must not leave *count uninitialized here, and we + must actually set it to a nonzero value which indicates an error + and that the result is *not* an empty atoms table. */ + *count = 1; + // this error will be flagged in the caller, so no need to add even + // more noise here + //pd_error(o, "lua: error: not a table"); ok = 0; } lua_pop(L, 1); @@ -1755,6 +2060,7 @@ static int pdlua_outlet(lua_State *L) int count; t_atom *atoms; + char msg[MAXPDSTRING]; PDLUA_DEBUG("pdlua_outlet: stack top %d", lua_gettop(L)); if (lua_islightuserdata(L, 1)) { @@ -1764,7 +2070,7 @@ static int pdlua_outlet(lua_State *L) if (lua_isnumber(L, 2)) out = lua_tonumber(L, 2) - 1; /* C has 0.., Lua has 1.. */ else { - pd_error(o, "lua: error: outlet must be a number"); + pd_error(o, "%s: error: outlet index must be a number", src_info(L, msg)); lua_pop(L, 4); /* pop all the arguments */ return 0; } @@ -1776,11 +2082,11 @@ static int pdlua_outlet(lua_State *L) sym = gensym((char *) s); /* const cast */ if (s) { - if (strlen(s) != sl) pd_error(o, "lua: warning: symbol munged (contains \\0 in body)"); + if (strlen(s) != sl) pd_error(o, "%s: warning: selector symbol munged (contains \\0 in body) [outlet %d]", src_info(L, msg), out+1); lua_pushvalue(L, 4); atoms = pdlua_popatomtable(L, &count, o); if (count == 0 || atoms) outlet_anything(o->out[out], sym, count, atoms); - else pd_error(o, "lua: error: no atoms??"); + else pd_error(o, "%s: error: %s atoms table [outlet %d]", src_info(L, msg), lua_isnoneornil(L, 4)?"missing":"invalid", out+1); if (atoms) { free(atoms); @@ -1788,15 +2094,15 @@ static int pdlua_outlet(lua_State *L) return 0; } } - else pd_error(o, "lua: error: null selector"); + else pd_error(o, "%s: error: null selector [outlet %d]", src_info(L, msg), out+1); } - else pd_error(o, "lua: error: selector must be a string"); + else pd_error(o, "%s: error: selector must be a string [outlet %d]", src_info(L, msg), out+1); } - else pd_error(o, "lua: error: outlet out of range"); + else pd_error(o, "%s: error: outlet index out of range [outlet %d]", src_info(L, msg), out+1); } - else pd_error(NULL, "lua: error: no object to outlet from"); + else pd_error(NULL, "%s: error: null object for outlet", src_info(L, msg)); } - else pd_error(NULL, "lua: error: bad arguments to outlet"); + else pd_error(NULL, "%s: error: missing object for outlet", src_info(L, msg)); lua_pop(L, 4); /* pop all the arguments */ PDLUA_DEBUG("pdlua_outlet: end. stack top %d", lua_gettop(L)); return 0; @@ -1821,6 +2127,7 @@ static int pdlua_send(lua_State *L) int count; t_atom *atoms; + char msg[MAXPDSTRING]; PDLUA_DEBUG("pdlua_send: stack top is %d", lua_gettop(L)); if (lua_isstring(L, 1)) { @@ -1828,18 +2135,18 @@ static int pdlua_send(lua_State *L) receivesym = gensym((char *) receivename); /* const cast */ if (receivesym) { - if (strlen(receivename) != receivenamel) pd_error(NULL, "lua: warning: symbol munged (contains \\0 in body)"); + if (strlen(receivename) != receivenamel) pd_error(NULL, "%s: warning: receive symbol munged (contains \\0 in body) [send %s]", src_info(L, msg), receivename); if (lua_isstring(L, 2)) { selname = lua_tolstring(L, 2, &selnamel); selsym = gensym((char *) selname); /* const cast */ if (selsym) { - if (strlen(selname) != selnamel) pd_error(NULL, "lua: warning: symbol munged (contains \\0 in body)"); + if (strlen(selname) != selnamel) pd_error(NULL, "%s: warning: selector symbol munged (contains \\0 in body) [send %s]", src_info(L, msg), receivename); lua_pushvalue(L, 3); atoms = pdlua_popatomtable(L, &count, NULL); if ((count == 0 || atoms) && (receivesym->s_thing)) typedmess(receivesym->s_thing, selsym, count, atoms); - else pd_error(NULL, "lua: error: no atoms??"); + else pd_error(NULL, "%s: error: %s atoms table [send %s]", src_info(L, msg), lua_isnoneornil(L, 3)?"missing":"invalid", receivename); if (atoms) { free(atoms); @@ -1847,13 +2154,13 @@ static int pdlua_send(lua_State *L) return 0; } } - else pd_error(NULL, "lua: error: null selector"); + else pd_error(NULL, "%s: error: null selector [send %s]", src_info(L, msg), receivename); } - else pd_error(NULL, "lua: error: selector must be a string"); + else pd_error(NULL, "%s: error: selector must be a string [send %s]", src_info(L, msg), receivename); } - else pd_error(NULL, "lua: error: null receive name"); + else pd_error(NULL, "%s: error: null receive name in send", src_info(L, msg)); } - else pd_error(NULL, "lua: error: receive name must be string"); + else pd_error(NULL, "%s: error: receive name in send must be string", src_info(L, msg)); PDLUA_DEBUG("pdlua_send: fail end. stack top is %d", lua_gettop(L)); return 0; } @@ -2040,6 +2347,49 @@ static int pdlua_error(lua_State *L) return 0; } +static void pdlua_packagepath(lua_State *L, const char *path) +{ + PDLUA_DEBUG("pdlua_packagepath: stack top %d", lua_gettop(L)); + lua_getglobal(L, "package"); + lua_pushstring(L, "path"); + lua_gettable(L, -2); + const char *packagepath = lua_tostring(L, -1); + char *buf = malloc(2*strlen(path)+20+strlen(packagepath)); + if (!buf) { + lua_pop(L, 2); + return; + } +#ifdef _WIN32 + sprintf(buf, "%s\\?.lua;%s\\?\\init.lua;%s", path, path, packagepath); +#else + sprintf(buf, "%s/?.lua;%s/?/init.lua;%s", path, path, packagepath); +#endif + lua_pop(L, 1); + lua_pushstring(L, "path"); + lua_pushstring(L, buf); + lua_settable(L, -3); + lua_pushstring(L, "cpath"); + lua_gettable(L, -2); + packagepath = lua_tostring(L, -1); + buf = realloc(buf, 2*strlen(path)+20+strlen(packagepath)); + if (!buf) { + lua_pop(L, 2); + return; + } +#ifdef _WIN32 + sprintf(buf, "%s\\?.dll;%s", path, packagepath); +#else + sprintf(buf, "%s/?.so;%s", path, packagepath); +#endif + lua_pop(L, 1); + lua_pushstring(L, "cpath"); + lua_pushstring(L, buf); + lua_settable(L, -3); + lua_pop(L, 1); + free(buf); + PDLUA_DEBUG("pdlua_packagepath: end. stack top %d", lua_gettop(L)); +} + static void pdlua_setrequirepath ( /* FIXME: documentation (is this of any use at all?) */ lua_State *L, @@ -2051,11 +2401,9 @@ static void pdlua_setrequirepath lua_pushstring(L, "_setrequirepath"); lua_gettable(L, -2); lua_pushstring(L, path); - lua_pushstring(L, pdlua_datadir); - if (lua_pcall(L, 2, 0, 0) != 0) + if (lua_pcall(L, 1, 0, 0) != 0) { - pd_error(NULL, "lua: internal error in `pd._setrequirepath': %s", lua_tostring(L, -1)); - lua_pop(L, 1); + mylua_error(L, NULL, "setrequirepath"); } lua_pop(L, 1); PDLUA_DEBUG("pdlua_setrequirepath: end. stack top %d", lua_gettop(L)); @@ -2072,8 +2420,7 @@ static void pdlua_clearrequirepath lua_gettable(L, -2); if (lua_pcall(L, 0, 0, 0) != 0) { - pd_error(NULL, "lua: internal error in `pd._clearrequirepath': %s", lua_tostring(L, -1)); - lua_pop(L, 1); + mylua_error(L, NULL, "clearrequirepath"); } lua_pop(L, 1); PDLUA_DEBUG("pdlua_clearrequirepath: end. stack top %d", lua_gettop(L)); @@ -2105,7 +2452,9 @@ static int pdlua_dofilex(lua_State *L) if (c) { filename = luaL_optstring(L, 2, NULL); - fd = sys_trytoopenone(c->c_externdir->s_name, filename, "", + if (!filename || !*filename) return 0; + const char *path = c->c_externdir->s_name; + fd = trytoopenone(path && *path ? path : pdlua_datadir, filename, "", buf, &ptr, MAXPDSTRING, 1); if (fd >= 0) { @@ -2120,14 +2469,13 @@ static int pdlua_dofilex(lua_State *L) { close(fd); pdlua_clearrequirepath(L); - lua_error(L); + mylua_error(L, NULL, NULL); } else { if (lua_pcall(L, 0, LUA_MULTRET, 0)) { - pd_error(NULL, "lua: error running `%s':\n%s", filename, lua_tostring(L, -1)); - lua_pop(L, 1); + mylua_error(L, NULL, NULL); close(fd); pdlua_clearrequirepath(L); } @@ -2139,11 +2487,11 @@ static int pdlua_dofilex(lua_State *L) } } } - else pd_error(NULL, "lua: error loading `%s': sys_trytoopenone() failed", filename); + else pd_error(NULL, "lua: dofilex: couldn't locate `%s'", filename); } - else pd_error(NULL, "lua: error in class:dofilex() - class is null"); + else pd_error(NULL, "lua: dofilex: null class"); } - else pd_error(NULL, "lua: error in class:dofilex() - object is wrong type"); + else pd_error(NULL, "lua: dofilex: wrong type of object"); lua_pushstring(L, buf); /* return the path as well so we can open it later with pdlua_menu_open() */ PDLUA_DEBUG("pdlua_dofilex end. stack top is %d", lua_gettop(L)); @@ -2176,6 +2524,7 @@ static int pdlua_dofile(lua_State *L) if (o) { filename = luaL_optstring(L, 2, NULL); + if (!filename || !*filename) return 0; fd = canvas_open(o->canvas, filename, "", buf, &ptr, MAXPDSTRING, 1); if (fd >= 0) { @@ -2191,14 +2540,13 @@ static int pdlua_dofile(lua_State *L) { close(fd); pdlua_clearrequirepath(L); - lua_error(L); + mylua_error(L, o, NULL); } else { if (lua_pcall(L, 0, LUA_MULTRET, 0)) { - pd_error(o, "lua: error running `%s':\n%s", filename, lua_tostring(L, -1)); - lua_pop(L, 1); + mylua_error(L, NULL, NULL); close(fd); pdlua_clearrequirepath(L); } @@ -2210,17 +2558,34 @@ static int pdlua_dofile(lua_State *L) } } } - else pd_error(o, "lua: error loading `%s': canvas_open() failed", filename); + else pd_error(o, "lua: dofile: couldn't locate `%s'", filename); } - else pd_error(NULL, "lua: error in object:dofile() - object is null"); + else pd_error(NULL, "lua: dofile: null object"); } - else pd_error(NULL, "lua: error in object:dofile() - object is wrong type"); + else pd_error(NULL, "lua: dofile: wrong type of object"); lua_pushstring(L, buf); /* return the path as well so we can open it later with pdlua_menu_open() */ PDLUA_DEBUG("pdlua_dofile end. stack top is %d", lua_gettop(L)); return lua_gettop(L) - n; } +static int pdlua_canvas_realizedollar(lua_State *L) +{ + if (lua_islightuserdata(L, 1) && lua_isstring(L, 2)) + { + t_pdlua *o = lua_touserdata(L, 1); + if (o && o->canvas) + { + const char *sym_name = lua_tostring(L, 2); + t_symbol *s = gensym(sym_name); + t_symbol *result = canvas_realizedollar(o->canvas, s); + lua_pushstring(L, result->s_name); + return 1; + } + } + return 0; +} + /** Initialize the pd API for Lua. */ static void pdlua_init(lua_State *L) /**< Lua interpreter state. */ @@ -2238,6 +2603,9 @@ static void pdlua_init(lua_State *L) lua_pushstring(L, "_register"); lua_pushcfunction(L, pdlua_class_new); lua_settable(L, -3); + lua_pushstring(L, "_get_class"); + lua_pushcfunction(L, pdlua_get_class); + lua_settable(L, -3); lua_pushstring(L, "_create"); lua_pushcfunction(L, pdlua_object_new); lua_settable(L, -3); @@ -2310,12 +2678,33 @@ static void pdlua_init(lua_State *L) lua_pushstring(L, "post"); lua_pushcfunction(L, pdlua_post); lua_settable(L, -3); + lua_pushstring(L, "_get_args"); + lua_pushcfunction(L, pdlua_get_arguments); + lua_settable(L, -3); lua_pushstring(L, "_set_args"); lua_pushcfunction(L, pdlua_set_arguments); lua_settable(L, -3); + lua_pushstring(L, "_canvas_realizedollar"); + lua_pushcfunction(L, pdlua_canvas_realizedollar); + lua_settable(L, -3); lua_pushstring(L, "_error"); lua_pushcfunction(L, pdlua_error); lua_settable(L, -3); + /* 20240906 ag: Added TIMEUNITPERMSEC, systime and timesince, to make + clock_set useable. NOTE: TIMEUNITPERMSEC is the time unit for systime, + timesince, and clock_set and is from m_sched.c. It isn't in the Pd + headers anywhere, but its value has been the same forever, so we just + include it here and expose it in the Lua API. */ +#define TIMEUNITPERMSEC (32. * 441.) + lua_pushstring(L, "TIMEUNITPERMSEC"); + lua_pushnumber(L, TIMEUNITPERMSEC); + lua_settable(L, -3); + lua_pushstring(L, "systime"); + lua_pushcfunction(L, pdlua_systime); + lua_settable(L, -3); + lua_pushstring(L, "timesince"); + lua_pushcfunction(L, pdlua_timesince); + lua_settable(L, -3); lua_pop(L, 1); PDLUA_DEBUG("pdlua_init: end. stack top is %d", lua_gettop(L)); } @@ -2334,14 +2723,16 @@ static int pdlua_loader_fromfd class_set_extern_dir(gensym(dirbuf)); pdlua_setrequirepath(__L(), dirbuf); reader.fd = fd; + // we want to have the filename with extension as the name of the chunk + char filename[MAXPDSTRING]; + snprintf(filename, MAXPDSTRING-1, "%s.pd_lua", name); #if LUA_VERSION_NUM < 502 - if (lua_load(__L(), pdlua_reader, &reader, name) || lua_pcall(__L(), 0, 0, 0)) + if (lua_load(__L(), pdlua_reader, &reader, filename) || lua_pcall(__L(), 0, 0, 0)) #else // 5.2 style - if (lua_load(__L(), pdlua_reader, &reader, name, NULL) || lua_pcall(__L(), 0, 0, 0)) + if (lua_load(__L(), pdlua_reader, &reader, filename, NULL) || lua_pcall(__L(), 0, 0, 0)) #endif // LUA_VERSION_NUM < 502 { - pd_error(NULL, "lua: error loading `%s':\n%s", name, lua_tostring(__L(), -1)); - lua_pop(__L(), 1); + mylua_error(__L(), NULL, NULL); pdlua_clearrequirepath(__L()); class_set_extern_dir(&s_); PDLUA_DEBUG("pdlua_loader: script error end. stack top %d", lua_gettop(__L())); @@ -2432,7 +2823,7 @@ static int pdlua_loader_pathwise /* ag: Try loading /.pd_lua (experimental). sys_trytoopenone will correctly find the file in a subdirectory if a path is given, and it will then return that subdir in dirbuf. */ - if ((fd = sys_trytoopenone(path, objectname, ".pd_lua", + if ((fd = trytoopenone(path, objectname, ".pd_lua", dirbuf, &ptr, MAXPDSTRING, 1)) >= 0) if(pdlua_loader_wrappath(fd, objectname, dirbuf)) return 1; @@ -2443,7 +2834,7 @@ static int pdlua_loader_pathwise strcat(filename, "/"); strncat(filename, classname, MAXPDSTRING-strlen(filename)); filename[MAXPDSTRING-1] = 0; - if ((fd = sys_trytoopenone(path, filename, ".pd_lua", + if ((fd = trytoopenone(path, filename, ".pd_lua", dirbuf, &ptr, MAXPDSTRING, 1)) >= 0) if(pdlua_loader_wrappath(fd, objectname, dirbuf)) return 1; @@ -2509,7 +2900,7 @@ void pdlua_setup(void) #endif if (strlen(pdlua_version) == 0) { // NOTE: This should be set from the Makefile, otherwise we fall back to: - pdlua_version = "0.12.6"; + pdlua_version = "0.12.18"; } snprintf(pdluaver, MAXPDSTRING-1, "pdlua %s (GPL) 2008 Claude Heiland-Allen, 2014 Martin Peach et al.", pdlua_version); snprintf(compiled, MAXPDSTRING-1, "pdlua: compiled for pd-%d.%d on %s", @@ -2570,8 +2961,17 @@ void pdlua_setup(void) // external dir in /pdlua. snprintf(pdlua_datadir, MAXPDSTRING-1, "%s/pdlua", datadir); #else - snprintf(pdlua_datadir, MAXPDSTRING-1, "%s", pdlua_proxyinlet_class->c_externdir->s_name); + const char *s = pdlua_proxyinlet_class->c_externdir->s_name; + if (!sys_isabsolutepath(s)) { + // try to turn this into an absolute path + char real_path[PATH_MAX+1]; + if (realpath(s, real_path)) s = real_path; + } + snprintf(pdlua_datadir, MAXPDSTRING-1, "%s", s); #endif + if (!getcwd(pdlua_cwd, MAXPDSTRING)) + // if we can't get the cwd, this is the best that we can do + strcpy(pdlua_cwd, "."); snprintf(pd_lua_path, MAXPDSTRING-1, "%s/pd.lua", pdlua_datadir); /* the full path to pd.lua */ PDLUA_DEBUG("pd_lua_path %s", pd_lua_path); fd = open(pd_lua_path, O_RDONLY); @@ -2582,6 +2982,12 @@ void pdlua_setup(void) if (fd >= 0) { /* pd.lua was opened */ reader.fd = fd; + // We need to set up Lua's package.path here so that pdx.lua can be + // found (and possibly other pre-loaded extension modules in the + // future). Note that we can't just use pdlua_setrequirepath() here + // because it calls pd._setrequirepath in pd.lua which isn't loaded + // yet at this point. + pdlua_packagepath(__L(), pdlua_datadir); #if LUA_VERSION_NUM < 502 result = lua_load(__L(), pdlua_reader, &reader, "pd.lua"); #else // 5.2 style @@ -2596,10 +3002,9 @@ void pdlua_setup(void) if (0 != result) //if (lua_load(__L(), pdlua_reader, &reader, "pd.lua") || lua_pcall(__L(), 0, 0, 0)) { - pd_error(NULL, "lua: error loading `pd.lua':\n%s", lua_tostring(__L(), -1)); + mylua_error(__L(), NULL, NULL); pd_error(NULL, "lua: loader will not be registered!"); pd_error(NULL, "lua: (is `pd.lua' in Pd's path list?)"); - lua_pop(__L(), 1); } else { diff --git a/pdlua.h b/pdlua.h index e5c579b..d403856 100644 --- a/pdlua.h +++ b/pdlua.h @@ -40,7 +40,7 @@ typedef struct _pdlua_gfx char* current_layer_tag; gfx_transform* transforms; int num_transforms; - char current_color[8]; // Keep track of current color + char current_color[10]; // Keep track of current color // Variables to keep track of mouse button state and drag position int mouse_drag_x, mouse_drag_y, mouse_down; @@ -55,17 +55,20 @@ typedef struct _pdlua_gfx /** Pd object data. */ typedef struct pdlua { - t_object pd; /**< We are a Pd object. */ - int inlets; /**< Number of inlets. */ - struct pdlua_proxyinlet *proxy_in; /**< The inlets themselves. */ + t_object pd; // We are a Pd object. + int inlets; // Number of inlets. + struct pdlua_proxyinlet *proxy_in; // The inlets themselves. t_inlet **in; - int outlets; /**< Number of outlets. */ - t_outlet **out; /**< The outlets themselves. */ - int siginlets; /**< Number of signal inlets. */ - int sigoutlets; /**< Number of signal outlets. */ - t_canvas *canvas; /**< The canvas that the object was created on. */ - int has_gui; /**< True if graphics are enabled. */ - t_pdlua_gfx gfx; /**< Holds state for graphics. */ + int outlets; // Number of outlets. + t_outlet **out; // The outlets themselves. + int siginlets; // Number of signal inlets. + int sigoutlets; // Number of signal outlets. + int sig_warned; // Flag for perform signal errors. + t_canvas *canvas; // The canvas that the object was created on. + int has_gui; // True if graphics are enabled. + t_pdlua_gfx gfx; // Holds state for graphics. + t_class *class; // Holds our class pointer. + t_class *class_gfx; // Holds our gfx class pointer. } t_pdlua; lua_State* __L(); diff --git a/pdlua/tutorial/examples/bar~.pd_lua b/pdlua/tutorial/examples/bar~.pd_lua new file mode 100644 index 0000000..2f104e7 --- /dev/null +++ b/pdlua/tutorial/examples/bar~.pd_lua @@ -0,0 +1,43 @@ +local bar = pd.Class:new():register("bar~") + +function bar:initialize(sel, atoms) + self.outlets = {SIGNAL} + self.phase = 0 + self.cycle = 0 + self.freq = 233 + return true +end + +function bar:dsp(samplerate, blocksize) + self.samplerate = samplerate + self.blocksize = blocksize +end + +function bar:random_walk(dmin, dmax) + self.cycle = self.cycle + 1 + if self.cycle > 100 then + self.freq = self.freq + math.random(-1, 1)*math.random(dmin, dmax) + self.freq = math.min(600, math.max(150, self.freq)) + self.cycle = 0 + end +end + +function bar:perform() + -- random frequency change + local dmin, dmax = 30, 70 + self:random_walk(dmin, dmax) + local freq, a, b = self.freq, 0.4, 0.08 + local out = {} -- result table + -- random phase offset + local d = b * math.random() + for i = 1, self.blocksize do + -- phase in radians + local x = 2 * math.pi * (self.phase + d) + out[i] = a * math.sin(x) + self.phase = self.phase + freq / self.samplerate + if self.phase >= 1 then + self.phase = self.phase - 1 + end + end + return out +end diff --git a/pdlua/tutorial/examples/dial.pd b/pdlua/tutorial/examples/dial.pd index 8612693..5334f06 100644 --- a/pdlua/tutorial/examples/dial.pd +++ b/pdlua/tutorial/examples/dial.pd @@ -1,4 +1,5 @@ -#N canvas 433 315 876 306 12; +#N canvas 543 350 734 300 12; +#X declare -lib pdlua; #X obj 40 50 dial; #X floatatom 40 10 5 0 0 0 - phase -, f 5; #X obj 200 70 tgl 15 0 empty empty empty 17 7 0 10 #fcfcfc #000000 @@ -6,35 +7,31 @@ #X text 200 40 clock; #X obj 200 94 metro 1000; #X floatatom 200 152 5 0 0 0 - - -, f 5; -#X msg 200 210 0; -#X obj 200 180 s phase; -#X text 400 40 random speedometer; -#X obj 400 70 tgl 15 0 empty empty empty 17 7 0 10 #fcfcfc #000000 +#X msg 150 210 0; +#X obj 200 260 s phase; +#X text 350 40 random speedometer; +#X obj 350 70 tgl 15 0 empty empty empty 17 7 0 10 #fcfcfc #000000 #000000 0 1; -#X floatatom 400 152 5 0 0 0 - - -, f 5; -#X obj 400 239 line; -#X obj 400 274 s phase; -#X obj 400 123 expr phase = random(-100 \, 100)/300.; -#X msg 400 210 \$1 100; -#X obj 400 94 metro 100; -#X obj 200 123 expr phase = phase + 1/60.; -#X obj 200 240 t f f; +#X floatatom 350 152 5 0 0 0 - - -, f 5; +#X obj 350 219 line; +#X obj 350 264 s phase; +#X msg 350 190 \$1 100; +#X obj 350 94 metro 100; #X obj 20 12 bng 15 250 50 0 empty empty empty 17 7 0 10 #fcfcfc #000000 #000000; #X floatatom 40 210 5 0 0 0 - - -, f 5; -#X obj 40 274 v phase; -#X obj 40 238 change; -#X obj 680 70 adc~; -#X obj 740 70 tgl 15 0 empty empty empty 17 7 0 10 #fcfcfc #000000 +#X obj 40 264 v phase; +#X obj 570 70 adc~; +#X obj 630 70 tgl 15 0 empty empty empty 17 7 0 10 #fcfcfc #000000 #000000 0 1; -#X obj 680 274 s phase; -#X text 680 40 dB meter; -#X obj 680 123 env~; -#X obj 680 99 *~ 0.5; -#X obj 680 186 expr ($f1-50)/100; -#X floatatom 680 152 5 0 0 0 - - -, f 5; -#X text 700 206 0..100 dB -> -0.5..+0.5 phase, f 16; -#X msg 740 94 \; pd dsp \$1; +#X obj 570 264 s phase; +#X text 570 40 dB meter; +#X obj 570 123 env~; +#X obj 570 99 *~ 0.5; +#X obj 570 186 expr ($f1-50)/100; +#X floatatom 570 152 5 0 0 0 - - -, f 5; +#X text 590 206 0..100 dB -> -0.5..+0.5 phase, f 16; +#X msg 630 94 \; pd dsp \$1; #N canvas 767 395 479 405 colors-and-size 0; #X text 120 70 black on white; #X text 120 100 white on black; @@ -74,29 +71,29 @@ color (face), f 40; #X connect 22 0 9 0; #X restore 90 10 pd colors-and-size; #X text 230 10 <--- click to open; -#X connect 0 0 19 0; +#X obj 200 123 expr phase + 1/60.; +#X obj 350 123 expr random(-100 \, 100)/300.; +#X obj 570 10 declare -lib pdlua; +#X connect 0 0 16 0; #X connect 1 0 0 0; #X connect 2 0 4 0; -#X connect 4 0 16 0; +#X connect 4 0 30 0; #X connect 5 0 7 0; -#X connect 6 0 17 0; -#X connect 9 0 15 0; -#X connect 10 0 14 0; +#X connect 6 0 7 0; +#X connect 9 0 14 0; +#X connect 10 0 13 0; #X connect 11 0 12 0; -#X connect 13 0 10 0; -#X connect 14 0 11 0; -#X connect 15 0 13 0; -#X connect 16 0 5 0; -#X connect 17 0 20 0; -#X connect 17 1 12 0; -#X connect 18 0 0 0; -#X connect 19 0 21 0; -#X connect 21 0 20 0; -#X connect 22 0 27 0; -#X connect 22 1 27 0; -#X connect 23 0 31 0; -#X connect 26 0 29 0; -#X connect 27 0 26 0; -#X connect 28 0 24 0; -#X connect 29 0 28 0; -#X connect 32 0 0 0; +#X connect 13 0 11 0; +#X connect 14 0 31 0; +#X connect 15 0 0 0; +#X connect 16 0 17 0; +#X connect 18 0 23 0; +#X connect 18 1 23 0; +#X connect 19 0 27 0; +#X connect 22 0 25 0; +#X connect 23 0 22 0; +#X connect 24 0 20 0; +#X connect 25 0 24 0; +#X connect 28 0 0 0; +#X connect 30 0 5 0; +#X connect 31 0 10 0; diff --git a/pdlua/tutorial/examples/foo~.pd_lua b/pdlua/tutorial/examples/foo~.pd_lua new file mode 100644 index 0000000..cd43b72 --- /dev/null +++ b/pdlua/tutorial/examples/foo~.pd_lua @@ -0,0 +1,42 @@ +local foo = pd.Class:new():register("foo~") + +function foo:initialize(sel, atoms) + self.outlets = {SIGNAL} + self.phase = 0 + self.cycle = 0 + self.freq = 133 + return true +end + +function foo:dsp(samplerate, blocksize) + self.samplerate = samplerate + self.blocksize = blocksize +end + +function foo:random_walk(dmin, dmax) + self.cycle = self.cycle + 1 + if self.cycle > 100 then + self.freq = self.freq + math.random(-1, 1)*math.random(dmin, dmax) + self.freq = math.min(1000, math.max(150, self.freq)) + self.cycle = 0 + end +end + +function foo:perform() + local dmin, dmax = 50, 100 + self:random_walk(dmin, dmax) + local freq, a, b = self.freq, 0.3, 0.1 + local out = {} -- result table + -- random phase offset + local d = b * math.random() + for i = 1, self.blocksize do + -- phase in radians + local x = 2 * math.pi * (self.phase + d) + out[i] = a * math.sin(x) + self.phase = self.phase + freq / self.samplerate + if self.phase >= 1 then + self.phase = self.phase - 1 + end + end + return out +end diff --git a/pdlua/tutorial/examples/live-xfade.pd b/pdlua/tutorial/examples/live-xfade.pd new file mode 100644 index 0000000..5716b34 --- /dev/null +++ b/pdlua/tutorial/examples/live-xfade.pd @@ -0,0 +1,36 @@ +#N canvas 430 336 450 337 12; +#X declare -stdpath pdlua; +#X declare -lib pdlua; +#N canvas 0 0 450 300 (subpatch) 0; +#X array scope 1000 float 0 black black; +#X coords 0 1 999 -1 200 140 1; +#X restore 230 160 graph; +#X obj 130 236 tgl 15 1 empty empty empty 17 7 0 10 #fcfcfc #000000 +#000000 1 1; +#X obj 130 260 metro 100; +#X obj 20 200 luaxfade~; +#X obj 20 80 tgl 15 0 empty empty empty 17 7 0 10 #fcfcfc #000000 #000000 +0 1; +#X obj 20 20 r pdluax; +#X obj 20 300 tabwrite~ scope; +#X obj 20 240 output~; +#X obj 230 50 declare -stdpath pdlua; +#X obj 230 111 pd-remote; +#X msg 230 81 pdluax reload; +#X obj 20 49 route reload; +#X obj 20 150 foo~; +#X obj 80 150 bar~; +#X msg 20 109 fade \$1 100 0; +#X obj 230 20 declare -lib pdlua; +#X connect 1 0 2 0; +#X connect 2 0 6 0; +#X connect 3 0 6 0; +#X connect 3 0 7 0; +#X connect 3 0 7 1; +#X connect 4 0 14 0; +#X connect 5 0 11 0; +#X connect 10 0 9 0; +#X connect 11 0 4 0; +#X connect 12 0 3 0; +#X connect 13 0 3 1; +#X connect 14 0 3 0; diff --git a/pdlua/tutorial/examples/localsend.pd b/pdlua/tutorial/examples/localsend.pd new file mode 100644 index 0000000..383c103 --- /dev/null +++ b/pdlua/tutorial/examples/localsend.pd @@ -0,0 +1,20 @@ +#N canvas 580 203 398 197 12; +#X declare -lib pdlua; +#X obj 240 110 tgl 20 0 empty \$0-onoff empty 17 7 0 10 #dfdfdf #000000 +#000000 1 1; +#X floatatom 240 144 5 0 0 0 - - -, f 5; +#X obj 40 120 bng 20 250 50 0 empty empty empty 17 7 0 10 #dfdfdf #000000 +#000000; +#X text 68 119 <-- click me; +#X obj 175 19 declare -lib pdlua; +#X obj 40 154 localsend \$0-onoff; +#X obj 290 110 tgl 20 0 empty onoff empty 17 7 0 10 #dfdfdf #000000 +#000000 0 1; +#X floatatom 290 144 5 0 0 0 - - -, f 5; +#X msg 40 50 sender \\\$0-onoff; +#X msg 40 80 sender onoff; +#X connect 0 0 1 0; +#X connect 2 0 5 0; +#X connect 6 0 7 0; +#X connect 8 0 5 0; +#X connect 9 0 5 0; diff --git a/pdlua/tutorial/examples/localsend.pd_lua b/pdlua/tutorial/examples/localsend.pd_lua new file mode 100644 index 0000000..f4a75c5 --- /dev/null +++ b/pdlua/tutorial/examples/localsend.pd_lua @@ -0,0 +1,23 @@ +local localsend = pd.Class:new():register("localsend") + +function localsend:initialize(sel, atoms) + self.inlets = 1 + -- pass the symbol from the creation argument, + -- which gets automatically expanded here + self.sender = tostring(atoms[1]) + return true +end + +function localsend:in_1_sender(x) + local sendername = tostring(x[1]) + + -- store the original name as argument (like "\$0-foo") + self:set_args({sendername}) + + -- apply the expanded name with the local id + self.sender = self:canvas_realizedollar(sendername) +end + +function localsend:in_1_bang() + pd.send(self.sender, "bang", {}) +end diff --git a/pdlua/tutorial/examples/luarecv.pd b/pdlua/tutorial/examples/luarecv.pd index c19fffc..d0e5145 100644 --- a/pdlua/tutorial/examples/luarecv.pd +++ b/pdlua/tutorial/examples/luarecv.pd @@ -1,19 +1,20 @@ -#N canvas 504 158 546 313 12; +#N canvas 505 158 546 313 12; #X declare -lib pdlua; -#X obj 307 108 bng 20 250 50 0 empty empty empty 17 7 0 10 #dfdfdf #000000 #000000; +#X obj 307 108 bng 20 250 50 0 empty empty empty 17 7 0 10 #dfdfdf +#000000 #000000; #X obj 307 141 luarecv \$0-stuff; #X msg 109 109 99; #X msg 129 146 1 2 3; #X msg 142 176 foo 1 bar 99; #X obj 109 219 s \$0-stuff; -#X floatatom 155 108 5 0 0 0 - - \$0-stuff 0; +#X floatatom 155 108 5 0 0 0 - - #0-stuff, f 5; #X obj 307 175 list prepend set; #X obj 307 209 list trim; #X text 333 106 <-- then here; #X msg 307 243 31; #X obj 293 42 declare -lib pdlua; #X text 195 102 (built in send), f 8; -#X text 107 76 first send values; +#X text 3 146 click here -->; #X connect 0 0 1 0; #X connect 1 0 7 0; #X connect 2 0 5 0; diff --git a/pdlua/tutorial/examples/luatab.pd b/pdlua/tutorial/examples/luatab.pd index d508979..36555e0 100644 --- a/pdlua/tutorial/examples/luatab.pd +++ b/pdlua/tutorial/examples/luatab.pd @@ -1,4 +1,5 @@ -#N canvas 203 479 460 310 12; +#N canvas 205 479 460 310 12; +#X declare -stdpath pdlua; #X declare -lib pdlua; #N canvas 0 0 450 300 (subpatch) 0; #X array wave 1024 float 1 black black; @@ -147,9 +148,10 @@ #X obj 30 220 luatab wave; #X obj 30 249 bng 20 250 50 0 empty empty empty 17 7 0 10 #dfdfdf #000000 #000000; -#X obj 292 49 declare -lib pdlua; -#X obj 292 120 pd-remote; -#X msg 292 90 pdluax reload; +#X obj 270 50 declare -stdpath pdlua; +#X obj 270 121 pd-remote; +#X msg 270 91 pdluax reload; +#X obj 270 270 declare -lib pdlua; #X connect 1 0 2 0; #X connect 2 0 3 0; #X connect 6 0 5 0; diff --git a/pdlua/tutorial/examples/luatab.pd_lua b/pdlua/tutorial/examples/luatab.pd_lua index 6fec07d..de02822 100644 --- a/pdlua/tutorial/examples/luatab.pd_lua +++ b/pdlua/tutorial/examples/luatab.pd_lua @@ -1,15 +1,14 @@ local luatab = pd.Class:new():register("luatab") --- our own pdlua extension, needed for the reload functionality -local pdx = require 'pdx' +--local pdx = require 'pdx' function luatab:initialize(sel, atoms) -- single inlet for the frequency, bang goes to the single outlet when we -- finished generating a new waveform self.inlets = 1 self.outlets = 1 - -- enable the reload callback - pdx.reload(self) + --pdx.reload(self) + --pdx.unreload(self) -- the name of the array/table should be in the 1st creation argument if type(atoms[1]) == "string" then self.tabname = atoms[1] diff --git a/pdlua/tutorial/examples/luaxfade.pd b/pdlua/tutorial/examples/luaxfade.pd index 280b5dc..ac0c9e9 100644 --- a/pdlua/tutorial/examples/luaxfade.pd +++ b/pdlua/tutorial/examples/luaxfade.pd @@ -1,12 +1,13 @@ -#N canvas 562 353 516 321 12; -#X obj 30 140 luaxfade; +#N canvas 563 353 516 321 12; +#X declare -lib pdlua; +#X obj 30 140 luaxfade~; #X floatatom 110 51 5 0 1 0 - - -, f 5; #N canvas 0 50 450 250 (subpatch) 0; #X array scope 4800 float 0 black black; #X coords 0 1 4799 -1 200 140 1; #X restore 240 130 graph; #X obj 160 180 tgl 20 0 empty empty empty 0 -10 0 12 #fcfcfc #000000 -#000000 1 1; +#000000 0 1; #X obj 160 205 metro 100; #X floatatom 30 20 5 0 0 0 - - -, f 5; #X obj 30 250 tabwrite~ scope; @@ -20,6 +21,7 @@ #X text 240 34 x: 0-1 (0 = left \, 1 = right channel); #X text 240 53 time: ramp time (msec), f 36; #X text 240 72 delay: initial delay (msec), f 36; +#X obj 310 290 declare -lib pdlua; #X connect 0 0 6 0; #X connect 0 0 9 0; #X connect 0 0 9 1; diff --git a/pdlua/tutorial/examples/luaxfade.pd_lua b/pdlua/tutorial/examples/luaxfade~.pd_lua similarity index 95% rename from pdlua/tutorial/examples/luaxfade.pd_lua rename to pdlua/tutorial/examples/luaxfade~.pd_lua index 59223d9..9795b67 100644 --- a/pdlua/tutorial/examples/luaxfade.pd_lua +++ b/pdlua/tutorial/examples/luaxfade~.pd_lua @@ -1,4 +1,4 @@ -local luaxfade = pd.Class:new():register("luaxfade") +local luaxfade = pd.Class:new():register("luaxfade~") function luaxfade:initialize(sel, atoms) self.inlets = {SIGNAL,SIGNAL} @@ -21,7 +21,7 @@ function luaxfade:in_1_fade(atoms) -- been run yet, then we cannot compute the sample delay and ramp times -- below, so we bail out, telling the user to enable dsp first. if not self.samplerate then - self:error("luaxfade: unknown sample rate, please enable dsp first") + self:error("luaxfade~: unknown sample rate, please enable dsp first") return end local fade, time, delay = table.unpack(atoms) diff --git a/pdlua/tutorial/examples/pdx.lua b/pdlua/tutorial/examples/pdx.lua index 1b37d5b..cb75f01 100644 --- a/pdlua/tutorial/examples/pdx.lua +++ b/pdlua/tutorial/examples/pdx.lua @@ -3,12 +3,6 @@ pdx.lua: useful extensions to pd.lua Copyright (C) 2020 Albert Gräf -To use this in your pd-lua scripts: local pdx = require 'pdx' - -Currently there's only the pdx.reload() function, which implements a kind of -remote reload functionality based on dofile and receivers, as explained in the -pd-lua tutorial. More may be added in the future. - This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 @@ -27,13 +21,25 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. local pdx = {} --[[ -Reload functionality. Call pdx.reload() on your object to enable, and -pdx.unreload() to disable this functionality again. +Reload functionality. pdx.reload() enables, and pdx.unreload() disables it. + +NOTE: As of pd-lua 0.12.8, this module is now pre-loaded, and pdx.reload() +gets called automatically before running the initialize method of any object, +so calling pdx.reload() explicitly is no longer needed. (Old code importing +the module and doing the call will continue to work, though.) Instead, you +will now have to call pdx.unreload() in your initialize method if you want to +*disable* this feature for some reason. pdx.reload installs a "pdluax" receiver which reloads the object's script file when it receives the "reload" message without any arguments, or a "reload class" message with a matching class name. +Before reloading the script, we also execute the object's prereload() method if +it exists, and the postreload() method after reloading, so that the user can +customize object reinitialization as needed (e.g., to call initialize() after +reloading, or to perform some other custom reinitialization). This was +suggested by @ben-wes (thanks Ben!). + We have to go to some lengths here since we only want the script to be reloaded *once* for each class, not for every object in the class. This complicates things quite a bit. In particular, we need to keep track of object @@ -59,21 +65,97 @@ local function finalize(self) end end --- Our receiver. In the future, more functionality may be added here, but at --- present this only recognizes the "reload" message and checks the class --- name, if given. +-- pre-reload actions +local function do_prereload(self, data) + -- save the current state + data.state = { + self.inlets, + self.outlets, + type(self.paint) == "function" + } + -- invoke the prereload method if it exists + if self.prereload and type(self.prereload) == "function" then + self:prereload() + end +end + +-- post-reload actions +local function do_postreload(self, data) + -- update the object's finalizer and restore our own, in case + -- anything has changed there + if self.finalize ~= finalize then + data.finalize = self.finalize + self.finalize = finalize + end + -- invoke the postreload method if it exists + if self.postreload and type(self.postreload) == "function" then + self:postreload() + end + -- retrieve the prereload state + local inlets, outlets, has_gui = table.unpack(data.state) + data.state = nil + -- recreate inlets and outlets as needed + local function iolets_eq(a, b) + -- compare two iolet signatures a and b + if type(a) ~= type(b) then + return false + elseif type(a) == "table" then + if #a ~= #b then + return false + else + for i = 1, #a do + if a[i] ~= b[i] then + return false + end + end + return true + end + else + return a == b + end + end + if not iolets_eq(self.inlets, inlets) then + pd._createinlets(self._object, self.inlets) + end + if not iolets_eq(self.outlets, outlets) then + pd._createoutlets(self._object, self.outlets) + end + -- also create the gui if a paint method was added during reload + if not has_gui and type(self.paint) == "function" then + -- NOTE: At present, you can only switch from non-gui to gui, but that + -- will be the most common use case anyway. The extra 1 flag in the call + -- informs the gui that redrawing the object may be in order. + pd._creategui(self._object, 1) + end +end + +-- Our receiver. This is the centerpiece of the extension. In the future, more +-- functionality may be added here. At present this catches the "reload" +-- message, checking the class name if given, doing the actual reloading of +-- the script, as well as invoking some callbacks before and afterwards which +-- provide hooks for user customizations. local function pdluax(self, sel, atoms) if sel == "reload" then -- reload message, check that any extra argument matches the class name if atoms[1] == nil or atoms[1] == self._name then - pd.post(string.format("pdx: reloading %s", self._name)) - self:dofilex(self._scriptname) - -- update the object's finalizer and restore our own, in case - -- anything has changed there - if self.finalize ~= finalize then - reloadables[self._name][self].finalize = self.finalize - self.finalize = finalize - end + -- iterate over *all* objects in this class and invoke their + -- prereload methods + for obj, data in pairs(reloadables[self._name]) do + if type(obj) == "table" then + do_prereload(obj, data) + end + end + -- only one instance (the one with the receiver) does the actual + -- loading of the script file (no need to load it more than once) + pd.post(string.format("pdx: reloading %s", self._name)) + self:dofilex(self._scriptname) + -- iterate over *all* objects in this class, invoke their postreload + -- methods, and update the objects themselves as needed (iolets, gui) + for obj, data in pairs(reloadables[self._name]) do + if type(obj) == "table" then + do_postreload(obj, data) + end + end end end end @@ -123,8 +205,10 @@ function pdx.reload(self) reloadables[self._name][self] = { finalize = self.finalize } self.finalize = finalize end - else - -- New class, make this the default receiver. + elseif self._name ~= "pdlua" and self._name ~= "pdluax" then + -- New class, make this the default receiver. Note that since dofilex() + -- is designed for regular (.pd_lua) objects only, we explicitly exclude + -- the built-in pdlua and pdluax classes here, to prevent crashes. reloadables[self._name] = { current = self } reloadables[self._name][self] = { finalize = self.finalize } -- install our finalizer diff --git a/pdlua/tutorial/examples/pdxtest.pd b/pdlua/tutorial/examples/pdxtest.pd new file mode 100644 index 0000000..cfa5609 --- /dev/null +++ b/pdlua/tutorial/examples/pdxtest.pd @@ -0,0 +1,40 @@ +#N canvas 180 344 450 323 12; +#X declare -stdpath pdlua; +#X obj 120 166 tgl 15 1 empty empty empty 17 7 0 10 #fcfcfc #000000 +#000000 1 1; +#X obj 120 190 metro 100; +#X obj 20 260 output~; +#X obj 220 30 declare -stdpath pdlua; +#X obj 220 91 pd-remote; +#X msg 220 61 pdluax reload; +#X obj 20 100 pdxtest~; +#X floatatom 20 30 5 0 0 0 - - -, f 5; +#X obj 48 60 adc~; +#N canvas 736 383 450 300 (subpatch) 0; +#X array scope 1000 float 0 black #000000; +#X array scope2 1000 float 0 black #f90606; +#X coords 0 1 999 -1 200 140 1; +#X restore 220 150 graph; +#X floatatom 73 130 5 0 0 0 - - -, f 5; +#N canvas 605 404 450 300 scope 0; +#X obj 20 80 tabwrite~ scope; +#X obj 100 110 tabwrite~ scope2; +#X obj 100 20 inlet~; +#X obj 20 20 inlet~; +#X obj 168 20 inlet; +#X connect 2 0 1 0; +#X connect 3 0 0 0; +#X connect 4 0 0 0; +#X connect 4 0 1 0; +#X restore 20 230 pd scope; +#X connect 0 0 1 0; +#X connect 1 0 11 2; +#X connect 5 0 4 0; +#X connect 6 0 2 0; +#X connect 6 0 11 0; +#X connect 6 1 2 1; +#X connect 6 1 11 1; +#X connect 6 2 10 0; +#X connect 7 0 6 0; +#X connect 8 0 6 1; +#X connect 8 1 6 2; diff --git a/pdlua/tutorial/examples/pdxtest~.pd_lua b/pdlua/tutorial/examples/pdxtest~.pd_lua new file mode 100644 index 0000000..e20c8a7 --- /dev/null +++ b/pdlua/tutorial/examples/pdxtest~.pd_lua @@ -0,0 +1,85 @@ +local pdxtest = pd.Class:new():register("pdxtest~") + +-- This is intended as a playgound for the livecoding features in the latest +-- pdx.lua version. In particular, the new prerelod and postreload methods can +-- now be used to modify the number of inlets and outlets of an object on the +-- fly. + +function pdxtest:initialize(sel, atoms) + -- change these around and reload! + self.inlets = {DATA,SIGNAL,SIGNAL} + self.outlets = {SIGNAL,SIGNAL,DATA} + self.phase = 0 + self.cycle = 0 + self.freq = 133 + return true +end + +function pdxtest:prereload() + -- stuff to do pre-reload goes here + pd.post("About to reload!") +end + +function pdxtest:postreload() + -- stuff to do post-reload goes here + pd.post("Reloaded!") + -- instead of doing a full initialization, you could also just change the + -- number of inlets and outlets here + self:initialize() +end + +-- we keep this generic so that you can put the data inlets and outlets +-- wherever you please +function pdxtest:in_n_float(n, x) + -- show the value we got, along with the inlet index + pd.post(string.format("float: #%d: %g", n, x)) + -- send the data to whatever data outlets we have + if type(self.outlets) == "number" then + -- all data outlets + for i = 1, self.outlets do + self:outlet(i, "float", {x}) + end + elseif type(self.outlets) == "table" then + -- full signature, need to look for DATA outlets + for i = 1, #self.outlets do + if self.outlets[i] == DATA then + self:outlet(i, "float", {x}) + end + end + end +end + +function pdxtest:dsp(samplerate, blocksize) + self.samplerate = samplerate + self.blocksize = blocksize +end + +function pdxtest:random_walk(dmin, dmax) + -- random walk of sine waves with varying frequencies (cf. foo~.pd_lua) + self.cycle = self.cycle + 1 + if self.cycle > 100 then + self.freq = self.freq + math.random(-1, 1)*math.random(dmin, dmax) + self.freq = math.min(1000, math.max(150, self.freq)) + self.cycle = 0 + end +end + +function pdxtest:perform(in1, in2) + local dmin, dmax = 50, 100 + self:random_walk(dmin, dmax) + local freq, a, b = self.freq, 0.3, 0.1 + local out = {} -- result table + -- random phase offset + local d = b * math.random() + for i = 1, self.blocksize do + -- phase in radians + local x = 2 * math.pi * (self.phase + d) + out[i] = a * math.sin(x) + self.phase = self.phase + freq / self.samplerate + if self.phase >= 1 then + self.phase = self.phase - 1 + end + end + -- return up to two extra signals straight from the input + return out, in1, in2 +end diff --git a/pdlua/tutorial/examples/tictoc.pd_lua b/pdlua/tutorial/examples/tictoc.pd_lua index d602596..5079c83 100644 --- a/pdlua/tutorial/examples/tictoc.pd_lua +++ b/pdlua/tutorial/examples/tictoc.pd_lua @@ -14,8 +14,6 @@ function tictoc:initialize(sel, atoms) return true end --- don't forget this, or else... - function tictoc:finalize() self.clock:destruct() end diff --git a/pdlua/tutorial/pd-lua-intro.pdf b/pdlua/tutorial/pd-lua-intro.pdf index 3902665..573b436 100644 Binary files a/pdlua/tutorial/pd-lua-intro.pdf and b/pdlua/tutorial/pd-lua-intro.pdf differ diff --git a/pdlua/tutorial/tutorial-meta.pd b/pdlua/tutorial/tutorial-meta.pd new file mode 100644 index 0000000..ec83eb8 --- /dev/null +++ b/pdlua/tutorial/tutorial-meta.pd @@ -0,0 +1,8 @@ +#N canvas 2 51 320 90 10; +#N canvas 2 51 382 154 META 0; +#X text 10 10 NAME pdlua-tutorial; +#X text 10 30 LICENSE CC BY-SA 4.0; +#X text 10 50 DESCRIPTION The pdlua tutorial \, with examples; +#X text 10 70 AUTHOR Albert Gräf; +#X restore 43 36 pd META; +#X text 39 15 The pdlua tutorial \, with examples; diff --git a/pdlua_gfx.h b/pdlua_gfx.h index 9d7a1c0..60ff4e1 100644 --- a/pdlua_gfx.h +++ b/pdlua_gfx.h @@ -42,6 +42,7 @@ int xxsys_hostfontsize(int fontsize, int zoom) #define MAX(a, b) (((a) > (b)) ? (a) : (b)) +static void mylua_error (lua_State *L, t_pdlua *o, const char *descr); // Functions that need to be implemented separately for each Pd flavour static int gfx_initialize(t_pdlua *obj); @@ -94,8 +95,7 @@ void pdlua_gfx_repaint(t_pdlua *o, int firsttime) { if (lua_pcall(__L(), 1, 0, 0)) { - pd_error(o, "lua: error in repaint:\n%s", lua_tostring(__L(), -1)); - lua_pop(__L(), 1); /* pop the error string */ + mylua_error(__L(), o, "repaint"); } lua_pop(__L(), 1); /* pop the global "pd" */ @@ -116,8 +116,7 @@ void pdlua_gfx_mouse_event(t_pdlua *o, int x, int y, int type) { if (lua_pcall(__L(), 4, 0, 0)) { - pd_error(o, "lua: error in mouseevent:\n%s", lua_tostring(__L(), -1)); - lua_pop(__L(), 1); /* pop the error string */ + mylua_error(__L(), o, "mouseevent"); } lua_pop(__L(), 1); /* pop the global "pd" */ @@ -921,7 +920,7 @@ static int end_paint(lua_State* L) { static int set_color(lua_State* L) { t_pdlua_gfx *gfx = pop_graphics_context(L); - int r, g, b; + int r, g, b, a; if (lua_gettop(L) == 1) { // Single argument: parse as color ID instead of RGB int color_id = luaL_checknumber(L, 1); if(color_id != 1) @@ -942,9 +941,19 @@ static int set_color(lua_State* L) { b = luaL_checknumber(L, 3); } +#ifndef PURR_DATA // AFAIK, alpha is not supported in tcl/tk snprintf(gfx->current_color, 8, "#%02X%02X%02X", r, g, b); gfx->current_color[7] = '\0'; +#else + // ... but it is in Purr Data (nw.js gui) + a = 255; + if (lua_gettop(L) >= 4) { + a = luaL_checknumber(L, 4)*255; + } + snprintf(gfx->current_color, 10, "#%02X%02X%02X%02X", r, g, b, a); + gfx->current_color[9] = '\0'; +#endif return 0; } diff --git a/pdluax-help.pd b/pdluax-help.pd index b0a44e4..104b711 100644 --- a/pdluax-help.pd +++ b/pdluax-help.pd @@ -21,7 +21,7 @@ #X text 163 351 1) symbol - source file name (without extension); #X text 39 150 You can right click the object and ask to open the source file if your system has an application set to handle this extension., f 74; #X text 260 227 Details on writting luax files ----->, f 20; -#X text 39 95 The [pdluax] object is similar to [pdlua] but you can use "*.pd_luax" instead and load them as arguments. It is less efficient but more flexible when developing or live-coding. You also need to load [pdlua] as a library first as with the [declare] object above., f 74; +#X text 39 95 The [pdluax] object is similar to [pdlua] but used to load .pd_luax files and instantiate objects from them. It is less efficient but more flexible than ordinary Lua objects when developing or live-coding. You also need to load [pdlua] as a library first as with the [declare] object above., f 74; #X obj 113 191 pdluax pdlua/hello; #N canvas 613 120 540 414 quickstart 0; #X obj 36 94 cnv 15 450 100 empty empty pdluax\ HOWTO 20 12 0 14 #dfdfdf #000000 0; @@ -34,11 +34,12 @@ #X text 52 253 + Good for rapid development/testing cycles.; #X text 52 275 + Good for live coding.; #X text 52 296 + No need to restart Pd if you made a little mistake.; -#X text 52 363 - Reloading the file each time is slower.; -#X text 52 383 - Syntax is different to the syntax expected by the Lua loader (see below for discussion).; -#X text 52 416 - There is no "reload" functionality \, so you can have multiple objects called [pdluax foo] but that have different behaviours.; -#X text 52 458 - Data shared between objects must be accessible globally.; -#X text 52 477 - The above two points mean some mistakes/changes mean you have to restart Pd anyway.; +#X text 52 359 - Reloading the file each time is slower.; +#X text 52 375 - Syntax is different to the syntax expected by the Lua loader (see below for discussion).; +#X text 52 405 - There is no "reload" functionality \, so you can have multiple objects called [pdluax foo] but that have different behaviours.; +#X text 52 447 - Data shared between objects must be accessible globally.; +#X text 52 465 - The above two points mean some mistakes/changes mean you have to restart Pd anyway.; +#X text 52 496 NOTE: Modern pd-lua versions offer a much more advanced live coding facility -- check out pdx.lua in the tutorial.; #X text 51 563 The last expression/statement in the file should be of the form:; #X obj 79 610 cnv 15 300 60 empty empty empty 20 12 0 14 #ffffff #404040 0; #X text 91 614 return function (self \, sel \, atoms); @@ -61,6 +62,6 @@ #N canvas 0 22 450 278 (subpatch) 0; #X coords 0 1 100 -1 450 935 1; #X restore 36 93 graph; -#X text 45 21 Find basic instructions/examples below. For a detailed tutorial \, check the 'pdlua/tutorial' folder for a PDF documente. You can also find this tutorial online at: https://agraef.github.io/pd-lua/tutorial/pd-lua-intro.html, f 66; +#X text 45 21 Find basic instructions/examples below. For more details \, check 'pd-lua-intro.pdf' in the 'pdlua/tutorial' folder. You can also find this tutorial online at: https://agraef.github.io/pd-lua/tutorial/pd-lua-intro.html, f 66; #X restore 386 241 pd quickstart; #X text 233 190 <-- loading "hello.pd_luax" file.; diff --git a/tutorial/15-remote-control1.png b/tutorial/15-remote-control1.png index 3a99632..39d0f94 100644 Binary files a/tutorial/15-remote-control1.png and b/tutorial/15-remote-control1.png differ diff --git a/tutorial/15-remote-control2.png b/tutorial/15-remote-control2.png new file mode 100644 index 0000000..2266d84 Binary files /dev/null and b/tutorial/15-remote-control2.png differ diff --git a/tutorial/16-remote-control2.gif b/tutorial/16-remote-control2.gif index 624b205..369bf89 100644 Binary files a/tutorial/16-remote-control2.gif and b/tutorial/16-remote-control2.gif differ diff --git a/tutorial/17-live-xfade.png b/tutorial/17-live-xfade.png new file mode 100644 index 0000000..957e565 Binary files /dev/null and b/tutorial/17-live-xfade.png differ diff --git a/tutorial/17-signal4.png b/tutorial/17-signal4.png index 51a5754..80805f1 100644 Binary files a/tutorial/17-signal4.png and b/tutorial/17-signal4.png differ diff --git a/tutorial/18-graphics1.png b/tutorial/18-graphics1.png index 6e894dc..748711f 100644 Binary files a/tutorial/18-graphics1.png and b/tutorial/18-graphics1.png differ diff --git a/tutorial/18-graphics2.png b/tutorial/18-graphics2.png index fb29a10..234332c 100644 Binary files a/tutorial/18-graphics2.png and b/tutorial/18-graphics2.png differ diff --git a/tutorial/18-graphics3.png b/tutorial/18-graphics3.png index 2501fc6..b36aaf8 100644 Binary files a/tutorial/18-graphics3.png and b/tutorial/18-graphics3.png differ diff --git a/tutorial/18-graphics4.png b/tutorial/18-graphics4.png index b9aa326..e97027a 100644 Binary files a/tutorial/18-graphics4.png and b/tutorial/18-graphics4.png differ diff --git a/tutorial/18-graphics5.png b/tutorial/18-graphics5.png index f45d87c..d679f81 100644 Binary files a/tutorial/18-graphics5.png and b/tutorial/18-graphics5.png differ diff --git a/tutorial/18-graphics6.png b/tutorial/18-graphics6.png index 0297434..3ff5462 100644 Binary files a/tutorial/18-graphics6.png and b/tutorial/18-graphics6.png differ diff --git a/tutorial/19-dollar-symbols.png b/tutorial/19-dollar-symbols.png new file mode 100644 index 0000000..660304e Binary files /dev/null and b/tutorial/19-dollar-symbols.png differ diff --git a/tutorial/pd-lua-intro.html b/tutorial/pd-lua-intro.html index 763d752..a6c37cf 100644 --- a/tutorial/pd-lua-intro.html +++ b/tutorial/pd-lua-intro.html @@ -701,32 +701,31 @@ -pd-lua-intro +A Quick Introduction to Pd-Lua
-

A Quick Introduction to Pd-Lua

Albert Gräf <aggraef@gmail.com>
Computer Music Dept., Institute of Art History and Musicology
Johannes Gutenberg University (JGU) Mainz, Germany
August 2024

This document is licensed under CC BY-SA 4.0. Other formats: Markdown source, PDF
Permanent link: https://agraef.github.io/pd-lua/tutorial/pd-lua-intro.html

Why Pd-Lua?

Pd's facilities for data structures, iteration, and recursion are somewhat limited, thus sooner or later you'll probably run into a problem that can't be easily solved by a Pd abstraction any more. At this point you'll have to consider writing an external object (or just external, for short) in a "real" programming language instead. Pd externals are usually programmed using C, the same programming language that Pd itself is written in. But novices may find C difficult to learn, and the arcana of Pd's C interface may also be hard to master.

Enter Pd-Lua, the Pd programmer's secret weapon, which lets you develop your externals in the Lua scripting language. Pd-Lua was originally written by Claude Heiland-Allen and has since been maintained by a number of other people in the Pd community. Lua, from PUC Rio, is open-source (under the MIT license), mature, very popular, and supported by a large developer community. It is a small programming language, but very capable, and is generally considered to be relatively easy to learn. For programming Pd externals, you'll also need to learn a few bits and pieces which let you interface your Lua functions to Pd, as explained in this tutorial, but programming externals in Lua is still quite easy and a lot of fun. Using Pd-Lua, you can program your own externals ranging from little helper objects to full-blown synthesizers, sequencers, and algorithmic composition tools. It gives you access to Pd arrays and tables, as well as a number of other useful facilities such as clocks and receivers, which we'll explain in some detail. Pd-Lua also ships with a large collection of instructive examples which you'll find helpful when exploring its possibilities.

Pd-Lua was originally designed for control processing, so we used to recommend Faust for doing dsp instead. We still do, but Faust isn't for everyone; being a purely functional language, Faust follows a paradigm which most programmers aren't very familiar with. Fortunately, thanks to the work of Timothy Schoen, the most recent Pd-Lua versions now also provide support for signal processing and even graphics. So it is now possible to create pretty much any kind of Pd object in Lua, including dsp objects. (However, Faust will almost certainly execute dsp code much more efficiently than Pd-Lua, as it generates highly optimized native code for just this purpose.)

Note that we can't possibly cover Pd or the Lua language themselves here, so you'll have to refer to other online resources to learn about those. In particular, check out the Lua website, which has extensive documentation available, and maybe have a look at Derek Banas' video tutorial for a quick overview of Lua. For Pd, we recommend the Pd FLOSS Manual at https://flossmanuals.net/ to get started.

Installation

Pd-Lua works inside any reasonably modern Pd flavor. This encompasses vanilla Pd, of course, but also Purr Data which includes an up-to-date version of Pd-Lua for Lua 5.4 and has it enabled by default, so you should be ready to go immediately; no need to install anything else. The same is true for plugdata (version 0.6.3 or later), a Pd flavor which can also run as a plug-in inside a DAW.

With vanilla Pd, you can install the pdlua package from Deken. There's also an official Debian package, maintained by IOhannes Zmölnig. You can also compile Pd-Lua from source, using the author's Github repository. Compilation instructions are in the README, and you'll also find some Mac and Windows binaries there. In either case, after installing Pd-Lua you also have to add pdlua to Pd's startup libraries.

If all is well, you should see a message like the following in the Pd console (note that for vanilla Pd you'll have to switch the log level to 2 or more to see that message):

This will also tell you the Lua version that Pd-Lua is using, so that you can install a matching version of the stand-alone Lua interpreter if needed. Lua should be readily available from your package repositories on Linux, and for Mac and Windows you can find binaries on the Lua website. In the following we generally assume that you're using Lua 5.3 or later (using Lua versions older than 5.3 is not recommended).

If all is not well and you do not see that message, then most likely Pd-Lua refused to load because the Lua library is missing. This shouldn't happen if you installed Pd-Lua from a binary package, but if it does then you may have to manually install the right version of the Lua library to make Pd-Lua work. Make sure that you install the package with the Lua library in it; on Debian, Ubuntu and their derivatives this will be something like liblua5.4-0.

A basic example

With that out of the way, let's have a look at the most essential parts of a Lua external. To make an external, say foo, loadable by Pd-Lua, you need to put it into a Lua script, which is simply a text file with the right name (which must be the same as the object name, foo in this case) and extension (which needs to be .pd_lua), so the file name will be foo.pd_lua in this example.

Any implementation of an object must always include:

  • a call to the pd.Class:new():register method which registers the object class with Pd (this should always be the first line of the script, other than comments)

  • a definition of the initialize method for your object class

Here is a prototypical example (this is the contents of the foo.pd_lua file):

Note that in the first line we called pd.Class:new():register with the name of the object class as a string, which must be the same as the basename of the script, otherwise Pd's loader will get very confused, create the wrong object class, print a (rather cryptic) error message, and won't be able to create the object.

We also assigned the created class (which is represented as a Lua table) to a variable foo (which we made local to the script file here, as explained below). We need that variable as a qualifier for the methods of the object class, including initialize. You can actually name that variable whatever you want, as long as you use that name consistently throughout the script. This can be useful at times, if the actual class name you chose, as it is known to Pd and set with pd.Class:new():register (as well as being the basename of your .pd_lua script), is a jumble of special characters such as fo:o#?!, which isn't a valid Lua identifier.

Next comes the initialize method, which is implemented as a Lua function, prefixing the method name with the name of the class variable we created above and a colon, i.e., foo:initialize. (This colon syntax is used for all functions that represent methods, which receive the called object as an implicit self parameter; please check the section on function definitions in the Lua manual for details.) As a bare minimum, as is shown here, this method must return true, otherwise the loader will assume that the object creation has failed, and will complain that it couldn't create the object with an error message.

We mention in passing here that Pd-Lua also provides a parameter-less postinitialize method which can be used to execute code after the object has been created, but before the object starts processing messages. We'll see an example of this method later.


NOTE: Pd-Lua runs all Lua objects in the same instance of the Lua interpreter. Therefore, as a general guideline, we want to keep the global name space tidy and clean. That's why we made foo a local variable, which means that its scope is confined to this single script. Note that this isn't needed for the member variables and methods, as these are securely stowed away inside the object and not accessible from the outside anyway, if the class variable is local. But the same caveat applies to all variables and functions in the script file that might be needed to implement the object, so normally you want to mark these as local, too (or turn them into member variables and methods, if that seems more appropriate).

We mention in passing that global variables and functions may also have their uses if you need to share a certain amount of global state between different Lua objects. But even then it's usually safer to have the objects communicate with each other behind the scenes using receivers, which we'll explain later.


To actually use the object class we just created, Pd needs be able to find our foo.pd_lua file. We'll discuss different approaches in the following section, but the easiest way to achieve this is to just drop foo.pd_lua into the directory that your patch is in (say, pd-lua in your home directory). Now we can just create our first foo object (hit Ctrl+1, then type the object name foo), and we should see something like this:

First light

Hooray, it works! :)) Well, this object doesn't do anything right now, so let's equip it with a single inlet/outlet pair. This is what the initialize method is for, so we have to edit that method accordingly.

NB: If you closed the editor already and don't remember where the file is, you can just right-click the object and choose Open, which will open the .pd_lua file in your favorite text editor, as configured in your desktop and/or shell environment.

Note that, as we already mentioned, the self variable here is an implicit parameter of any Lua method, which refers to the object itself. Every Pd-Lua object has two member variables inlets and outlets which let us specify the number of inlets and outlets our object should have. This needs to be done when the object is initialized; afterwards, the number of inlets and outlets is set in stone and can't be changed any more.

Next, we have to make sure that Pd picks up our edited class definition. Since the Pd-Lua loader will never reload the .pd_lua file for any given object class during a Pd session, we will have to save the patch, quit Pd, relaunch it and reopen the patch:

Inlet and outlet

So there, we got our single inlet/outlet pair now. To do anything with these, we finally have to add some message handlers to our object. Say, for instance, we want to handle a bang message by incrementing a counter and outputting its current value to the outlet. We first have to initialize the counter value in the initialize method. As we want each foo object to have its own local counter value, we create the counter as a member variable:

It's not necessary to declare the self.counter variable in any way, just give it an initial value and be done with it. Finally, we have to add a method for the bang message, which looks as follows:

We'll dive into the naming conventions for message handlers later, but note that in_1 specifies the first (and only) inlet and bang the kind of message we expect. In the body of the method we increment the self.counter value and output its new value on the first (and only) outlet. This is done by the predefined self:outlet method which takes three arguments: the outlet number, the (Pd) data type to output, and the output value itself. (In general, it's possible to have multiple values there, e.g., when outputting a list value. Therefore the output value is always specified as a Lua table, hence the curly braces around the float output value.)

Throwing everything together, our Lua external now looks as follows:

And here's the same patch again, which now lets us drag the hand to change the phase value:

Dial with mouse interaction

More dial action: clocks and speedometers

Now that our dial object is basically finished, let's do something interesting with it. The most obvious thing is to just turn it into a clock (albeit one with just a seconds hand) counting off the seconds. For that we just need to add a metro object which increments the phase angle and sends the value to the dial each second:

A clock

Pd lets us store the phase angle in a named variable (v phase) which can be recalled in an expr object doing the necessary calculations. The expr object sends the computed value to the phase receiver, which updates both the variable and the upper numbox, and the numbox then updates the dial. Note that we also set the variable whenever the dial outputs a new value, so you can also drag around the hand to determine the starting phase. And we added a 0 message to reset the hand to the 12 o'clock home position when needed.

Here's another little example, rather useless but fun, simulating a speedometer which just randomly moves the needle left and right:

A speedometer

I'm sure you can imagine many more creative uses for this simple but surprisingly versatile little GUI object, which we did in just a few lines of fairly simple Lua code. Have fun with it! An extended version of this object, which covers some more features of the graphics API that we didn't discuss here to keep things simple, can be found as dial.pd and dial.pd_lua in the tutorial examples:

Extended dial example

The extended example adds messages for resizing the object and setting colors, and also shows how to save and restore object state in the creation arguments using the set_args() method mentioned at the beginning of this section. The accompanying patch covers all the examples we discussed here, and adds a third example showing how to utilize our dial object as a dB meter.

Live coding

I've been telling you all along that in order to make Pd-Lua pick up changes you made to your .pd_lua files, you have to relaunch Pd and reload your patches. Well, as you probably guessed by now, this isn't actually true. So in this section we are going to cover Pd-Lua's live coding features, which let you modify your sources and have Pd-Lua reload them on the fly, without ever exiting the Pd environment. This rapid incremental style of development is one of the hallmark features of dynamic interactive programming environments like Pd and Lua. Musicians also like to employ it to modify their programs live on stage, which is where the term "live coding" comes from.

I've kept this topic for the final section of this guide, because it is somewhat advanced, and there are several different (traditional and new) methods available which differ in capabilities and ease of use. However, in modern Pd-Lua versions there is one preferred method which rules them all, so if you want to cut to the chase as quickly as possible, then feel free to just skip ahead to the pdx.lua section now.

If you're still here, then you probably want to learn about the other methods, too. So in the following we first describe the predefined Pd-Lua object classes pdlua and pdluax, so that you know the "traditional" live-coding instruments that Pd-Lua had on offer for a long time. We also discuss how to add a reload message to your existing object definitions. This is quite easy to do by directly employing Pd-Lua's dofile function, which is also what both pdlua and pdluax use internally.

These methods all work with pretty much any Pd-Lua version out there, but require a little bit of setup which can get time-consuming and error-prone if you're dealing with large patches involving a lot of different Lua objects.

That's why Pd-Lua nowadays includes an extension module called pdx.lua that "just works" out of the box and automatizes everything, so that you only have to put a simple reload message in your main patch and be done with it. This is also the method we recommend, to novices and expert users alike. We describe it last so that you can also gather a good understanding of Pd-Lua's traditional live coding methods, and learn to appreciate them. These are still used in many older scripts, and Pd-Lua will continue to support them for backward compatibility.

pdlua

The pdlua object accepts a single kind of message of the form load filename on its single inlet, which causes the given Lua file to be loaded and executed. Since pdlua has no outlets, its uses are rather limited. However, it does enable you to load global Lua definitions and execute an arbitrary number of statements, e.g., to post some text to the console or transmit messages to Pd receivers using the corresponding Pd-Lua functions. For instance, here's a little Lua script loadtest.lua which simply increments a global counter variable (first initializing it to zero if the variable doesn't exist yet) and posts its current value in the console:

To run this Lua code in Pd, you just need to connect the message load loadtest.lua to pdlua's inlet (note that you really need to specify the full filename here, there's no default suffix):

pdlua example

Now, each time you click on the load loadtest.lua message, the file is reloaded and executed, resulting in some output in the console, e.g.:

Also, you can edit the script between invocations and the new code will be loaded and used immediately. E.g., if you change counter + 1 to counter - 1, you'll get:

That's about all there is to say about pdlua; it's a very simple object.

pdluax

pdluax is a bit more elaborate and allows you to create real Pd-Lua objects with an arbitrary number of inlets, outlets, and methods. To these ends, it takes a single creation argument, the basename of a .pd_luax file. This file is a Lua script returning a function to be executed in pdluax's own initialize method, which contains all the usual definitions, including the object's method definitions, in its body. This function receives the object's self as well as all the extra arguments pdluax was invoked with, and should return true if creation of the object succeeded.

For instance, here's a simplified version of our foo counter object, rewritten as a .pd_luax file, to be named foo.pd_luax:

Note the colon syntax self:in_1_bang(). This adds the bang method directly to the self object rather than its class, which is pdluax. (We obviously don't want to modify the class itself, which may be used to create any number of different kinds of objects, each with their own collection of methods.) Also note that the outer function is "anonymous" (nameless) here; you can name it, but there's usually no need to do that, because this function will be executed just once, when the corresponding pdluax object is created. Another interesting point to mention here is that this approach of including all the object's method definitions in its initialization method works with regular .pd_lua objects, too; try it!

In the patch, we invoke a .pd_luax object by specifying the basename of its script file as pdluax's first argument, adding any additional creation arguments that the object itself might need:

pdluax example

These pdluax foo objects work just the same as their regular foo counterparts, but there's an important difference: The code in foo.pd_luax is loaded every time you create a new pdluax foo object. Thus you can easily modify that file and just add a new pdluax foo object to have it run the latest version of your code. For instance, in foo.pd_luax take the line that reads:

Now change that + operator to -:

Don't forget to save your edits, then go back to the patch and recreate the pdluax foo object on the left. The quickest way to do that is to just delete the object, then use Pd's "Undo" operation, Ctrl+Z. Et voilà: the new object now decrements the counter rather than incrementing it. Also note that the other object on the right still runs the old code which increments the counter; thus you will have to give that object the same treatment if you want to update it, too.

While pdluax was considered Pd-Lua's main workhorse for live coding in the past, it has its quirks. Most notably, the syntax is different from regular object definitions, so you have to change the code if you want to turn it into a .pd_lua file. Also, having to recreate an object to reload the script file is quite disruptive (it resets the internal state of the object), and may leave objects in an inconsistent state (different objects may use various different versions of the same script file). Sometimes this may be what you want, but it makes pdluax somewhat difficult to use. It's not really tailored for interactive development, but it shines if you need a specialized tool for changing your objects on a whim in a live situation.

Fortunately, if you're not content with Pd-Lua's traditional facilities for live coding, it's easy to roll your own using the internal dofile method, which is discussed in the next subsection.

dofile and dofilex

So let's discuss how to use dofile in a direct fashion. Actually, we're going to use its companion dofilex here, which works the same as dofile, but loads Lua code relative to the "externdir" of the class (the directory of the .pd_lua file) rather than the directory of the Pd canvas with the pdlua object, which is what dofile does. Normally, this won't make much of a difference, but it will matter if Lua class names are specified using relative pathnames, such as ../foo or bar/baz. Since we're reloading class definitions here, it's better to use dofilex so that our objects don't break if we move things about.

The method we sketch out below is really simple and doesn't have any of the drawbacks of the pdluax object, but you still have to add a small amount of boilerplate code to your existing object definition. Here is how dofilex is invoked:

  • self:dofilex(scriptname): This loads the given Lua script, like Lua's loadfile, but also performs a search on Pd's path to locate the file, and finally executes the file if it was loaded successfully. Note that self must be a valid Pd-Lua object, which is used solely to determine the externdir of the object's class, so that the script will be found if it is located there.

The return values of dofilex are those of the Lua script, along with the path under which the script was found. If the script itself returns no value, then only the path will be returned. (We don't use any of this information in what follows, but it may be useful in more elaborate schemes. For instance, pdluax uses the returned function to initialize the object, and the path in setting up the object's actual script name.)

Of course, dofilex needs the name of the script file to be loaded. We could hardcode this as a string literal, but it's easier to just ask the object itself for this information. Each Pd-Lua object has a number of private member variables, among them _name (which is the name under which the object class was registered) and _scriptname (which is the name of the corresponding script file, usually this is just _name with the .pd_lua extension tacked onto it). The latter is what we need. Pd-Lua also offers a whoami() method for this purpose, but that method just returns _scriptname if it is set, or _name otherwise. Regular Pd-Lua objects always have _scriptname set, so it will do for our purposes.

Finally, we need to decide how to invoke dofilex in our object. The easiest way to do this is to just add a message handler (i.e., an inlet method) to the object. For instance, say that the object is named foo which is defined in the foo.pd_lua script. Then all you have to do is add something like the following definition to the script:

As we already discussed, this code uses the object's internal _scriptname variable, and so is completely generic. You can just copy this over to any .pd_lua file, if you replace the foo prefix with whatever the name of your actual class variable is. With that definition added, you can now just send the object a reload message whenever you want to have its script file reloaded.


NOTE: This works because the pd.Class:new():register("foo") call of the object only registers a new class if that object class doesn't exist yet; otherwise it just returns the existing class.

By reloading the script file, all of the object's method definitions will be overwritten, not only for the object receiving the reload message, but for all objects of the same class, so it's sufficient to send the message to any (rather than every) object of the class. Also, existing object state (as embodied by the internal member variables of each object) will be preserved.

In general all this works pretty well, but there are some caveats, too. Note that if you delete one of the object's methods, or change its name, the old method will still hang around in the runtime class definition until you relaunch Pd. That's because reloading the script does not erase any old method definitions, it merely replaces existing and adds new ones.

Finally, keep in mind that reloading the script file does not re-execute the initialize method. This method is only invoked when an object is instantiated. Thus, in particular, reloading the file won't change the number of inlets and outlets of an existing object. Newly created objects will pick up the changes in initialize, though, and have the proper number of inlets and outlets if those member variables were changed.


Let's give this a try, using luatab.pd_lua from the "Using arrays and tables" section as an example. In fact, that's a perfect showcase for live coding, since we want to be able to change the definition of the waveform function f in luatab:in_1_float on the fly. Just add the following code to the end of luatab.pd_lua:

Now launch the luatab.pd patch and connect a reload message to the luatab wave object, like so:

Live-coding with dofilex 1

Next change the wavetable function to whatever you want, e.g.:

Return to the patch, click the reload message, and finally reenter the frequency value, so that the waveform gets updated:

Live-coding with dofilex 2

pdx.lua

The method sketched out in the preceding subsection works well enough for simple patches. However, having to manually wire up the reload message to one object of each class that you're editing is still quite cumbersome. In a big patch, which is being changed all the time, this quickly becomes unwieldy. Wouldn't it be nice if we could equip each object with a special receiver, so that we can just click a message somewhere in the patch to reload a given class, or even all Pd-Lua objects at once? Or even send that message via the pdsend program, e.g., from the text editor in which you edit the Lua source of your object?

Well, all this is in fact possible, but the implementation is a bit too involved to fully present here. So we have provided this in a separate pdx.lua module, which you can find in the sources accompanying this tutorial for your perusal. As of Pd-Lua 0.12.8, pdx.lua is pre-loaded and all the required setup is performed automatically. You only have to add a message like the following to your patch, which goes to the special pdluax receiver (note that this is unrelated to the pdluax object discussed previously, it just incidentally uses the same name):

When clicked, this just reloads all Pd-Lua objects in the patch. You can also specify the class to be reloaded (the receiver matches this against each object's class name):

Or maybe name several classes, like so:

You get the idea. Getting set up for remote control via pdsend isn't much harder. E.g., let's say that we use UDP port 4711 on localhost for communicating with Pd, then you just need to connect netreceive 4711 1 to the pdluax receiver in a suitable way. Let's use the luatab.pd_lua object from the previous subsection as an example. You can remove the in_1_reload handler from that script -- it's no longer needed, as pdx.lua now dispatches the reload messages for us. Here's how the revised patch looks like:

Live-coding with remote control

This doesn't look any simpler than before, but it also does a whole lot more. Clicking the message not just reloads the luatab script, but any Lua object you have running, in any of the patches you have opened. And you can now use pdsend 4711 localhost udp to reload your Lua objects from literally anywhere. Any decent code editor will let you bind a keyboard command which does this for you. Myself, I'm a die-hard Emacs fan, so I've included a little elisp module pd-remote.el which shows how to do this. Once you've added this to your .emacs, you can just type Ctrl+C Ctrl+K in any Lua buffer to make Pd reload your Lua scripts after editing them. It doesn't get much easier than that.

In addition, I've provided a little abstraction named pd-remote.pd which takes care of adding the netreceive and messaging bits and also looks much tidier in your patches. Using the abstraction is easy: Insert pd-remote into the patch you're working on, and (optionally) connect a pdluax reload message (without the ; prefix) to the inlet of the abstraction; or use something like pdluax reload foo to reload a specific object class. Now you can just click on that message to reload your script files, and the abstraction will also respond to such messages on port 4711 (the port number can be changed in the abstraction if needed). Here's how that looks like in a patch:

Live-coding with pd-remote.pd


NOTE: To make Pd find the pd-remote.pd abstraction without having to copy it to your project directory, you can add the pdlua external directory (which is where the abstraction gets installed) to your Pd library path, either in your Pd setup, or inside the patch with a declare -stdpath object, as shown above.

The pd-remote.el file can be installed in your Emacs site-lisp directory if needed. However, the easiest way to install it is from MELPA, a large repository of Emacs packages. Please also check the pd-remote repository on GitHub for the latest pd-remote version and further details. This also includes a pointer to a Visual Studio Code extension written by Baris Altun which can be used as a replacement for pd-remote.el if you prefer VS Code for editing.


And here's a little gif showing the above patch in action. You may want to watch this in Typora or your favorite web browser to make the animation work.

Live-coding in action

So there you have it: Not one, not two, but three different ways to live-code with Pd-Lua (or four, if you count in the pdlua object). pdx.lua certainly is the most advanced and user-friendly solution among these, but you can choose whatever best fits your purpose and is the most convenient for you.

Object reinitialization in pdx.lua

If pdx.lua reloads a script file, it normally does not run the initialize method. This is by design, as we want the reload process to be as smooth as possible while running a patch, and invoking the initialize method could potentially be quite disruptive, as it usually reinitializes all your member variables.

However, pdx.lua has another trick up its sleeve if you do need some kind of custom reinitialization. There are two callback methods that you can implement, prereload and postreload which will be invoked immediately before and after reloading the script file, respectively. Either method can be used to reinitialize your objects during reloading in any desired way. The only difference between the two methods is that prereload still runs in the old script, while postreload executes the new code which has just been loaded. Typically you'd use prereload to perform any required bookkeeping or state-saving before the script gets loaded, and postreload for custom initialization afterwards.

In particular, these callbacks can change any member variables, either directly or by invoking other methods. The most important use cases probably are to change the inlets and outlets variables, and to add a paint method, which can be done on the fly to reconfigure your objects accordingly. To these ends, you can just call the initialize method from postreload. To demonstrate this, in the tutorial examples you'll find a pdxtest.pd patch and pdxtest~.pd_lua script. The script has the following reload method:

Now, if you need to change the number of inlets and outlets of the object, you can just modify the definitions of inlets and outlets in the script's initialize method and reload. Easy as pie. Try it! Instructions can be found in the script.

Live coding and dsp

One caveat about using any of the above live-coding solutions in conjunction with Pd-Lua's signal processing capabilities is in order. When the Lua code of an object class gets reloaded, the existing code is replaced immediately. There isn't any kind of automatic "cross-fade" between old and new code. If you change the perform method of that class, there may well be discontinuities in the generated output signals which result in audible clicks. This won't matter much if you're just developing an object in your studio. But live on stage you may want to avoid this -- unless you accept or even cherish such glitches as part of your live performance.

There are ways to work around this issue, however. To demonstrate this, the tutorial examples include the following live-xfade.pd patch:

Live cross-fade

The foo~ and bar~ objects in this example are essentially the same, with some minor variations in the sound generation parameters. The particular sounds in this example are not important, each object just outputs a random walk of sine waves of varying frequencies with some phase distortion. But they will produce clicks when switching them abruptly, thus we need a smooth cross-fade between the two sound sources. This is handled by the luaxfade~ object from the Signals section.

What's special here is that the transitions are being triggered automatically, by the received reload messages. By these means, you can edit, say, the foo~ object while the bar~ object is playing, then save your edits and send a reload message. At this point the new code for foo~ is loaded while the cross-fade from bar~ to foo~ is initiated at the same time.

This method obviously requires some preparation and diligence when being used live on stage. Having some kind of automatic cross-fade functionality for dsp objects baked into Pd-Lua's run-time system would make this a lot easier. Maybe this can be provided by pdx.lua in a future release.

Conclusion

Congratulations! If you made it this far, you should have learned more than enough to start using Pd-Lua successfully for your own projects. You should also be able to read and understand the many examples in the Pd-Lua distribution, which illustrate all the various features in much more detail than we could muster in this introduction. You can find these in the examples folder, both in the Pd-Lua sources and the pdlua folder of your Pd installation.

The examples accompanying this tutorial (including the pdx.lua, pdlua-remote.el and pdlua-remote.pd files mentioned at the end of the pdx.lua section) are also available for your perusal in the examples subdirectory of the folder where you found this document.

Finally, I'd like to thank Claude Heiland-Allen for creating such an amazing tool, it makes programming Pd externals really easy and fun. Kudos also to Roberto Ierusalimschy for Lua, which for me is one of the best-designed, easiest to learn, and most capable multi-paradigm scripting languages there are today, while also being fast, simple, and light-weight.

\ No newline at end of file diff --git a/tutorial/pd-lua-intro.md b/tutorial/pd-lua-intro.md index a81dba6..855e6a5 100644 --- a/tutorial/pd-lua-intro.md +++ b/tutorial/pd-lua-intro.md @@ -1,12 +1,17 @@ +--- +author: Albert Gräf +title: A Quick Introduction to Pd-Lua +footer: ${title} - ${pageNo} / ${pageCount} +--- # A Quick Introduction to Pd-Lua Albert Gräf \<\> Computer Music Dept., Institute of Art History and Musicology Johannes Gutenberg University (JGU) Mainz, Germany -August 2024 +September 2024 -This document is licensed under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). Other formats: [Markdown](https://github.com/agraef/pd-lua/blob/master/tutorial/pd-lua-intro.md) source, [PDF](https://github.com/agraef/pd-lua/blob/master/pdlua/tutorial/pd-lua-intro.pdf) +This document is licensed under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). Other formats: [Markdown](https://github.com/agraef/pd-lua/blob/master/tutorial/pd-lua-intro.md) source, [PDF](https://agraef.github.io/pd-lua/tutorial/pd-lua-intro.pdf) Permanent link: ## Why Pd-Lua? @@ -141,7 +146,23 @@ So let's relaunch Pd, reload the patch again, and add some GUI elements to test ![Basic example](03-basic-example.png) -Note that this is still a very basic example. While the example is complete and fully functional, we have barely scratched the surface here. Pd-Lua also allows you to process an object's creation arguments (employing the `atoms` parameter of the `initialize` method, which we didn't use above), log messages and errors in the Pd console, create handlers for different types of input messages, output data to different outlets, work with Pd arrays, clocks, and receivers, and even do some live coding. We will dive into each of these topics in the following sections. +Note that this is still a very basic example. While the example is complete and fully functional, we have barely scratched the surface here. Pd-Lua also allows you to process an object's creation arguments (employing the `atoms` parameter of the `initialize` method, which we didn't use above), log messages and errors in the Pd console, create handlers for different types of input messages and signals, output data and signals to different outlets, work with Pd arrays, clocks, and receivers, and even do some graphics and live coding. We will dive into each of these topics in the rest of this tutorial. + +## Objects and Shortcuts + +You may have wondered about the whole `pd.Class:new():register("foo")` thing, why is that so complicated? The answer is that Lua uses *prototypes* (class-less objects) to define object-oriented data structures and their methods. Consequently, Pd-Lua defines a few of these to represent various kinds of Pd objects on the Lua side. There's `pd.Class` which represents Pd object classes, `pd.Table` which is used to access Pd arrays and tables, and `pd.Clock`, `pd.Receive` for Pd's clock and receiver objects. + +Each kind of object needs a way to create an instance of the object, that's what the `new()` method is for, and a second method (usually called `register`, but Pd arrays use `sync` instead) which associates the Lua object with its counterpart in Pd. The `register()` method also creates the corresponding data structure in Pd in most cases. + +So that's why we first invoke `new()` and then `register()` on the prototype (`pd.Class`, etc.); it's always this two-step process. However, since version 0.12.14, Pd-Lua also provides some convenient *shortcuts*. In the case of `pd.Class:new():register()`, the shortcut is simply `pd.class()`. (Note that `class` is written in all lower-case, while the prototype `pd.Class` has `Class` capitalized -- this may be hard to see in the font we've chosen for this document.) + +Thus, we can abbreviate the definition of the `foo` object class in our first example as: + +~~~lua +local foo = pd.class("foo") +~~~ + +Much shorter, and easy to remember. Shortcuts for the other kinds of objects will be introduced along with the long forms in the corresponding sections below. Feel free to use them. But note that in the tutorial and the accompanying examples we generally keep to the long forms, so that our examples will work unchanged with older Pd-Lua versions. ## Where your Lua files go @@ -323,17 +344,18 @@ Thanks for using foo! ### Lua errors -We all make mistakes. It's inevitable that you'll run into errors in the Lua code you wrote, so let's finally discuss how those mishaps are handled. Pd-Lua simply reports errors from the Lua interpreter in the Pd console. For instance, suppose that we mistyped `pd.post` as `pd_post` in the code for the one-time welcome message above. You'll see an error message like this in the console: +We all make mistakes. It's inevitable that you'll run into errors in the Lua code you wrote, so let's finally discuss how those mishaps are handled. Pd-Lua simply reports errors from the Lua interpreter in the Pd console. For instance, suppose that we omitted the `pd` qualifier in the one-time welcome message above, turning `pd.post` into just `post` (quite an easy mistake to make). You'll then see an error message like this in the console: ~~~ -error: pdlua_new: error in constructor for `foo': -[string "foo"]:7: attempt to call a nil value (global 'pd_post') +error: lua: foo.pd_lua: 29: attempt to call a nil value (global 'post') error: couldn't create "foo" ~~~ -In this case the error happened in the `initialize` method, so the object couldn't actually be created, and you will have to correct the typo before going on. Fortunately, the message tells us exactly where the error occurred, so we can fix it easily. Syntax errors anywhere in the script file will be caught and handled in a similar fashion. +You'll recall that an undefined global like the mistyped `post` symbol above will always yield the `nil` value in Lua, which obviously isn't a function and thus can't be called like one. (Not all error messages will be quite as perspicuous, so a web search may be in order to figure them out.) In this case the error happened in the `postinitialize` method, so the object couldn't actually be created, and you will have to correct the typo before going on. Fortunately, the message tells us exactly where the error occurred, so we can fix it easily. + +Syntax errors anywhere in the script file will be caught and handled in a similar fashion. -Runtime errors in inlet methods, on the other hand, will allow your objects to be created and to start executing; they just won't behave as expected and cause somewhat cryptic errors to be printed in the console. For instance, let's suppose that you forgot the curly braces around the float value in `self:outlet` (a fairly common error), so that the method reads: +Runtime errors in inlet methods, on the other hand, will allow your objects to be created and to start executing. They just won't behave as expected and cause a variety of error messages to be printed in the console which might look a bit cryptic at first glance. For instance, let's suppose that you forgot the curly braces around the float value in `self:outlet` (another fairly common error), so that the method reads: ~~~lua function foo:in_1_bang() @@ -342,17 +364,16 @@ function foo:in_1_bang() end ~~~ -Lua is a dynamically-typed language, so this little glitch becomes apparent only when you actually send a bang message to the object, which causes the following errors to be logged in the console: +Lua is a dynamically-typed language, so this little glitch becomes apparent only when you actually send a `bang` message to the object, which causes the following error to be logged in the console: ~~~ -error: lua: error: not a table +error: foo.pd_lua: 41: error: invalid atoms table [outlet 1] ... click the link above to track it down, or click the 'Find Last Error' item in the Edit menu. -error: lua: error: no atoms?? ~~~ -Ok, so the first message tells us that *somewhere* Pd-Lua expected a table but got a non-table value. The second message actually comes from the C routine deep down in the bowls of Pd-Lua which does the actual output to an outlet. If you see that message, it's a telltale sign that you tried to output an atom not properly wrapped in a Lua table, but it gives no indication of where that happened either, other than that you can use "Find Last Error" to locate the object which caused the problem. +This message actually comes from the C routine deep down in Pd-Lua which checks the arguments of the `outlet` method for validity before it sends any data through the outlet. In this case it took issue with the `atoms` argument. If you see that message, it's a telltale sign that you tried to output an atom not properly wrapped in a Lua table. The message also tells you about the outlet number which caused the issue, and the line number with the definition of the method in which the error happened (which is the `foo:in_1_bang` method in this case). So it's not the *exact* location of the `self:outlet()` call, but still close enough so that you can easily find that spot with the extra information provided in the error message. -It goes without saying that the Pd-Lua developers could have chosen a better error message there. Well, at least we now have an idea what happened and in which object, but we may then still have to start peppering our code with `pd.post` calls in order to find (and fix) the issue. +To actually fix the error, simply go to the line number from the error message in the editor to find the method with the error, and edit the `outlet` call to add the curly braces around `self.counter` back in. In case you already closed the editor, recall that you can just click on the error message (or use the "Find Last Error" operation) to locate the object, then right-click the object and open it. ## Inlets and outlets @@ -509,11 +530,11 @@ And here you can see the object running in a little test patch which outputs the Pd's arrays provide an efficient means to store possibly large vectors of float values. These are often used for sample data (waveforms) of a given size (the number of samples), but can also be employed to store copious amounts of numerical control data. Arrays are usually associated with a graphical display (called a *graph*), and Pd's table object lets you create an array along with a graph as a special kind of subpatch. -Pd-Lua provides a `Table` class to represent array and table data, and a few functions to query and manipulate that data. This comes in handy, e.g., if you want to fill an array with a computed waveform. While Pd has its own corresponding facilities, complicated waveforms are often much easier to create in Lua, which offers a fairly complete set of basic mathematical functions in its standard library, and a whole lot more through 3rd party libraries such as [Numeric Lua](https://github.com/carvalho/numlua). +Pd-Lua provides `pd.Table` to represent array and table data, and a few functions to query and manipulate that data. This comes in handy, e.g., if you want to fill an array with a computed waveform. While Pd has its own corresponding facilities, complicated waveforms are often much easier to create in Lua, which offers a fairly complete set of basic mathematical functions in its standard library, and a whole lot more through 3rd party libraries such as [Numeric Lua](https://github.com/michal-h21/numlua). Here are the array/table functions provided by Pd-Lua. Note that like in Pd arrays, indices are zero-based and thus range from `0` to `tab:length()-1`. -- `pd.Table:new():sync(name)`: creates the Lua representation of a Pd array and associates it with the Pd array named `name`. The result is `nil` if an array or table of that name doesn't exist. You usually assign that value to a local variable (named `tab` below) to refer to it later. +- `pd.Table:new():sync(name)`, short form: `pd.table(name)`: creates the Lua representation of a Pd array and associates it with the Pd array named `name`. This requires that an array or table of that name already exists (`sync` will *not* create it), otherwise the result is `nil`. You usually assign that value to a local variable (named `tab` below) to refer to it later. - `tab:length()`: returns the length of `tab` (i.e., the number of samples in it) @@ -586,23 +607,27 @@ In the same vein, the Pd-Lua distribution includes a much more comprehensive exa Clocks are used internally in Pd to implement objects which "do things" when a timeout occurs, such as delays, pipes, and metronomes. Pd-Lua exposes this functionality so that objects written in Lua can do the same. The following functions are provided: -- `pd.Clock:new():register(self, method)`: This creates a new clock for the Pd-Lua object `self` which, when it goes off, runs the method specified as a string `method`. Let's say that `method` is `"trigger"`, then `self:trigger()` will be called without arguments when the clock times out. You usually want to assign the result (a `pd.Clock` object) to a member variable of the object (called `self.clock` below), so that you can refer to it later. +- `pd.Clock:new():register(self, method)`, short form `pd.clock(self, method)`: This creates a new clock for the Pd-Lua object `self` which, when it goes off, runs the method specified as a string `method`. Let's say that `method` is `"trigger"`, then `self:trigger()` will be called without arguments when the clock times out. You usually want to assign the result (a `pd.Clock` object) to a member variable of the object (called `self.clock` below), so that you can refer to it later. - `self.clock:delay(time)`: sets the clock so that it will go off (and call the clock method) after `time` milliseconds -- `self.clock:set(systime)`: sets the clock so that it will go off at the specified absolute `systime` (measured in Pd "ticks", whatever that means) +- `self.clock:set(systime)`: sets the clock so that it will go off at the specified absolute `systime` (measured in Pd time units of `pd.TIMEUNITPERMSEC`, see below) - `self.clock:unset()`: unsets the clock, canceling any timeout that has been set previously -- `self.clock:destruct()`: destroys the clock; this is to be called in the `finalize` method, so that the clock doesn't go off (trying to invoke an invalid object) after an object was deleted +- `self.clock:destruct()`: destroys the clock -We mention in passing that you can call `self.clock:delay(time)` as soon as the clock has been created, even in the `initialize` method of an object. Furthermore, you can have as many clocks as you want in the same object, carrying out different actions, as long as you assign each clock to a different method. -Presumably, the `self.clock:destruct()` method should also be invoked automatically when setting `self.clock` to `nil`, but the available documentation isn't terribly clear on this. So we recommend explicitly calling `self.clock:destruct()` in `self:finalize` to be on the safe side, as the documentation advises us to do, because otherwise "weird things will happen." +--- + +**NOTE:** Calling `self.clock:destruct()` on a clock that isn't needed any more (in the `finalize` method at the latest) used to be mandatory, and is still possible, but since 0.12.12 it is no longer necessary, as it also happens automatically when the target object gets deleted. This makes it now possible to have one-shot timers like `pd.Clock:new():register(self, "oneshot"):delay(1000)` which aren't assigned to any variable. (This is not recommended, though, if you want to maintain compatibility with earlier Pd-Lua versions.) -Also note that `self.clock:set()` isn't terribly useful right now, because it refers to Pd's internal "systime" clock which isn't readily available in Pd-Lua. +--- + + +We mention in passing that you can call `self.clock:delay(time)` as soon as the clock has been created, even in the `initialize` method of an object. Furthermore, you can have as many clocks as you want in the same object, carrying out different actions, as long as you assign each clock to a different method. -With these caveats in mind, here is a little `tictoc` object we came up with for illustration purposes, along with the usual test patch. +Here is a little `tictoc` object we came up with for illustration purposes. ~~~lua local tictoc = pd.Class:new():register("tictoc") @@ -621,8 +646,6 @@ function tictoc:initialize(sel, atoms) return true end --- don't forget this, or else... - function tictoc:finalize() self.clock:destruct() end @@ -670,21 +693,32 @@ function tictoc:tictoc() end ~~~ +And here is the usual test patch: + ![Clock example](08-clock.png) + + +An explanation of `self.clock:set(systime)` is still in order. In contrast to `self.clock:delay(time)`, this schedules a timeout at the given *absolute* time given in `pd.TIMEUNITPERMSEC` units. This may be preferable in some situations, e.g., if you have to schedule some events that use increasing timestamps instead of delta times. Version 0.12.12 of Pd-Lua adds the `pd.TIMEUNITPERMSEC` constant and some support functions for dealing with these time values: + +- `pd.systime()`: Returns the current time in `pd.TIMEUNITPERMSEC` units. You can use this to derive a value for `self.clock:set`'s `systime` argument. For instance, `self.clock:set(pd.systime() + time * pd.TIMEUNITPERMSEC)` is equivalent to `self.clock:delay(time)`. +- `pd.timesince(systime)`: Returns the time since the given `systime` in msec. You might use this to measure the time between two events, like Pd's built-in `timer` object does. In particular, `pd.timesince(0)` gives you the msec equivalent of `pd.systime()`, i.e., Pd's current time in msec. + +Also note that the notion of time alluded to above is Pd's internal or *logical* time which always advances monotonically (until it wraps around) with each dsp tick, i.e., once per 64 samples with Pd's default block size setting. + More comprehensive examples using clocks can be found in the Pd-Lua distribution; have a look, e.g., at ldelay.pd_lua and luametro.pd_lua. Also, lpipe.pd_lua demonstrates how to dynamically create an entire collection of clocks in order to implement a delay line for a stream of messages. ## Using receivers As every seasoned Pd user knows, Pd also enables you to transmit messages in a wireless fashion, using receiver symbols, or just *receivers* for short, as destination addresses. In Pd, this is done through the built-in `send` and `receive` objects, as well as the "send" and "receive symbol" properties of GUI objects. -Sending messages to a receiver in Pd-Lua is straightforward: +Sending messages to a receiver is straightforward: - `pd.send(sym, sel, atoms)`: Sends a message with the given selector symbol `sel` (a string) and arguments `atoms` (a Lua table, which may be empty if the message has no arguments) to the given receiver `sym` (a string). -This works pretty much like the `outlet` method, but outputs messages to the given receiver instead. For instance, let's say you have a toggle with receiver symbol `onoff` in your patch, then you can turn on that toggle with a call like `pd.send("onoff", "float", {1})`. (Recall that the `atoms` argument always needs to be a table, even if it is a singleton, lest you'll get that dreaded "no atoms??" error that we discussed earlier). +This works pretty much like the `outlet` method, but outputs messages to the given receiver instead. For instance, let's say you have a toggle with receiver symbol `onoff` in your patch, then you can turn on that toggle with a call like `pd.send("onoff", "float", {1})`. (Recall that the `atoms` argument always needs to be a table, even if it is a singleton, lest you'll get that "invalid atoms table" error that we discussed earlier). -One complication are receiver symbols using a `$0-` patch id prefix, which are commonly used to differentiate receiver symbols in different toplevel patches or abstractions, in order to prevent name clashes. A Pd-Lua object doesn't have any idea of what toplevel patch it is located in, and what the numeric id of that patch is, so you'll have to expand the `$0-` prefix on the Pd side and pass it, e.g., as a creation argument. For instance, suppose that the toggle receiver is in fact named `$0-onoff`, then something like the following Pd-Lua object will do the trick, if you invoke it as `luasend $0-onoff`: +One complication are receiver symbols using a `$0-` patch id prefix, which are commonly used to differentiate receiver symbols in different toplevel patches or abstractions, in order to prevent name clashes. For instance, suppose that the toggle receiver is in fact named `$0-onoff`, then something like the following Pd-Lua object will do the trick, if you invoke it as `luasend $0-onoff`: ~~~lua local luasend = pd.Class:new():register("luasend") @@ -704,15 +738,17 @@ Of course, this also handles ordinary receive symbols just fine if you pass them ![Send example](09-send.png) -It is worth noting here that the same technique applies whenever you need to pass on "$" arguments to a Pd-Lua object in a Pd abstraction. +--- -So let's have a look at receivers now. These work pretty much like clocks in that you create them registering a method, and destroy them when they are no longer needed: +**NOTE:** The same technique applies whenever you need to pass on "$" arguments to a Pd-Lua object in a Pd abstraction. This also works in older Pd-Lua versions. However, since Pd-Lua 0.12.17 there's also the possibility to expand such "$" symbols directly in Lua, see [Expanding dollar symbols](#expanding-dollar-symbols) below. -- `pd.Receive:new():register(self, sym, method)`: This creates a new receiver named `sym` (a string) for the Pd-Lua object `self` which, when a message for that receiver becomes available, runs the method specified as a string `method`. Let's say that `method` is `"receive"`, then `self:receive(sel, atoms)` will be invoked with the selector symbol `sel` and arguments `atoms` of the transmitted message. You want to assign the result (a `pd.Receive` object) to a member variable of the object (called `self.recv` below), so that you can refer to it later (if only to destroy it, see below). +--- -- `self.recv:destruct()`: destroys the receiver +So let's have a look at receivers now. These work pretty much like clocks in that you create them registering a method, and destroy them when they are no longer needed: -Note that the same caveat applies to receivers as in the case of clocks. That is, you should use the `destruct` method to destroy receivers in the `finalize` routine of the receiving object, so that they don't hang around when their object is long dead. Otherwise, you guessed it, "weird things will happen." +- `pd.Receive:new():register(self, sym, method)`, short form `pd.receive(self, sym, method)`: This creates a new receiver named `sym` (a string) for the Pd-Lua object `self` which, when a message for that receiver becomes available, runs the method specified as a string `method`. Let's say that `method` is `"receive"`, then `self:receive(sel, atoms)` will be invoked with the selector symbol `sel` and arguments `atoms` of the transmitted message. You want to assign the result (a `pd.Receive` object) to a member variable of the object (called `self.recv` below), so that you can refer to it later (if only to destroy it, see below). + +- `self.recv:destruct()`: destroys the receiver (as with clocks, this is now entirely optional, but it may still be useful if the receiver needs to be removed before its target object ceases to be) Here is a little example which receives any kind of message, stores it, and outputs the last stored message when it gets a `bang` on its inlet. @@ -753,6 +789,54 @@ The obligatory test patch: ![Receive example](10-receive.png) +### Expanding dollar symbols + +Some more in-depth information about dollar symbols and their use in Lua is in order. Specifically, we will discuss two new Pd-Lua object methods, `canvas_realizedollar()` (available since Pd-Lua 0.12.17) and `set_args()` (available since 0.12.0) which help you manage your receiver and sender symbols. A third related method is `get_args()`, available since 0.12.18, which we also mention in passing for the sake of the completeness, but will not use in our example. These helper methods are technically part of Pd-Lua's graphics API (see [Graphics](#graphics) below) since they are often used in conjunction with graphical objects. But they work just as well with ordinary (non-graphical) Lua objects and come in handy here, that's why we introduce them now: + +- `self:canvas_realizedollar(symbol)`: Expands "$" symbols in the `symbol` string argument and returns the resulting string. +- `self:set_args(atoms)`: Sets the given list `atoms` of numbers and strings as the object's creation arguments, and updates the display of the object on the canvas accordingly. +- `self:get_args()`: Returns the object's current creation arguments as a list of numbers and strings in the same format as the argument of `set_args()`. This means that `self:set_args(self:get_args())` will just re-create the existing list of creation arguments, which by itself isn't all that useful. But you can manipulate the table returned by `get_args()` using Lua's table functions before passing the resulting table to `set_args()`. In particular, this allows you to replace certain arguments while keeping others. + +In case you're not familiar with "$" symbols in Pd, let's first discuss the basics; more information can be found in the Pd manual. The patch id `$0` is widely used in Pd for send and receive names to avoid conflicts with other receivers and senders of similar names. Pd expands `$0` to the canvas id, which differs for every open patch, and it expands `$1`, `$2` etc. if the corresponding creation arguments are set in the context of an abstraction or clone instance. (Also note that in Purr Data there is another `$@` symbol which expands to the list of all creation arguments.) + +If you set the sender or receiver names for your Pd-Lua object as *creation arguments* of the object, Pd automatically expands them for you as discussed above. But if you want to set them through *messages* to the object, you will often use a quoted symbol like `\$0-foo`, so that your object receives the symbol unexpanded. You've probably seen this before, as it is also common practice, e.g., with Pd's GUI objects like sliders, radio buttons, etc. (e.g., see escaping-characters in the message help patch for an example). + +Quoting dollar symbols in this way offers the advantage that you can record the "$" symbol itself (rather than its expansion) in the object's properties (in the case of Pd GUI objects) or in its creation arguments (as can be done with Pd-Lua's `set_args()` method). But of course this means that if you want to use the actual receiver symbol in your Lua object then you need to expand the symbol in Lua (which is what the `canvas_realizedollar()` method is for). + +Here's a simple example illustrating this: + +~~~lua +local localsend = pd.Class:new():register("localsend") + +function localsend:initialize(sel, atoms) + self.inlets = 1 + -- pass the symbol from the creation argument, + -- which gets automatically expanded here + self.sender = tostring(atoms[1]) + return true +end + +function localsend:in_1_sender(x) + local sendername = tostring(x[1]) + + -- store the original name as argument (like "\$0-foo") + self:set_args({sendername}) + + -- apply the expanded name with the local id + self.sender = self:canvas_realizedollar(sendername) +end + +function localsend:in_1_bang() + pd.send(self.sender, "bang", {}) +end +~~~ + +And here's a test patch from the tutorial examples which shows this object in action: + +![Dollar symbol example](19-dollar-symbols.png) + +Try clicking on the `sender` messages in the patch and watch the symbol argument of the object change accordingly. Clicking the bang object labeled "click me" will then send the bang message to one of the two toggles on the right, depending on the `sender` message you clicked. + ## Signals and graphics So far all of our examples only did control processing, which is what Pd-Lua was originally designed for. But thanks to the work of Timothy Schoen (the creator and main developer of [plugdata](https://plugdata.org/)), as of version 0.12.0 Pd-Lua also provides facilities for audio signal processing and graphics. It goes without saying that these capabilities vastly extend the scope of Pd-Lua applications, as you can now program pretty much any kind of Pd object in Lua, covering both signal and control processing, as well as custom GUI objects. We'll first discuss how to write a signal processing (a.k.a. dsp) object in Pd-Lua, and then go on to show the implementation of a simple GUI object using the graphics API. @@ -767,11 +851,11 @@ So far all of our examples only did control processing, which is what Pd-Lua was Enabling signal processing in a Pd-Lua object involves three ingredients: -1. **Adding signal inlets and outlets:** As before, this is done by setting the `inlets` and `outlets` member variables in the `initialize` method. But instead of setting these variables to just the numbers of inlets and outlets, you set them to a table which indicates the signal and control in- and outlets with the special `SIGNAL` and `DATA` values. The number of in- and outlets is then given by the size of these tables. Thus, e.g., you'd use `self.inlets = { SIGNAL, SIGNAL, DATA }` if you need two signal and one control data inlet, in that order. +1. **Adding signal inlets and outlets:** As before, this is done by setting the `inlets` and `outlets` member variables in the `initialize` method. But instead of setting each variable to just a number, you specify a *signature*, which is a table indicating the signal and control in- and outlets with the special `SIGNAL` and `DATA` values. The number of in- and outlets is then given by the size of these tables. Thus, e.g., you'd use `self.inlets = { SIGNAL, SIGNAL, DATA }` if you need two signal and one control data inlet, in that order. Note that a number as the value of `inlets` or `outlets` corresponds to a signature with just `DATA` values in it. 2. **Adding a dsp method:** This step is optional. The `dsp` method gets invoked whenever signal processing is turned on in Pd, passing two parameters: `samplerate` and `blocksize`. The former tells you about the sample rate (number of audio samples per second) Pd runs at, which will be useful if your object needs to translate time and frequency values from physical units (i.e., seconds, milliseconds, and Hz) to sample-based time and frequency values, so usually you want to store the given value in a member variable of your object. The latter specifies the block size, i.e., the number of samples Pd expects to be processed during each call of the `perform` method (see below). You only need to store that number if your object doesn't have any signal inlets, so that you know how many samples need to be generated. Otherwise the block size can also be inferred from the size of the `in` tables passed to the `perform` method. Adding the `dsp` method is optional. You only have to define it if the signal and control data processing in your object requires the `samplerate` and `blocksize` values, or if you need to be notified when dsp processing gets turned on for some other reason. -3. **Adding a perform method:** This method is where the actual signal processing happens. If your object outputs any signal data, the `perform` method needs to be implemented, otherwise you'll get lots of errors in the Pd console as soon as you turn on dsp processing. The object receives blocks of signal data from the inlets as (numeric) Lua table arguments, and needs to return a tuple with the blocks of signal data for each outlet, again as Lua tables. +3. **Adding a perform method:** This method is where the actual signal processing happens. It receives blocks of signal data from the inlets through its arguments, where each block is represented as a Lua table containing floating point sample values. The method then needs to return a tuple of similar Lua tables with the blocks of signal data for each outlet. Note that the number of *arguments* of the method matches the number of signal *inlets*, while the number of *return values* corresponds to the number of signal *outlets*. The `perform` method is *not* optional; if your object outputs any signal data, the method needs to be implemented, otherwise you'll get a `perform: function should return a table` or similar error in the Pd console as soon as you turn on dsp processing. -In addition to the `dsp` and `perform` methods, your object may contain any number of methods doing the usual control data processing on the `DATA` inlets. It is also possible to receive control data on the `SIGNAL` inlets; however, you won't be able to receive `float` messages on those inlets, because Pd interprets float data on signal inlets as constant signals, so they get passed as blocks of signal data to the `perform` method instead. +In addition to the `dsp` and `perform` methods, your object may contain any number of methods doing the usual control data processing on the `DATA` inlets. It is also possible to receive control data on the `SIGNAL` inlets; however, you won't be able to receive `float` messages, because they will be interpreted as constant signals which get passed as blocks of signal data to the `perform` method instead. #### Example 1: Mixing signals @@ -802,7 +886,7 @@ Note that here we replaced the signal data in the `in1` table with the result, s Easy enough. And this is how this object works in a little test patch: -![17-signal1](17-signal1.png) +![Mixing signals](17-signal1.png) #### Example 2: Analyzing a signal @@ -827,7 +911,7 @@ end A little test patch: -![17-signal2](17-signal2.png) +![Analyzing a signal](17-signal2.png) #### Example 3: Generating a signal @@ -875,14 +959,14 @@ end The obligatory test patch: -![17-signal3](17-signal3.png) +![Generating a signal](17-signal3.png) #### Real-world example: Cross-fades -Ok, the previous example got a bit more complicated, but it's still rather straightforward if you know how to compute a sine wave as blocks of signal data. But the above are really just toy examples for illustration purposes. Let's finally take a look at a somewhat more realistic and useful example which performs cross-fades on two incoming signals by doing linear interpolation between the input signals. A customary design for this kind of dsp object is to have a target cross-fade value (xfade argument) ranging from 0 (all left signal) to 1 (all right signal). We also want to be able to smoothly ramp from one cross-fade value to the next in order to avoid clicks (time argument), and have an initial delay until moving to the new xfade value (delay argument). Here is the definition of a `luaxfade` object which does all this, including the checking of all argument values, so that we don't run into any Lua exceptions because of bad values. The example also illustrates how to receive control messages on a signal inlet (cf. the `fade` message on the left signal inlet). You can find this as luaxfade.pd_lua in the tutorial examples. +Let's finally take a look at a somewhat more realistic and useful example which performs cross-fades on two incoming signals by doing linear interpolation between the input signals. A customary design for this kind of dsp object is to have a target cross-fade value (`xfade` argument) ranging from 0 (all left signal) to 1 (all right signal). We also want to be able to smoothly ramp from one cross-fade value to the next in order to avoid clicks (`time` argument), and have an initial delay until moving to the new xfade value (`delay` argument). Here is the definition of a `luaxfade~` object which does all this, including the checking of all argument values, so that we don't run into any Lua exceptions because of bad values. The example also illustrates how to receive control messages on a signal inlet (cf. the `fade` message on the left signal inlet). You can find this as luaxfade~.pd_lua in the tutorial examples. ~~~lua -local luaxfade = pd.Class:new():register("luaxfade") +local luaxfade = pd.Class:new():register("luaxfade~") function luaxfade:initialize(sel, atoms) self.inlets = {SIGNAL,SIGNAL} @@ -905,7 +989,7 @@ function luaxfade:in_1_fade(atoms) -- been run yet, then we cannot compute the sample delay and ramp times -- below, so we bail out, telling the user to enable dsp first. if not self.samplerate then - self:error("luaxfade: unknown sample rate, please enable dsp first") + self:error("luaxfade~: unknown sample rate, please enable dsp first") return end local fade, time, delay = table.unpack(atoms) @@ -972,7 +1056,7 @@ end And here's the luaxfade.pd patch which takes a sine wave on the left and a noise signal on the right inlet and performs cross fades with a ramp time of 500 msec and an initial delay of 200 msec. To adjust these values, just edit the `fade` message accordingly. -![17-signal4](17-signal4.png) +![Cross-fades](17-signal4.png) ### Graphics @@ -980,7 +1064,7 @@ Timothy Schoen's Pd-Lua graphics API provides you with a way to equip an object In order to enable graphics in a Pd-Lua object, you have to provide a `paint` method. This receives a graphics context `g` as its argument, which lets you set the current color, and draw text and the various different geometric shapes using that color. In addition, you can provide methods to be called in response to mouse down, up, move, and drag actions on the object, which is useful to equip your custom GUI objects with mouse-based interaction. -Last but not least, Pd-Lua also provides the `set_args` method which lets you store internal object state in the object's creation arguments, which is useful if you need to keep track of persistent state when storing an object on disk (via saving the patch) or when duplicating or copying objects. This can also be used with ordinary Pd-Lua objects which don't utilize the graphics API, but it is most useful in the context of custom GUI objects. +Last but not least, the `set_args` method lets you store internal object state in the object's creation arguments, while `get_args` lets you retrieve those arguments. This is useful if you need to keep track of persistent state when storing an object on disk (via saving the patch) or when duplicating or copying objects. Also, their companion `canvas_realizedollar` method allows you to expand symbols containing "$" patch arguments like `$0`, `$1`, etc. These three are often combined, but they can also be used separately, and they work just as well with ordinary Pd-Lua objects which don't utilize the graphics API. In fact we've already introduced them while discussing receivers, see [Expanding dollar symbols](#expanding-dollar-symbols) above, so we won't go into them again; however, check the circle-gui example included in the distribution to see how to store Lua object configuration data persistently in the creation arguments of a custom GUI object. We use a custom GUI object, a simple kind of dial, as a running example to illustrate most of these elements in the following subsections. To keep things simple, we will not discuss the graphics API in much detail here, so you may want to check the graphics subpatch in the main pdlua-help patch, which contains a detailed listing of all available methods for reference. @@ -988,7 +1072,7 @@ We use a custom GUI object, a simple kind of dial, as a running example to illus Let's begin with a basic clock-like dial: just a circular *face* and a border around it, on which we draw a *center point* and the *hand* (a line) starting at the center point and pointing in any direction which indicates the current *phase* angle. So this is what we are aiming for: -![18-graphics1](18-graphics1.png) +![A basic dial](18-graphics1.png) Following the clock paradigm, we assume that a zero phase angle means pointing upwards (towards the 12 o'clock position), while +1 or -1 indicates the 6 o'clock position, pointing downwards. Phase angles less than -1 or greater than +1 wrap around. Positive phase differences denote clockwise, negative differences counter-clockwise rotation. And since we'd like to change the phase angle displayed on the dial, we add an inlet taking float values. @@ -1036,7 +1120,9 @@ function dial:paint(g) end ~~~ -The existence of the `paint` method tells Pd-Lua that this is a graphical object. As mentioned before, this method receives a graphics context `g` as argument. The graphics context is an internal data structure keeping track of the the graphics state of the object, which is used to invoke all the graphics operations. The `set_color` method of the graphics context is used to set the color for all drawing operations; in the case of `fill` operations it fills the graphical element with the color, while in the case of `stroke` operations it draws its border. There's just one color value, so we need to set the desired fill color in case of `fill`, and the desired stroke color in case of `stroke` operations. The color values 0 and 1 we use in this example are predefined, and indicate the default background color (usually white) and default foreground color (usually black), respectively. It is possible to choose other colors by calling `g:set_color(r, g, b)` with rgb color values instead, with each r, g, b value ranging from 0 to 255 (i.e., a byte value). For instance, the color "teal" would be specified as 0, 128, 128, the color "orange" as 255, 165, 0, "black" as 0, 0, 0, and "white" as 255, 255, 255. (The API also lets you specify rgba values, including an alpha a.k.a. opacity value a, but the alpha component will only work in plugdata right now, and even that I'm not really sure of. Thus it is best ignored for now.) +The existence of the `paint` method tells Pd-Lua that this is a graphical object. As mentioned before, this method receives a graphics context `g` as argument. The graphics context is an internal data structure keeping track of the graphics state of the object, which is used to invoke all the graphics operations. The `set_color` method of the graphics context is used to set the color for all drawing operations; in the case of `fill` operations it fills the graphical element with the color, while in the case of `stroke` operations it draws its border. There's just one color value, so we need to set the desired fill color in case of `fill`, and the desired stroke color in case of `stroke` operations. The color values 0 and 1 we use in this example are predefined, and indicate the default background color (usually white) and default foreground color (usually black), respectively. + +It is possible to choose other colors by calling `g:set_color(r, g, b)` with rgb color values instead, with each r, g, b value ranging from 0 to 255 (i.e., a byte value). For instance, the color "teal" would be specified as 0, 128, 128, the color "orange" as 255, 165, 0, "black" as 0, 0, 0, "white" as 255, 255, 255, etc. It's also possible to add a fourth *alpha* a.k.a. opacity value a, which is a floating point number in the range 0-1, where 0 means fully transparent, 1 fully opaque, and any value in between will blend in whatever is behind the graphical element to varying degrees. As of Pd-Lua 0.12.7, alpha values are fully supported in both plugdata and purr-data. In vanilla Pd they are simply ignored at present, so all graphical objects will be opaque no matter what alpha value you specify. Let's now take a closer look at the drawing operations themselves. We start out by filling the entire object rectangle, which is our drawing surface, with the default background color 0, using `g:fill_all()`. This operation is special in that it not only fills the entire object rectangle, but also creates a standard border rectangle around it. If you skip this, you'll get an object without border, which may be useful at times. @@ -1132,7 +1218,7 @@ end Easy as pie. Here's how our patch looks like now: -![18-graphics2](18-graphics2.png) +![Adding an outlet](18-graphics2.png) #### Mouse actions @@ -1177,31 +1263,37 @@ end And here's the same patch again, which now lets us drag the hand to change the phase value: -![18-graphics3](18-graphics3.png) +![Dial with mouse interaction](18-graphics3.png) #### More dial action: clocks and speedometers Now that our dial object is basically finished, let's do something interesting with it. The most obvious thing is to just turn it into a clock (albeit one with just a seconds hand) counting off the seconds. For that we just need to add a metro object which increments the phase angle and sends the value to the dial each second: -![18-graphics4](18-graphics4.png) +![A clock](18-graphics4.png) -In the patch, we store the current phase angle in the `phase` variable, so that it can be updated easily using the `expr` object by just assigning to the variable. Note that we also update the variable whenever the dial outputs a new value, so you can also drag around the hand to determine its starting phase. +Pd lets us store the phase angle in a named variable (`v phase`) which can be recalled in an `expr` object doing the necessary calculations. The `expr` object sends the computed value to the `phase` receiver, which updates both the variable and the upper numbox, and the numbox then updates the dial. Note that we also set the variable whenever the dial outputs a new value, so you can also drag around the hand to determine the starting phase. And we added a `0` message to reset the hand to the 12 o'clock home position when needed. Here's another little example, rather useless but fun, simulating a speedometer which just randomly moves the needle left and right: -![18-graphics5](18-graphics5.png) +![A speedometer](18-graphics5.png) I'm sure you can imagine many more creative uses for this simple but surprisingly versatile little GUI object, which we did in just a few lines of fairly simple Lua code. Have fun with it! An extended version of this object, which covers some more features of the graphics API that we didn't discuss here to keep things simple, can be found as dial.pd and dial.pd_lua in the tutorial examples: -![18-graphics6](18-graphics6.png) +![Extended dial example](18-graphics6.png) The extended example adds messages for resizing the object and setting colors, and also shows how to save and restore object state in the creation arguments using the `set_args()` method mentioned at the beginning of this section. The accompanying patch covers all the examples we discussed here, and adds a third example showing how to utilize our dial object as a dB meter. ## Live coding -I've been telling you all along that in order to make Pd-Lua pick up changes you made to your .pd_lua files, you have to relaunch Pd and reload your patches. Well, in this section we are going to discuss Pd-Lua's *live coding* features, which let you modify your sources and have Pd-Lua reload them on the fly, without ever exiting the Pd environment. This rapid incremental style of development is one of the hallmark features of dynamic programming environments like Pd and Lua. Musicians also like to employ it to modify their algorithmic composition programs live on stage, which is where the term "live coding" comes from. You'll probably be using live coding a lot while developing your Pd-Lua externals, but I've kept this topic for the final section of this guide, because it requires a good understanding of Pd-Lua's basic features. So without any further ado, let's dive right into it now. +I've been telling you all along that in order to make Pd-Lua pick up changes you made to your .pd_lua files, you have to relaunch Pd and reload your patches. Well, as you probably guessed by now, this isn't actually true. So in this section we are going to cover Pd-Lua's *live coding* features, which let you modify your sources and have Pd-Lua reload them on the fly, without ever exiting the Pd environment. This rapid incremental style of development is one of the hallmark features of dynamic interactive programming environments like Pd and Lua. Musicians also like to employ it to modify their programs live on stage, which is where the term "live coding" comes from. + +I've kept this topic for the final section of this guide, because it is somewhat advanced, and there are several different (traditional and new) methods available which differ in capabilities and ease of use. However, in modern Pd-Lua versions there *is* one preferred method which rules them all, so if you want to cut to the chase as quickly as possible, then feel free to just skip ahead to the [pdx.lua](#pdx.lua) section now. -First, we need to describe the predefined Pd-Lua object classes `pdlua` and `pdluax`, so that you know what's readily available. We'll also discuss how to add a `reload` message to your existing object definitions. This is quite easy to do by directly employing Pd-Lua's `dofile` method, which is also what both `pdlua` and `pdluax` use internally. These methods all work with older Pd-Lua versions, but they require a lot of manual fiddling with the Lua source and are are thus arduous and error-prone. That's why we also provide the special pdx.lua module which automatizes the entire process and is much less work than all the other approaches (basically, you just add two lines to your script and be done with it). Hence this is the method we recommend for all modern Pd-Lua versions. We describe it last so that you can also gather a good understanding of the "legacy" live coding methods on offer. But if you want something that just works with minimal effort, feel free to skip ahead to the [pdx.lua](#pdx.lua) section below which should be readable without any prior knowledge of the other approaches. +If you're still here, then you probably want to learn about the other methods, too. So in the following we first describe the predefined Pd-Lua object classes `pdlua` and `pdluax`, so that you know the "traditional" live-coding instruments that Pd-Lua had on offer for a long time. We also discuss how to add a `reload` message to your existing object definitions. This is quite easy to do by directly employing Pd-Lua's `dofile` function, which is also what both `pdlua` and `pdluax` use internally. + +These methods all work with pretty much any Pd-Lua version out there, but require a little bit of setup which can get time-consuming and error-prone if you're dealing with large patches involving a lot of different Lua objects. + +That's why Pd-Lua nowadays includes an extension module called *pdx.lua* that "just works" out of the box and automatizes everything, so that you only have to put a simple `reload` message in your main patch and be done with it. This is also the method we recommend, to novices and expert users alike. We describe it last so that you can also gather a good understanding of Pd-Lua's traditional live coding methods, and learn to appreciate them. These are still used in many older scripts, and Pd-Lua will continue to support them for backward compatibility. ### pdlua @@ -1272,13 +1364,13 @@ Now change that `+` operator to `-`: Don't forget to save your edits, then go back to the patch and recreate the `pdluax foo` object on the left. The quickest way to do that is to just delete the object, then use Pd's "Undo" operation, Ctrl+Z. Et voilà: the new object now decrements the counter rather than incrementing it. Also note that the other object on the right still runs the old code which increments the counter; thus you will have to give that object the same treatment if you want to update it, too. -While `pdluax` was considered Pd-Lua's main workhorse for live coding, it has its quirks. Most notably, the syntax is different from regular object definitions, so you have to change the code if you want to turn it into a .pd_lua file. Also, having to recreate an object to reload the script file is quite disruptive (it resets the internal state of the object), and may leave objects in an inconsistent state (different objects may use various different versions of the same script file). Sometimes this may be what you want, but it makes `pdluax` somewhat difficult to use. It's not really tailored for interactive development, but it shines if you need a specialized tool for changing your objects on a whim in a live situation. +While `pdluax` was considered Pd-Lua's main workhorse for live coding in the past, it has its quirks. Most notably, the syntax is different from regular object definitions, so you have to change the code if you want to turn it into a .pd_lua file. Also, having to recreate an object to reload the script file is quite disruptive (it resets the internal state of the object), and may leave objects in an inconsistent state (different objects may use various different versions of the same script file). Sometimes this may be what you want, but it makes `pdluax` somewhat difficult to use. It's not really tailored for interactive development, but it shines if you need a specialized tool for changing your objects on a whim in a live situation. -Fortunately, if you're not content with Pd-Lua's built-in facilities for live coding, it's easy to roll your own using the internal `dofile` method, which is discussed in the next subsection. +Fortunately, if you're not content with Pd-Lua's traditional facilities for live coding, it's easy to roll your own using the internal `dofile` method, which is discussed in the next subsection. ### dofile and dofilex -So let's discuss how to use `dofile` in a direct fashion. Actually, we're going to use its companion `dofilex` here, which works the same as `dofile`, but loads Lua code relative to the "externdir" of the class (the directory of the .pd_lua file) rather than the directory of the Pd canvas with the pdlua object, which is what `dofile` does. Normally, you don't have to worry about these intricacies, but they *will* matter if Lua class names are specified using relative pathnames, such as `../foo` or `bar/baz`. Since we're reloading class definitions here, it's better to use `dofilex` so that our code doesn't break if we later change the directory structure. +So let's discuss how to use `dofile` in a direct fashion. Actually, we're going to use its companion `dofilex` here, which works the same as `dofile`, but loads Lua code relative to the "externdir" of the class (the directory of the .pd_lua file) rather than the directory of the Pd canvas with the pdlua object, which is what `dofile` does. Normally, this won't make much of a difference, but it *will* matter if Lua class names are specified using relative pathnames, such as `../foo` or `bar/baz`. Since we're reloading class definitions here, it's better to use `dofilex` so that our objects don't break if we move things about. The method we sketch out below is really simple and doesn't have any of the drawbacks of the `pdluax` object, but you still have to add a small amount of boilerplate code to your existing object definition. Here is how `dofilex` is invoked: @@ -1320,7 +1412,7 @@ end Now launch the luatab.pd patch and connect a `reload` message to the `luatab wave` object, like so: -![Livecoding example 1](13-livecoding1.png) +![Live-coding with dofilex 1](13-livecoding1.png) Next change the wavetable function to whatever you want, e.g.: @@ -1332,25 +1424,19 @@ Next change the wavetable function to whatever you want, e.g.: Return to the patch, click the `reload` message, and finally reenter the frequency value, so that the waveform gets updated: -![Livecoding example 2](14-livecoding2.png) +![Live-coding with dofilex 2](14-livecoding2.png) ### pdx.lua -The method sketched out in the preceding subsection works well enough for simple patches. However, having to manually wire up the `reload` message to one object of each class that you're editing is still quite cumbersome. In a big patch, which is being changed all the time, this quickly becomes unwieldy. Wouldn't it be nice if we could equip each object with a special receiver, so that we can just click a message somewhere in the patch to reload a given class, or even all Pd-Lua objects at once? And maybe even do that remotely from the editor, using the `pdsend` program? - -Well, all this is in fact possible, but the implementation is a bit too involved to fully present it here. So we have provided this in a separate pdx.lua module, which you can find in the sources accompanying this tutorial. Setting up an object for this kind of remote control is easy, though. First, you need to import the `pdx` module into your script, using Lua's `require`: +The method sketched out in the preceding subsection works well enough for simple patches. However, having to manually wire up the `reload` message to one object of each class that you're editing is still quite cumbersome. In a big patch, which is being changed all the time, this quickly becomes unwieldy. Wouldn't it be nice if we could equip each object with a special receiver, so that we can just click a message somewhere in the patch to reload a given class, or even all Pd-Lua objects at once? Or even send that message via the `pdsend` program, e.g., from the text editor in which you edit the Lua source of your object? -~~~lua -local pdx = require 'pdx' -~~~ - -Then just call `pdx.reload(self)` somewhere in the `initialize` method. This will set up the whole receiver/dofilex machinery in a fully automatic manner. Finally, add a message like this to your patch, which goes to the special `pdluax` receiver (note that this is completely unrelated to the `pdluax` object discussed previously, it just incidentally uses the same name): +Well, all this is in fact possible, but the implementation is a bit too involved to fully present here. So we have provided this in a separate pdx.lua module, which you can find in the sources accompanying this tutorial for your perusal. As of Pd-Lua 0.12.8, pdx.lua is pre-loaded and all the required setup is performed automatically. You only have to add a message like the following to your patch, which goes to the special `pdluax` receiver (note that this is unrelated to the `pdluax` object discussed previously, it just incidentally uses the same name): ~~~ ; pdluax reload ~~~ -When clicked, this just reloads all Pd-Lua objects in the patch, provided they have been set up with `pdx.reload`. You can also specify the class to be reloaded (the receiver matches this against each object's class name): +When clicked, this just reloads all Pd-Lua objects in the patch. You can also specify the class to be reloaded (the receiver matches this against each object's class name): ~~~ ; pdluax reload foo @@ -1362,88 +1448,68 @@ Or maybe name several classes, like so: ; pdluax reload foo, reload bar ~~~ -You get the idea. Getting set up for remote control via `pdsend` isn't much harder. E.g., let's say that we use UDP port 4711 on localhost for communicating with Pd, then you just need to connect `netreceive 4711 1` to the `; pdluax reload` message in a suitable way, e.g.: +You get the idea. Getting set up for remote control via `pdsend` isn't much harder. E.g., let's say that we use UDP port 4711 on localhost for communicating with Pd, then you just need to connect `netreceive 4711 1` to the `pdluax` receiver in a suitable way. Let's use the luatab.pd_lua object from the previous subsection as an example. You can remove the `in_1_reload` handler from that script -- it's no longer needed, as pdx.lua now dispatches the `reload` messages for us. Here's how the revised patch looks like: + +![Live-coding with remote control](15-remote-control1.png) -![Remote control 1](15-remote-control1.png) +This doesn't look any simpler than before, but it also does a whole lot more. Clicking the message not just reloads the `luatab` script, but *any* Lua object you have running, in *any* of the patches you have opened. And you can now use `pdsend 4711 localhost udp` to reload your Lua objects from literally anywhere. Any decent code editor will let you bind a keyboard command which does this for you. Myself, I'm a die-hard Emacs fan, so I've included a little elisp module *pd-remote.el* which shows how to do this. Once you've added this to your .emacs, you can just type Ctrl+C Ctrl+K in any Lua buffer to make Pd reload your Lua scripts after editing them. It doesn't get much easier than that. -You can then use `pdsend 4711 localhost udp` to transmit the `reload` message to Pd when needed. You probably don't want to run those commands yourself, but a decent code editor will let you bind a keyboard command which does this for you. Myself, I'm a die-hard Emacs fan, so I've included a little elisp module pd-remote.el which shows how to do this. Once you've added this to your .emacs, you can just type Ctrl+C Ctrl+K in Emacs to make Pd reload your Lua script after saving it. It doesn't get much easier than that. +In addition, I've provided a little abstraction named *pd-remote.pd* which takes care of adding the `netreceive` and messaging bits and also looks much tidier in your patches. Using the abstraction is easy: Insert `pd-remote` into the patch you're working on, and (optionally) connect a `pdluax reload` message (without the `;` prefix) to the inlet of the abstraction; or use something like `pdluax reload foo` to reload a specific object class. Now you can just click on that message to reload your script files, and the abstraction will also respond to such messages on port 4711 (the port number can be changed in the abstraction if needed). Here's how that looks like in a patch: -Moreover, for your convenience I've also added a little abstraction named pd-remote.pd which takes care of the `netreceive` and messaging bits and will look much tidier in your patches. Using the abstraction is easy: Insert `pd-remote` into the patch you're working on, and connect a `pdluax reload` message (without the `;` prefix) to the inlet of the abstraction. Now you can just click on that message to reload your script files. In fact any of the variations of reload messages discussed above will work, if you remove the `;` prefix, and the abstraction will also respond to such messages on port 4711 by default (the port number can be changed in the abstraction if needed). +![Live-coding with pd-remote.pd](15-remote-control2.png) --- -**NOTE:** As of Pd-Lua 0.12.5, using pdx.lua has become much easier, since pdx.lua, pd-remote.el, and pd-remote.pd get installed in your pdlua external directory, and this directory now also gets searched for Lua imports. Thus `require 'pdx'` will just work in all your Pd-Lua scripts without any further ado. To make Pd find the pd-remote.pd abstraction, you'll still have to copy it to your project directory. Or you can add the pdlua directory to your Pd library path. (Depending on how Pd-Lua was installed, you may want to do that anyway so that the pdlua directory shows up in Pd's help browser.) +**NOTE:** To make Pd find the pd-remote.pd abstraction without having to copy it to your project directory, you can add the pdlua external directory (which is where the abstraction gets installed) to your Pd library path, either in your Pd setup, or inside the patch with a `declare -stdpath` object, as shown above. -The pd-remote.el file can be installed in your Emacs site-lisp directory if needed. Please also check the [pd-remote](https://github.com/agraef/pd-remote) repository on GitHub for the latest pd-remote version and further details. This also includes a pointer to a Visual Studio Code extension written by Baris Altun which can be used as a replacement for pd-remote.el if you're not familiar with Emacs, or just prefer to use VS Code as an editor. +The pd-remote.el file can be installed in your Emacs site-lisp directory if needed. However, the easiest way to install it is from [MELPA](https://melpa.org/), a large repository of Emacs packages. Please also check the [pd-remote](https://github.com/agraef/pd-remote) repository on GitHub for the latest pd-remote version and further details. This also includes a pointer to a Visual Studio Code extension written by Baris Altun which can be used as a replacement for pd-remote.el if you prefer VS Code for editing. --- -So here's the full source code of our reworked `luatab` example (now with the `in_1_reload` handler removed and the `pdx.reload` call added to the `initialize` method): +And here's a little gif showing the above patch in action. You may want to watch this in [Typora](https://www.typora.io/) or your favorite web browser to make the animation work. -~~~lua -local luatab = pd.Class:new():register("luatab") +![Live-coding in action](16-remote-control2.gif) --- our own pdlua extension, needed for the reload functionality -local pdx = require 'pdx' +So there you have it: Not one, not two, but three different ways to live-code with Pd-Lua (or four, if you count in the `pdlua` object). pdx.lua certainly is the most advanced and user-friendly solution among these, but you can choose whatever best fits your purpose and is the most convenient for you. -function luatab:initialize(sel, atoms) - -- single inlet for the frequency, bang goes to the single outlet when we - -- finished generating a new waveform - self.inlets = 1 - self.outlets = 1 - -- enable the reload callback - pdx.reload(self) - -- the name of the array/table should be in the 1st creation argument - if type(atoms[1]) == "string" then - self.tabname = atoms[1] - return true - else - self:error(string.format("luatab: expected array name, got %s", - tostring(atoms[1]))) - return false - end -end +### Object reinitialization in pdx.lua -function luatab:in_1_float(freq) - if type(freq) == "number" then - -- the waveform we want to compute, adjust this as needed - local function f(x) - return math.sin(2*math.pi*freq*(x+1))/(x+1) - end - -- get the Pd array and its length - local t = pd.Table:new():sync(self.tabname) - if t == nil then - self:error(string.format("luatab: array or table %s not found", - self.tabname)) - return - end - local l = t:length() - -- Pd array indices are zero-based - for i = 0, l-1 do - -- normalize arguments to the 0-1 range - t:set(i, f(i/l)) - end - -- this is needed to update the graph display - t:redraw() - -- output a bang to indicate that we've generated a new waveform - self:outlet(1, "bang", {}) - else - self:error(string.format("luatab: expected frequency, got %s", - tostring(freq))) - end +If pdx.lua reloads a script file, it normally does *not* run the `initialize` method. This is by design, as we want the reload process to be as smooth as possible while running a patch, and invoking the `initialize` method could potentially be quite disruptive, as it usually reinitializes all your member variables. + +However, pdx.lua has another trick up its sleeve if you do need some kind of custom reinitialization. There are two callback methods that you can implement, `prereload` and `postreload` which will be invoked immediately before and after reloading the script file, respectively. Either method can be used to reinitialize your objects during reloading in any desired way. The only difference between the two methods is that `prereload` still runs in the old script, while `postreload` executes the new code which has just been loaded. Typically you'd use `prereload` to perform any required bookkeeping or state-saving before the script gets loaded, and `postreload` for custom initialization afterwards. + +In particular, these callbacks can change any member variables, either directly or by invoking other methods. The most important use cases probably are to change the `inlets` and `outlets` variables, and to add a `paint` method, which can be done on the fly to reconfigure your objects accordingly. To these ends, you can just call the `initialize` method from `postreload`. To demonstrate this, in the tutorial examples you'll find a pdxtest.pd patch and pdxtest~.pd_lua script. The script has the following `reload` method: + +~~~lua +function pdxtest:postreload() + -- stuff to do post-reload goes here + pd.post("Reloaded!") + -- instead of doing a full initialization, you could also just change the + -- number of inlets and outlets here + self:initialize() end ~~~ -And here's a little gif showing the above patch in action. You may want to watch this in [Typora](https://www.typora.io/) or your favorite web browser to make the animation work. +Now, if you need to change the number of inlets and outlets of the object, you can just modify the definitions of `inlets` and `outlets` in the script's `initialize` method and reload. Easy as pie. Try it! Instructions can be found in the script. + +### Live coding and dsp + +One caveat about using any of the above live-coding solutions in conjunction with Pd-Lua's [signal processing capabilities](#signals) is in order. When the Lua code of an object class gets reloaded, the existing code is replaced immediately. There isn't any kind of automatic "cross-fade" between old and new code. If you change the `perform` method of that class, there may well be discontinuities in the generated output signals which result in audible clicks. This won't matter much if you're just developing an object in your studio. But live on stage you may want to avoid this -- unless you accept or even cherish such glitches as part of your live performance. + +There are ways to work around this issue, however. To demonstrate this, the tutorial examples include the following live-xfade.pd patch: + +![Live cross-fade](17-live-xfade.png) + +The `foo~` and `bar~` objects in this example are essentially the same, with some minor variations in the sound generation parameters. The particular sounds in this example are not important, each object just outputs a random walk of sine waves of varying frequencies with some phase distortion. But they *will* produce clicks when switching them abruptly, thus we need a smooth cross-fade between the two sound sources. This is handled by the [`luaxfade~`](#real-world-example-cross-fades) object from the Signals section. -![Remote control 2](16-remote-control2.gif) +What's special here is that the transitions are being triggered automatically, *by the received reload messages*. By these means, you can edit, say, the `foo~` object while the `bar~` object is playing, then save your edits and send a `reload` message. At this point the new code for `foo~` is loaded while the cross-fade from `bar~` to `foo~` is initiated at the same time. -So there you have it: four different ways to live-code with Pd-Lua. Choose whatever best fits your purpose and is the most convenient for you. +This method obviously requires some preparation and diligence when being used live on stage. Having some kind of automatic cross-fade functionality for dsp objects baked into Pd-Lua's run-time system would make this a lot easier. Maybe this can be provided by pdx.lua in a future release. ## Conclusion Congratulations! If you made it this far, you should have learned more than enough to start using Pd-Lua successfully for your own projects. You should also be able to read and understand the many examples in the Pd-Lua distribution, which illustrate all the various features in much more detail than we could muster in this introduction. You can find these in the examples folder, both in the Pd-Lua sources and the pdlua folder of your Pd installation. -The examples accompanying this tutorial (including the pdx.lua, pdlua-remote.el and pdlua-remote.pd files mentioned at the end of the previous section) are also available for your perusal in the examples subdirectory of the folder where you found this document. +The examples accompanying this tutorial (including the pdx.lua, pdlua-remote.el and pdlua-remote.pd files mentioned at the end of the pdx.lua section) are also available for your perusal in the examples subdirectory of the folder where you found this document. -Finally, I'd like to thank Claude Heiland-Allen for creating such an amazing tool, it makes programming Pd externals really easy and fun. Kudos also to Roberto Ierusalimschy for Lua, which for me is one of the best-designed, easiest to learn, and most capable multi-paradigm scripting languages there are today, while also being fast, simple, and light-weight. +Finally, I'd like to thank Claude Heiland-Allen for creating such an amazing tool, it makes programming Pd externals really easy and fun. Kudos also to Roberto Ierusalimschy for Lua, which for me is one of the best-designed, easiest to learn, and most capable multi-paradigm scripting languages there are today, while also being fast, simple, and light-weight. \ No newline at end of file