aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--COPYING674
-rw-r--r--COPYING.LGPL501
-rw-r--r--Makefile31
-rw-r--r--README.md26
-rw-r--r--config.ld14
-rw-r--r--config/config.mk33
-rw-r--r--doc/classes/buffer.html247
-rw-r--r--doc/classes/delay.html270
-rw-r--r--doc/classes/gain.html179
-rw-r--r--doc/classes/interp.html322
-rw-r--r--doc/classes/ladspa.html298
-rw-r--r--doc/classes/mixer.html139
-rw-r--r--doc/classes/nullsink.html188
-rw-r--r--doc/classes/oscil.html392
-rw-r--r--doc/classes/pan.html240
-rw-r--r--doc/classes/playback.html331
-rw-r--r--doc/classes/sample.html203
-rw-r--r--doc/classes/serial.html161
-rw-r--r--doc/classes/timer.html226
-rw-r--r--doc/index.html153
-rw-r--r--doc/ldoc_fixed.css312
-rw-r--r--doc/modules/biquad.html189
-rw-r--r--doc/modules/expression.html221
-rw-r--r--doc/modules/fs.html138
-rw-r--r--doc/modules/keycallback.html133
-rw-r--r--doc/modules/udp.html311
-rw-r--r--external/ladspa.h603
-rw-r--r--lib/distrib.lua38
-rw-r--r--lib/functional.lua171
-rw-r--r--lib/graphviz.lua48
-rw-r--r--lib/ladspa_interface.lua63
-rw-r--r--lib/lazy.lua24
-rw-r--r--lib/midimap.lua34
-rw-r--r--lib/osc.lua76
-rw-r--r--lib/util.lua139
-rw-r--r--mod/biquad.c202
-rw-r--r--mod/buffer.c281
-rw-r--r--mod/delay.c233
-rw-r--r--mod/expression.c500
-rw-r--r--mod/fs.c103
-rw-r--r--mod/gain.c164
-rw-r--r--mod/interp.c257
-rw-r--r--mod/keycallback.c147
-rw-r--r--mod/ladspa.c604
-rw-r--r--mod/lookback.c106
-rw-r--r--mod/map.h49
-rw-r--r--mod/mixer.c114
-rw-r--r--mod/mutil.h69
-rw-r--r--mod/null.c76
-rw-r--r--mod/nullsink.c111
-rw-r--r--mod/oscil.c438
-rw-r--r--mod/pan.c187
-rw-r--r--mod/passthrough.c82
-rw-r--r--mod/playback.c342
-rw-r--r--mod/sample.c180
-rw-r--r--mod/sample.h33
-rw-r--r--mod/serial.c511
-rw-r--r--mod/spring.c173
-rw-r--r--mod/timer.c152
-rw-r--r--mod/udp.c391
61 files changed, 12634 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..140f8cf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*.so
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..f288702
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/COPYING.LGPL b/COPYING.LGPL
new file mode 100644
index 0000000..f6683e7
--- /dev/null
+++ b/COPYING.LGPL
@@ -0,0 +1,501 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Moe Ghoul>, 1 April 1990
+ Moe Ghoul, President of Vice
+
+That's all there is to it!
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..d002594
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,31 @@
+include config.mk
+
+CMODULE_PREFIX ?= cmodules
+LUAMODULE_PREFIX ?= luamodules
+CFLAGS := $(shell pkg-config --cflags melisma.h) -std=c11 -D_XOPEN_SOURCE=700 -D_GNU_SOURCE
+LDFLAGS := -shared -fPIC -lm
+CMODULES := $(CSOURCES:%.c=%.so)
+
+all: $(CMODULES)
+
+mod/sample.so: CFLAGS += $(shell pkg-config --cflags samplerate sndfile)
+mod/sample.so: LDFLAGS += $(shell pkg-config --libs samplerate sndfile)
+
+mod/%.so: mod/%.c
+ @ printf "CCLD\t%s\n" $(@)
+ @ $(CC) -o $(@) $(<) $(CFLAGS) $(LDFLAGS)
+
+install: all
+ install -Dm644 -t $(CMODULE_PREFIX)/melisma $(CMODULES)
+ install -Dm644 -t $(LUAMODULE_PREFIX)/melisma $(LUASOURCES)
+
+debug: CFLAGS += -Wno-cpp -O0 -g
+debug: all
+
+doc:
+ ldoc .
+
+clean:
+ rm -rf mod/*.so
+
+.PHONY: install clean doc
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6a5f63e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,26 @@
+# melisma-modules
+
+A collection of modules for [melisma](/melisma).
+Documentation is located [here](/melisma-modules/doc).
+
+## Building and installing
+
+Make sure melisma is installed and that melisma.h.pc is in PKG_CONFIG_PATH.
+Copy config/config.mk to the root directory.
+Edit config.mk to customize which modules will be
+built/installed. Then build and install with:
+
+```bash
+$ make install CMODULE_PREFIX=... LUAMODULE_PREFIX=...
+```
+
+Some modules may require additional system libraries.
+Which modules need which libraries should be evident from the Makefile.
+
+To make modules available for import, make sure the installation
+prefixes are in LUA_PATH and LUA_CPATH.
+
+## Licensing
+
+melisma is licensed under GPL v3 or later (see COPYING).
+ladspa.h is licensed under LGPL 2.1 or later (see COPYING.LGPL).
diff --git a/config.ld b/config.ld
new file mode 100644
index 0000000..ab7e841
--- /dev/null
+++ b/config.ld
@@ -0,0 +1,14 @@
+project = 'Melisma modules'
+title = 'Melisma modules'
+description = 'Melisma module reference'
+format = 'markdown'
+no_space_before_args = 'show'
+sort = true
+style = '!fixed'
+
+postprocess_html = function(html, module)
+ return (html:gsub(
+ '<div id="navigation">',
+ '<div id="navigation">\n<p><a href="/melisma-modules">Back to repository</a></p>',
+ 1))
+end
diff --git a/config/config.mk b/config/config.mk
new file mode 100644
index 0000000..c0b14ca
--- /dev/null
+++ b/config/config.mk
@@ -0,0 +1,33 @@
+LUASOURCES := \
+ lib/distrib.lua \
+ lib/functional.lua \
+ lib/graphviz.lua \
+ lib/ladspa_interface.lua \
+ lib/lazy.lua \
+ lib/midimap.lua \
+ lib/osc.lua \
+ lib/util.lua \
+
+CSOURCES := \
+ mod/biquad.c \
+ mod/buffer.c \
+ mod/delay.c \
+ mod/expression.c \
+ mod/fs.c \
+ mod/gain.c \
+ mod/interp.c \
+ mod/keycallback.c \
+ mod/ladspa.c \
+ mod/lookback.c \
+ mod/mixer.c \
+ mod/null.c \
+ mod/nullsink.c \
+ mod/oscil.c \
+ mod/pan.c \
+ mod/passthrough.c \
+ mod/playback.c \
+ mod/sample.c \
+ mod/serial.c \
+ mod/spring.c \
+ mod/timer.c \
+ mod/udp.c \
diff --git a/doc/classes/buffer.html b/doc/classes/buffer.html
new file mode 100644
index 0000000..0811641
--- /dev/null
+++ b/doc/classes/buffer.html
@@ -0,0 +1,247 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+<head>
+ <title>Melisma modules</title>
+ <link rel="stylesheet" href="../ldoc_fixed.css" type="text/css" />
+</head>
+<body>
+
+<div id="container">
+
+<div id="product">
+ <div id="product_logo"></div>
+ <div id="product_name"><big><b></b></big></div>
+ <div id="product_description"></div>
+</div> <!-- id="product" -->
+
+
+<div id="main">
+
+
+<!-- Menu -->
+
+<div id="navigation">
+<p><a href="/melisma-modules">Back to repository</a></p>
+<br/>
+<h1>Melisma modules</h1>
+
+
+<ul>
+ <li><a href="../index.html">Index</a></li>
+</ul>
+
+<h2>Contents</h2>
+<ul>
+<li><a href="#Functions">Functions</a></li>
+<li><a href="#Methods">Methods</a></li>
+</ul>
+
+
+<h2>Classes</h2>
+<ul class="nowrap">
+ <li><strong>buffer</strong></li>
+ <li><a href="../classes/delay.html">delay</a></li>
+ <li><a href="../classes/gain.html">gain</a></li>
+ <li><a href="../classes/interp.html">interp</a></li>
+ <li><a href="../classes/ladspa.html">ladspa</a></li>
+ <li><a href="../classes/mixer.html">mixer</a></li>
+ <li><a href="../classes/nullsink.html">nullsink</a></li>
+ <li><a href="../classes/oscil.html">oscil</a></li>
+ <li><a href="../classes/pan.html">pan</a></li>
+ <li><a href="../classes/playback.html">playback</a></li>
+ <li><a href="../classes/sample.html">sample</a></li>
+ <li><a href="../classes/serial.html">serial</a></li>
+ <li><a href="../classes/timer.html">timer</a></li>
+</ul>
+<h2>Modules</h2>
+<ul class="nowrap">
+ <li><a href="../modules/biquad.html">biquad</a></li>
+ <li><a href="../modules/expression.html">expression</a></li>
+ <li><a href="../modules/fs.html">fs</a></li>
+ <li><a href="../modules/keycallback.html">keycallback</a></li>
+ <li><a href="../modules/udp.html">udp</a></li>
+</ul>
+
+</div>
+
+<div id="content">
+
+<h1>Class <code>buffer</code></h1>
+<p>Audio buffer for capture/playback.</p>
+<p>
+
+
+
+<pre>
+<span class="keyword">local</span> mbuffer = <span class="global">require</span> <span class="string">'melisma.buffer'</span>
+<span class="keyword">local</span> mevent = <span class="global">require</span> <span class="string">'melisma.event'</span>
+
+<span class="keyword">local</span> input = ...
+
+<span class="comment">-- Create a buffer with capacity of 5 seconds
+</span><span class="comment">-- Capture stops automatically when buffer is full.
+</span><span class="keyword">local</span> len = <span class="number">5</span>
+<span class="keyword">local</span> buffer = mbuffer.<span class="function-name">init</span>(input, len)
+
+<span class="comment">-- Capture audio
+</span>buffer:<span class="function-name">capture</span>()
+
+<span class="comment">-- Playback after capture 3 seconds
+</span>mevent.<span class="function-name">schedule</span>(<span class="number">3</span>, buffer.playback, buffer)
+</pre>
+
+</p>
+
+
+<h2><a href="#Functions">Functions</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#buffer.init">buffer.init(input, len)</a></td>
+ <td class="summary">Create an audio buffer.</td>
+ </tr>
+</table>
+<h2><a href="#Methods">Methods</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#buffer:capture">buffer:capture()</a></td>
+ <td class="summary">Start capture mode.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#buffer:length">buffer:length()</a></td>
+ <td class="summary">Get length of captured audio.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#buffer:playback">buffer:playback()</a></td>
+ <td class="summary">Start playback mode.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#buffer:speed">buffer:speed(speed)</a></td>
+ <td class="summary">Set playback speed.</td>
+ </tr>
+</table>
+
+<br/>
+<br/>
+
+
+ <h2 class="section-header "><a name="Functions"></a>Functions</h2>
+ Methods
+ <dl class="function">
+ <dt>
+ <a name = "buffer.init"></a>
+ <strong>buffer.init(input, len)</strong>
+ </dt>
+ <dd>
+ Create an audio buffer.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">input</span>
+ <span class="types"><span class="type">channel</span></span>
+ Input signal.
+ </li>
+ <li><span class="parameter">len</span>
+ <span class="types"><span class="type">number</span></span>
+ Buffer capacity in seconds.
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../classes/buffer.html#">buffer</a></span>
+ The newly created audio buffer.
+ </ol>
+
+
+
+
+</dd>
+</dl>
+ <h2 class="section-header "><a name="Methods"></a>Methods</h2>
+
+ <dl class="function">
+ <dt>
+ <a name = "buffer:capture"></a>
+ <strong>buffer:capture()</strong>
+ </dt>
+ <dd>
+ Start capture mode.
+
+
+
+
+
+
+
+</dd>
+ <dt>
+ <a name = "buffer:length"></a>
+ <strong>buffer:length()</strong>
+ </dt>
+ <dd>
+ Get length of captured audio.
+
+
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><span class="type">number</span></span>
+ Length of captured audio in seconds.
+ </ol>
+
+
+
+
+</dd>
+ <dt>
+ <a name = "buffer:playback"></a>
+ <strong>buffer:playback()</strong>
+ </dt>
+ <dd>
+ Start playback mode.
+
+
+
+
+
+
+
+</dd>
+ <dt>
+ <a name = "buffer:speed"></a>
+ <strong>buffer:speed(speed)</strong>
+ </dt>
+ <dd>
+ Set playback speed.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">speed</span>
+ <span class="types"><span class="type">number</span></span>
+ Playback speed.
+ </li>
+ </ul>
+
+
+
+
+
+</dd>
+</dl>
+
+
+</div> <!-- id="content" -->
+</div> <!-- id="main" -->
+<div id="about">
+<i>generated by <a href="http://github.com/lunarmodules/LDoc">LDoc 1.5.0</a></i>
+<i style="float:right;">Last updated 1980-01-01 00:00:00 </i>
+</div> <!-- id="about" -->
+</div> <!-- id="container" -->
+</body>
+</html>
diff --git a/doc/classes/delay.html b/doc/classes/delay.html
new file mode 100644
index 0000000..3a5470d
--- /dev/null
+++ b/doc/classes/delay.html
@@ -0,0 +1,270 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+<head>
+ <title>Melisma modules</title>
+ <link rel="stylesheet" href="../ldoc_fixed.css" type="text/css" />
+</head>
+<body>
+
+<div id="container">
+
+<div id="product">
+ <div id="product_logo"></div>
+ <div id="product_name"><big><b></b></big></div>
+ <div id="product_description"></div>
+</div> <!-- id="product" -->
+
+
+<div id="main">
+
+
+<!-- Menu -->
+
+<div id="navigation">
+<p><a href="/melisma-modules">Back to repository</a></p>
+<br/>
+<h1>Melisma modules</h1>
+
+
+<ul>
+ <li><a href="../index.html">Index</a></li>
+</ul>
+
+<h2>Contents</h2>
+<ul>
+<li><a href="#Functions">Functions</a></li>
+<li><a href="#Methods">Methods</a></li>
+</ul>
+
+
+<h2>Classes</h2>
+<ul class="nowrap">
+ <li><a href="../classes/buffer.html">buffer</a></li>
+ <li><strong>delay</strong></li>
+ <li><a href="../classes/gain.html">gain</a></li>
+ <li><a href="../classes/interp.html">interp</a></li>
+ <li><a href="../classes/ladspa.html">ladspa</a></li>
+ <li><a href="../classes/mixer.html">mixer</a></li>
+ <li><a href="../classes/nullsink.html">nullsink</a></li>
+ <li><a href="../classes/oscil.html">oscil</a></li>
+ <li><a href="../classes/pan.html">pan</a></li>
+ <li><a href="../classes/playback.html">playback</a></li>
+ <li><a href="../classes/sample.html">sample</a></li>
+ <li><a href="../classes/serial.html">serial</a></li>
+ <li><a href="../classes/timer.html">timer</a></li>
+</ul>
+<h2>Modules</h2>
+<ul class="nowrap">
+ <li><a href="../modules/biquad.html">biquad</a></li>
+ <li><a href="../modules/expression.html">expression</a></li>
+ <li><a href="../modules/fs.html">fs</a></li>
+ <li><a href="../modules/keycallback.html">keycallback</a></li>
+ <li><a href="../modules/udp.html">udp</a></li>
+</ul>
+
+</div>
+
+<div id="content">
+
+<h1>Class <code>delay</code></h1>
+<p>Digital delay line.</p>
+<p>
+
+</p>
+
+
+<h2><a href="#Functions">Functions</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#delay.init">delay.init(input)</a></td>
+ <td class="summary">Create a delay line.</td>
+ </tr>
+</table>
+<h2><a href="#Methods">Methods</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#delay:delay">delay:delay(d)</a></td>
+ <td class="summary">Set delay.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#delay:feedback">delay:feedback(f)</a></td>
+ <td class="summary">Set feedback.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#delay:gain">delay:gain(g)</a></td>
+ <td class="summary">Set gain.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#delay:wetdrymix">delay:wetdrymix(wd)</a></td>
+ <td class="summary">Set wet/dry mix.</td>
+ </tr>
+</table>
+
+<br/>
+<br/>
+
+
+ <h2 class="section-header "><a name="Functions"></a>Functions</h2>
+ Methods
+ <dl class="function">
+ <dt>
+ <a name = "delay.init"></a>
+ <strong>delay.init(input)</strong>
+ </dt>
+ <dd>
+ Create a delay line.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">input</span>
+ <span class="types"><span class="type">channel</span></span>
+ Input signal.
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../classes/delay.html#">delay</a></span>
+ Newly created delay line.
+ </ol>
+
+
+
+
+</dd>
+</dl>
+ <h2 class="section-header "><a name="Methods"></a>Methods</h2>
+
+ <dl class="function">
+ <dt>
+ <a name = "delay:delay"></a>
+ <strong>delay:delay(d)</strong>
+ </dt>
+ <dd>
+ Set delay.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">d</span>
+ <span class="types"><span class="type">number</span></span>
+ delay in seconds.
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../classes/delay.html#">delay</a></span>
+
+
+
+ </ol>
+
+
+
+
+</dd>
+ <dt>
+ <a name = "delay:feedback"></a>
+ <strong>delay:feedback(f)</strong>
+ </dt>
+ <dd>
+ Set feedback.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">f</span>
+ <span class="types"><span class="type">number</span></span>
+ Feedback.
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../classes/delay.html#">delay</a></span>
+
+
+
+ </ol>
+
+
+
+
+</dd>
+ <dt>
+ <a name = "delay:gain"></a>
+ <strong>delay:gain(g)</strong>
+ </dt>
+ <dd>
+ Set gain.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">g</span>
+ <span class="types"><span class="type">number</span></span>
+ Gain (<strong>TODO</strong>: Use decibels).
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../classes/delay.html#">delay</a></span>
+
+
+
+ </ol>
+
+
+
+
+</dd>
+ <dt>
+ <a name = "delay:wetdrymix"></a>
+ <strong>delay:wetdrymix(wd)</strong>
+ </dt>
+ <dd>
+ Set wet/dry mix.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">wd</span>
+ <span class="types"><span class="type">number</span></span>
+ Wet/dry mix.
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../classes/delay.html#">delay</a></span>
+
+
+
+ </ol>
+
+
+
+
+</dd>
+</dl>
+
+
+</div> <!-- id="content" -->
+</div> <!-- id="main" -->
+<div id="about">
+<i>generated by <a href="http://github.com/lunarmodules/LDoc">LDoc 1.5.0</a></i>
+<i style="float:right;">Last updated 1980-01-01 00:00:00 </i>
+</div> <!-- id="about" -->
+</div> <!-- id="container" -->
+</body>
+</html>
diff --git a/doc/classes/gain.html b/doc/classes/gain.html
new file mode 100644
index 0000000..46bea50
--- /dev/null
+++ b/doc/classes/gain.html
@@ -0,0 +1,179 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+<head>
+ <title>Melisma modules</title>
+ <link rel="stylesheet" href="../ldoc_fixed.css" type="text/css" />
+</head>
+<body>
+
+<div id="container">
+
+<div id="product">
+ <div id="product_logo"></div>
+ <div id="product_name"><big><b></b></big></div>
+ <div id="product_description"></div>
+</div> <!-- id="product" -->
+
+
+<div id="main">
+
+
+<!-- Menu -->
+
+<div id="navigation">
+<p><a href="/melisma-modules">Back to repository</a></p>
+<br/>
+<h1>Melisma modules</h1>
+
+
+<ul>
+ <li><a href="../index.html">Index</a></li>
+</ul>
+
+<h2>Contents</h2>
+<ul>
+<li><a href="#Functions">Functions</a></li>
+</ul>
+
+
+<h2>Classes</h2>
+<ul class="nowrap">
+ <li><a href="../classes/buffer.html">buffer</a></li>
+ <li><a href="../classes/delay.html">delay</a></li>
+ <li><strong>gain</strong></li>
+ <li><a href="../classes/interp.html">interp</a></li>
+ <li><a href="../classes/ladspa.html">ladspa</a></li>
+ <li><a href="../classes/mixer.html">mixer</a></li>
+ <li><a href="../classes/nullsink.html">nullsink</a></li>
+ <li><a href="../classes/oscil.html">oscil</a></li>
+ <li><a href="../classes/pan.html">pan</a></li>
+ <li><a href="../classes/playback.html">playback</a></li>
+ <li><a href="../classes/sample.html">sample</a></li>
+ <li><a href="../classes/serial.html">serial</a></li>
+ <li><a href="../classes/timer.html">timer</a></li>
+</ul>
+<h2>Modules</h2>
+<ul class="nowrap">
+ <li><a href="../modules/biquad.html">biquad</a></li>
+ <li><a href="../modules/expression.html">expression</a></li>
+ <li><a href="../modules/fs.html">fs</a></li>
+ <li><a href="../modules/keycallback.html">keycallback</a></li>
+ <li><a href="../modules/udp.html">udp</a></li>
+</ul>
+
+</div>
+
+<div id="content">
+
+<h1>Class <code>gain</code></h1>
+<p>Apply gain to signals.</p>
+<p>
+
+
+<pre>
+<span class="keyword">local</span> mgain = <span class="global">require</span> <span class="string">'melisma.gain'</span>
+
+<span class="comment">-- Interprets <code>cntl</code> as decibels and applies it to <code>input</code>
+</span><span class="keyword">local</span> input = ...
+<span class="keyword">local</span> cntl = ...
+<span class="keyword">local</span> gain = mgain.<span class="function-name">db</span>(input,cntl)
+</pre>
+
+</p>
+
+
+<h2><a href="#Functions">Functions</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#gain.db">gain.db(input, gain)</a></td>
+ <td class="summary">Creates a decibel gain unit.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#gain.linear">gain.linear(input, gain)</a></td>
+ <td class="summary">Creates a linear gain unit.</td>
+ </tr>
+</table>
+
+<br/>
+<br/>
+
+
+ <h2 class="section-header "><a name="Functions"></a>Functions</h2>
+ Methods
+ <dl class="function">
+ <dt>
+ <a name = "gain.db"></a>
+ <strong>gain.db(input, gain)</strong>
+ </dt>
+ <dd>
+ Creates a decibel gain unit.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">input</span>
+ <span class="types"><span class="type">channel</span></span>
+ Input signal.
+ </li>
+ <li><span class="parameter">gain</span>
+ <span class="types"><span class="type">number</span> or <span class="type">channel</span></span>
+ Constant gain in decibels or a controlling signal.
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../classes/gain.html#">gain</a></span>
+ The newly created gain unit.
+ </ol>
+
+
+
+
+</dd>
+ <dt>
+ <a name = "gain.linear"></a>
+ <strong>gain.linear(input, gain)</strong>
+ </dt>
+ <dd>
+ Creates a linear gain unit.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">input</span>
+ <span class="types"><span class="type">channel</span></span>
+ Input signal.
+ </li>
+ <li><span class="parameter">gain</span>
+ <span class="types"><span class="type">number</span> or <span class="type">channel</span></span>
+ Constant gain or a controlling signal.
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../classes/gain.html#">gain</a></span>
+ The newly created gain unit.
+ </ol>
+
+
+
+
+</dd>
+</dl>
+
+
+</div> <!-- id="content" -->
+</div> <!-- id="main" -->
+<div id="about">
+<i>generated by <a href="http://github.com/lunarmodules/LDoc">LDoc 1.5.0</a></i>
+<i style="float:right;">Last updated 1980-01-01 00:00:00 </i>
+</div> <!-- id="about" -->
+</div> <!-- id="container" -->
+</body>
+</html>
diff --git a/doc/classes/interp.html b/doc/classes/interp.html
new file mode 100644
index 0000000..cea6277
--- /dev/null
+++ b/doc/classes/interp.html
@@ -0,0 +1,322 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+<head>
+ <title>Melisma modules</title>
+ <link rel="stylesheet" href="../ldoc_fixed.css" type="text/css" />
+</head>
+<body>
+
+<div id="container">
+
+<div id="product">
+ <div id="product_logo"></div>
+ <div id="product_name"><big><b></b></big></div>
+ <div id="product_description"></div>
+</div> <!-- id="product" -->
+
+
+<div id="main">
+
+
+<!-- Menu -->
+
+<div id="navigation">
+<p><a href="/melisma-modules">Back to repository</a></p>
+<br/>
+<h1>Melisma modules</h1>
+
+
+<ul>
+ <li><a href="../index.html">Index</a></li>
+</ul>
+
+<h2>Contents</h2>
+<ul>
+<li><a href="#Functions">Functions</a></li>
+<li><a href="#Methods">Methods</a></li>
+</ul>
+
+
+<h2>Classes</h2>
+<ul class="nowrap">
+ <li><a href="../classes/buffer.html">buffer</a></li>
+ <li><a href="../classes/delay.html">delay</a></li>
+ <li><a href="../classes/gain.html">gain</a></li>
+ <li><strong>interp</strong></li>
+ <li><a href="../classes/ladspa.html">ladspa</a></li>
+ <li><a href="../classes/mixer.html">mixer</a></li>
+ <li><a href="../classes/nullsink.html">nullsink</a></li>
+ <li><a href="../classes/oscil.html">oscil</a></li>
+ <li><a href="../classes/pan.html">pan</a></li>
+ <li><a href="../classes/playback.html">playback</a></li>
+ <li><a href="../classes/sample.html">sample</a></li>
+ <li><a href="../classes/serial.html">serial</a></li>
+ <li><a href="../classes/timer.html">timer</a></li>
+</ul>
+<h2>Modules</h2>
+<ul class="nowrap">
+ <li><a href="../modules/biquad.html">biquad</a></li>
+ <li><a href="../modules/expression.html">expression</a></li>
+ <li><a href="../modules/fs.html">fs</a></li>
+ <li><a href="../modules/keycallback.html">keycallback</a></li>
+ <li><a href="../modules/udp.html">udp</a></li>
+</ul>
+
+</div>
+
+<div id="content">
+
+<h1>Class <code>interp</code></h1>
+<p>Interpolation.</p>
+<p>
+
+
+
+<pre>
+<span class="keyword">local</span> minterp = <span class="global">require</span> <span class="string">'melisma.interp'</span>
+<span class="keyword">local</span> mevent = <span class="global">require</span> <span class="string">'melisma.event'</span>
+<span class="keyword">local</span> mgain = <span class="global">require</span> <span class="string">'melisma.gain'</span>
+
+<span class="keyword">local</span> interp = minterp.<span class="function-name">init</span>(<span class="number">0</span>)
+
+interp:<span class="function-name">linear</span>(<span class="number">1</span>,<span class="number">1</span>)
+
+mevent.<span class="function-name">schedule</span>(<span class="number">0.5</span>, <span class="keyword">function</span>()
+ <span class="global">print</span>(<span class="string">'current value:'</span>, interp:<span class="function-name">sample</span>())
+<span class="keyword">end</span>)
+
+<span class="keyword">local</span> sig = ...
+<span class="keyword">local</span> gain = mgain.<span class="function-name">linear</span>(sig,interp)
+</pre>
+
+</p>
+
+
+<h2><a href="#Functions">Functions</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#interp.init">interp.init(x0)</a></td>
+ <td class="summary">Initialize an interpolation unit.</td>
+ </tr>
+</table>
+<h2><a href="#Methods">Methods</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#interp:ease_in">interp:ease_in(v, t)</a></td>
+ <td class="summary">Interpolate using ease-in interpolation.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#interp:ease_out">interp:ease_out(v, t)</a></td>
+ <td class="summary">Interpolate using ease-out interpolation.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#interp:linear">interp:linear(v, t)</a></td>
+ <td class="summary">Interpolate using linear interpolation.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#interp:sample">interp:sample()</a></td>
+ <td class="summary">Get the current value.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#interp:set">interp:set(x)</a></td>
+ <td class="summary">Set value directly.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#interp:smoothstep">interp:smoothstep(v, t)</a></td>
+ <td class="summary">Interpolate using smoothstep interpolation.</td>
+ </tr>
+</table>
+
+<br/>
+<br/>
+
+
+ <h2 class="section-header "><a name="Functions"></a>Functions</h2>
+ Methods
+ <dl class="function">
+ <dt>
+ <a name = "interp.init"></a>
+ <strong>interp.init(x0)</strong>
+ </dt>
+ <dd>
+ Initialize an interpolation unit.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">x0</span>
+ <span class="types"><span class="type">number</span></span>
+ Initial value.
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../classes/interp.html#">interp</a></span>
+ The newly created interpolation unit.
+ </ol>
+
+
+
+
+</dd>
+</dl>
+ <h2 class="section-header "><a name="Methods"></a>Methods</h2>
+
+ <dl class="function">
+ <dt>
+ <a name = "interp:ease_in"></a>
+ <strong>interp:ease_in(v, t)</strong>
+ </dt>
+ <dd>
+ Interpolate using ease-in interpolation.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">v</span>
+ <span class="types"><span class="type">number</span></span>
+ Target value.
+ </li>
+ <li><span class="parameter">t</span>
+ <span class="types"><span class="type">number</span></span>
+ Interpolation time.
+ </li>
+ </ul>
+
+
+
+
+
+</dd>
+ <dt>
+ <a name = "interp:ease_out"></a>
+ <strong>interp:ease_out(v, t)</strong>
+ </dt>
+ <dd>
+ Interpolate using ease-out interpolation.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">v</span>
+ <span class="types"><span class="type">number</span></span>
+ Target value.
+ </li>
+ <li><span class="parameter">t</span>
+ <span class="types"><span class="type">number</span></span>
+ Interpolation time.
+ </li>
+ </ul>
+
+
+
+
+
+</dd>
+ <dt>
+ <a name = "interp:linear"></a>
+ <strong>interp:linear(v, t)</strong>
+ </dt>
+ <dd>
+ Interpolate using linear interpolation.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">v</span>
+ <span class="types"><span class="type">number</span></span>
+ Target value.
+ </li>
+ <li><span class="parameter">t</span>
+ <span class="types"><span class="type">number</span></span>
+ Interpolation time.
+ </li>
+ </ul>
+
+
+
+
+
+</dd>
+ <dt>
+ <a name = "interp:sample"></a>
+ <strong>interp:sample()</strong>
+ </dt>
+ <dd>
+ Get the current value.
+
+
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><span class="type">number</span></span>
+ The current value.
+ </ol>
+
+
+
+
+</dd>
+ <dt>
+ <a name = "interp:set"></a>
+ <strong>interp:set(x)</strong>
+ </dt>
+ <dd>
+ Set value directly.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">x</span>
+ <span class="types"><span class="type">number</span></span>
+ New value.
+ </li>
+ </ul>
+
+
+
+
+
+</dd>
+ <dt>
+ <a name = "interp:smoothstep"></a>
+ <strong>interp:smoothstep(v, t)</strong>
+ </dt>
+ <dd>
+ Interpolate using smoothstep interpolation.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">v</span>
+ <span class="types"><span class="type">number</span></span>
+ Target value.
+ </li>
+ <li><span class="parameter">t</span>
+ <span class="types"><span class="type">number</span></span>
+ Interpolation time.
+ </li>
+ </ul>
+
+
+
+
+
+</dd>
+</dl>
+
+
+</div> <!-- id="content" -->
+</div> <!-- id="main" -->
+<div id="about">
+<i>generated by <a href="http://github.com/lunarmodules/LDoc">LDoc 1.5.0</a></i>
+<i style="float:right;">Last updated 1980-01-01 00:00:00 </i>
+</div> <!-- id="about" -->
+</div> <!-- id="container" -->
+</body>
+</html>
diff --git a/doc/classes/ladspa.html b/doc/classes/ladspa.html
new file mode 100644
index 0000000..846b65b
--- /dev/null
+++ b/doc/classes/ladspa.html
@@ -0,0 +1,298 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+<head>
+ <title>Melisma modules</title>
+ <link rel="stylesheet" href="../ldoc_fixed.css" type="text/css" />
+</head>
+<body>
+
+<div id="container">
+
+<div id="product">
+ <div id="product_logo"></div>
+ <div id="product_name"><big><b></b></big></div>
+ <div id="product_description"></div>
+</div> <!-- id="product" -->
+
+
+<div id="main">
+
+
+<!-- Menu -->
+
+<div id="navigation">
+<p><a href="/melisma-modules">Back to repository</a></p>
+<br/>
+<h1>Melisma modules</h1>
+
+
+<ul>
+ <li><a href="../index.html">Index</a></li>
+</ul>
+
+<h2>Contents</h2>
+<ul>
+<li><a href="#Functions">Functions</a></li>
+<li><a href="#Methods">Methods</a></li>
+</ul>
+
+
+<h2>Classes</h2>
+<ul class="nowrap">
+ <li><a href="../classes/buffer.html">buffer</a></li>
+ <li><a href="../classes/delay.html">delay</a></li>
+ <li><a href="../classes/gain.html">gain</a></li>
+ <li><a href="../classes/interp.html">interp</a></li>
+ <li><strong>ladspa</strong></li>
+ <li><a href="../classes/mixer.html">mixer</a></li>
+ <li><a href="../classes/nullsink.html">nullsink</a></li>
+ <li><a href="../classes/oscil.html">oscil</a></li>
+ <li><a href="../classes/pan.html">pan</a></li>
+ <li><a href="../classes/playback.html">playback</a></li>
+ <li><a href="../classes/sample.html">sample</a></li>
+ <li><a href="../classes/serial.html">serial</a></li>
+ <li><a href="../classes/timer.html">timer</a></li>
+</ul>
+<h2>Modules</h2>
+<ul class="nowrap">
+ <li><a href="../modules/biquad.html">biquad</a></li>
+ <li><a href="../modules/expression.html">expression</a></li>
+ <li><a href="../modules/fs.html">fs</a></li>
+ <li><a href="../modules/keycallback.html">keycallback</a></li>
+ <li><a href="../modules/udp.html">udp</a></li>
+</ul>
+
+</div>
+
+<div id="content">
+
+<h1>Class <code>ladspa</code></h1>
+<p>LADSPA plugin support.</p>
+<p>
+
+
+
+<pre>
+<span class="keyword">local</span> mladspa = <span class="global">require</span> <span class="string">'melisma.ladspa'</span>
+
+<span class="keyword">local</span> spec = <span class="string">'caps.so:Plate'</span>
+<span class="keyword">local</span> info = mladspa.<span class="function-name">info</span>(spec)
+
+<span class="global">print</span>(<span class="string">'name:'</span>,info.name)
+<span class="global">print</span>(<span class="string">'maker:'</span>,info.maker)
+
+<span class="keyword">local</span> input = ...
+<span class="keyword">local</span> plate = mladspa.<span class="global">load</span>(spec,input)
+
+plate:<span class="function-name">control</span>(<span class="number">1</span>,<span class="number">1</span>)
+
+<span class="keyword">local</span> left = plate:<span class="function-name">out</span>(<span class="number">6</span>)
+<span class="keyword">local</span> right = plate:<span class="function-name">out</span>(<span class="number">7</span>)
+</pre>
+
+</p>
+
+
+<h2><a href="#Functions">Functions</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#ladspa.info">ladspa.info(spec)</a></td>
+ <td class="summary">Get information about a specific plugin.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#ladspa.lib_info">ladspa.lib_info(path)</a></td>
+ <td class="summary">List plugins in the specified plugin library.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#ladspa.load">ladspa.load(spec, ...)</a></td>
+ <td class="summary">Load a ladspa plugin.</td>
+ </tr>
+</table>
+<h2><a href="#Methods">Methods</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#ladspa:control">ladspa:control(i, v)</a></td>
+ <td class="summary">Set the value of an input control port.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#ladspa:out">ladspa:out(i)</a></td>
+ <td class="summary">Get the audio output port at the specified index.</td>
+ </tr>
+</table>
+
+<br/>
+<br/>
+
+
+ <h2 class="section-header "><a name="Functions"></a>Functions</h2>
+ Methods
+ <dl class="function">
+ <dt>
+ <a name = "ladspa.info"></a>
+ <strong>ladspa.info(spec)</strong>
+ </dt>
+ <dd>
+ Get information about a specific plugin. </p>
+
+<p>Plugin specifications are written in the format <code>&lt;path&gt;:&lt;plugin&gt;</code>,
+where <code>&lt;path&gt;</code> is the full name of the plugin library
+(e.g. <code>caps.so</code>), and <code>&lt;plugin&gt;</code> is the name or label of the plugin.
+If <code>&lt;path&gt;</code> is not an absolute path, the library is searched for
+first in the current directory, then in <code>LADSPA_PATH</code>.</p>
+
+<p>This can be used to generate more ergonomic plugin interfaces.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">spec</span>
+ <span class="types"><a class="type" href="https://www.lua.org/manual/5.3/manual.html#6.4">string</a></span>
+ Plugin specification.
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="https://www.lua.org/manual/5.3/manual.html#6.6">table</a></span>
+ Plugin information.
+ </ol>
+
+
+
+
+</dd>
+ <dt>
+ <a name = "ladspa.lib_info"></a>
+ <strong>ladspa.lib_info(path)</strong>
+ </dt>
+ <dd>
+ List plugins in the specified plugin library. </p>
+
+<p>If <code>path</code> is not an absolute path, the library is searched for
+first in the current directory, then in <code>LADSPA_PATH</code>.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">path</span>
+ <span class="types"><a class="type" href="https://www.lua.org/manual/5.3/manual.html#6.4">string</a></span>
+ Path to plugin library.
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="https://www.lua.org/manual/5.3/manual.html#6.6">table</a></span>
+ List of short plugin descriptions.
+ </ol>
+
+
+
+
+</dd>
+ <dt>
+ <a name = "ladspa.load"></a>
+ <strong>ladspa.load(spec, ...)</strong>
+ </dt>
+ <dd>
+ Load a ladspa plugin. </p>
+
+<p>Input signals are connected to the plugins audio input ports in the
+order that they are declared.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">spec</span>
+ <span class="types"><a class="type" href="https://www.lua.org/manual/5.3/manual.html#6.4">string</a></span>
+ Plugin specification.
+ </li>
+ <li><span class="parameter">...</span>
+ <span class="types"><span class="type">channel</span></span>
+ Input signals.
+ </li>
+ </ul>
+
+
+
+
+
+</dd>
+</dl>
+ <h2 class="section-header "><a name="Methods"></a>Methods</h2>
+
+ <dl class="function">
+ <dt>
+ <a name = "ladspa:control"></a>
+ <strong>ladspa:control(i, v)</strong>
+ </dt>
+ <dd>
+ Set the value of an input control port. </p>
+
+<p>Ports are indexed based on the order they are declared by the plugin.
+This also means the indices of control ports are not necessarily consecutive.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">i</span>
+ <span class="types"><span class="type">integer</span></span>
+ One-based port index.
+ </li>
+ <li><span class="parameter">v</span>
+ <span class="types"><span class="type">number</span></span>
+ New control port value.
+ </li>
+ </ul>
+
+
+
+
+
+</dd>
+ <dt>
+ <a name = "ladspa:out"></a>
+ <strong>ladspa:out(i)</strong>
+ </dt>
+ <dd>
+ Get the audio output port at the specified index. </p>
+
+<p>Ports are indexed based on the order they are declared by the plugin.
+This also means the indices of audio ports are not necessarily consecutive.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">i</span>
+ <span class="types"><span class="type">integer</span></span>
+ One-based port index.
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><span class="type">channel</span></span>
+ Audio output at specified index.
+ </ol>
+
+
+
+
+</dd>
+</dl>
+
+
+</div> <!-- id="content" -->
+</div> <!-- id="main" -->
+<div id="about">
+<i>generated by <a href="http://github.com/lunarmodules/LDoc">LDoc 1.5.0</a></i>
+<i style="float:right;">Last updated 1980-01-01 00:00:00 </i>
+</div> <!-- id="about" -->
+</div> <!-- id="container" -->
+</body>
+</html>
diff --git a/doc/classes/mixer.html b/doc/classes/mixer.html
new file mode 100644
index 0000000..2f8625c
--- /dev/null
+++ b/doc/classes/mixer.html
@@ -0,0 +1,139 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+<head>
+ <title>Melisma modules</title>
+ <link rel="stylesheet" href="../ldoc_fixed.css" type="text/css" />
+</head>
+<body>
+
+<div id="container">
+
+<div id="product">
+ <div id="product_logo"></div>
+ <div id="product_name"><big><b></b></big></div>
+ <div id="product_description"></div>
+</div> <!-- id="product" -->
+
+
+<div id="main">
+
+
+<!-- Menu -->
+
+<div id="navigation">
+<p><a href="/melisma-modules">Back to repository</a></p>
+<br/>
+<h1>Melisma modules</h1>
+
+
+<ul>
+ <li><a href="../index.html">Index</a></li>
+</ul>
+
+<h2>Contents</h2>
+<ul>
+<li><a href="#Functions">Functions</a></li>
+</ul>
+
+
+<h2>Classes</h2>
+<ul class="nowrap">
+ <li><a href="../classes/buffer.html">buffer</a></li>
+ <li><a href="../classes/delay.html">delay</a></li>
+ <li><a href="../classes/gain.html">gain</a></li>
+ <li><a href="../classes/interp.html">interp</a></li>
+ <li><a href="../classes/ladspa.html">ladspa</a></li>
+ <li><strong>mixer</strong></li>
+ <li><a href="../classes/nullsink.html">nullsink</a></li>
+ <li><a href="../classes/oscil.html">oscil</a></li>
+ <li><a href="../classes/pan.html">pan</a></li>
+ <li><a href="../classes/playback.html">playback</a></li>
+ <li><a href="../classes/sample.html">sample</a></li>
+ <li><a href="../classes/serial.html">serial</a></li>
+ <li><a href="../classes/timer.html">timer</a></li>
+</ul>
+<h2>Modules</h2>
+<ul class="nowrap">
+ <li><a href="../modules/biquad.html">biquad</a></li>
+ <li><a href="../modules/expression.html">expression</a></li>
+ <li><a href="../modules/fs.html">fs</a></li>
+ <li><a href="../modules/keycallback.html">keycallback</a></li>
+ <li><a href="../modules/udp.html">udp</a></li>
+</ul>
+
+</div>
+
+<div id="content">
+
+<h1>Class <code>mixer</code></h1>
+<p>Mix signals.</p>
+<p>
+
+
+
+<pre>
+<span class="keyword">local</span> mmixer = <span class="global">require</span> <span class="string">'melisma.mixer'</span>
+
+<span class="keyword">local</span> signals = ...
+<span class="keyword">local</span> mixer = mmixer.<span class="function-name">init</span>(<span class="global">table</span>.<span class="function-name">unpack</span>(signals))
+</pre>
+
+</p>
+
+
+<h2><a href="#Functions">Functions</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#mixer.init">mixer.init(...)</a></td>
+ <td class="summary">Mix input signals in to a single output signal.</td>
+ </tr>
+</table>
+
+<br/>
+<br/>
+
+
+ <h2 class="section-header "><a name="Functions"></a>Functions</h2>
+ Methods
+ <dl class="function">
+ <dt>
+ <a name = "mixer.init"></a>
+ <strong>mixer.init(...)</strong>
+ </dt>
+ <dd>
+ Mix input signals in to a single output signal.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">...</span>
+ <span class="types"><span class="type">channel</span></span>
+ Input signals.
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../classes/mixer.html#">mixer</a></span>
+ The newly created mixer unit.
+ </ol>
+
+
+
+
+</dd>
+</dl>
+
+
+</div> <!-- id="content" -->
+</div> <!-- id="main" -->
+<div id="about">
+<i>generated by <a href="http://github.com/lunarmodules/LDoc">LDoc 1.5.0</a></i>
+<i style="float:right;">Last updated 1980-01-01 00:00:00 </i>
+</div> <!-- id="about" -->
+</div> <!-- id="container" -->
+</body>
+</html>
diff --git a/doc/classes/nullsink.html b/doc/classes/nullsink.html
new file mode 100644
index 0000000..d8a611b
--- /dev/null
+++ b/doc/classes/nullsink.html
@@ -0,0 +1,188 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+<head>
+ <title>Melisma modules</title>
+ <link rel="stylesheet" href="../ldoc_fixed.css" type="text/css" />
+</head>
+<body>
+
+<div id="container">
+
+<div id="product">
+ <div id="product_logo"></div>
+ <div id="product_name"><big><b></b></big></div>
+ <div id="product_description"></div>
+</div> <!-- id="product" -->
+
+
+<div id="main">
+
+
+<!-- Menu -->
+
+<div id="navigation">
+<p><a href="/melisma-modules">Back to repository</a></p>
+<br/>
+<h1>Melisma modules</h1>
+
+
+<ul>
+ <li><a href="../index.html">Index</a></li>
+</ul>
+
+<h2>Contents</h2>
+<ul>
+<li><a href="#Functions">Functions</a></li>
+<li><a href="#Methods">Methods</a></li>
+</ul>
+
+
+<h2>Classes</h2>
+<ul class="nowrap">
+ <li><a href="../classes/buffer.html">buffer</a></li>
+ <li><a href="../classes/delay.html">delay</a></li>
+ <li><a href="../classes/gain.html">gain</a></li>
+ <li><a href="../classes/interp.html">interp</a></li>
+ <li><a href="../classes/ladspa.html">ladspa</a></li>
+ <li><a href="../classes/mixer.html">mixer</a></li>
+ <li><strong>nullsink</strong></li>
+ <li><a href="../classes/oscil.html">oscil</a></li>
+ <li><a href="../classes/pan.html">pan</a></li>
+ <li><a href="../classes/playback.html">playback</a></li>
+ <li><a href="../classes/sample.html">sample</a></li>
+ <li><a href="../classes/serial.html">serial</a></li>
+ <li><a href="../classes/timer.html">timer</a></li>
+</ul>
+<h2>Modules</h2>
+<ul class="nowrap">
+ <li><a href="../modules/biquad.html">biquad</a></li>
+ <li><a href="../modules/expression.html">expression</a></li>
+ <li><a href="../modules/fs.html">fs</a></li>
+ <li><a href="../modules/keycallback.html">keycallback</a></li>
+ <li><a href="../modules/udp.html">udp</a></li>
+</ul>
+
+</div>
+
+<div id="content">
+
+<h1>Class <code>nullsink</code></h1>
+<p>Reference otherwise disconnected nodes.</p>
+<p>
+
+
+
+<pre>
+<span class="keyword">local</span> mnullsink = <span class="global">require</span> <span class="string">'melisma.nullsink'</span>
+<span class="keyword">local</span> minterp = <span class="global">require</span> <span class="string">'melisma.interp'</span>
+<span class="keyword">local</span> mevent = <span class="global">require</span> <span class="string">'melisma.event'</span>
+
+<span class="keyword">local</span> interp = minterp.<span class="function-name">init</span>(<span class="number">0</span>)
+interp:<span class="function-name">linear</span>(<span class="number">1</span>,<span class="number">60</span>)
+
+<span class="comment">-- The output of interp is disconnected.
+</span><span class="comment">-- Need to connect it to a nullsink so we
+</span><span class="comment">-- can sample it in the event callback below.
+</span><span class="keyword">local</span> nullsink = mnullsink.<span class="function-name">init</span>()
+nullsink:<span class="function-name">ref</span>(interp)
+
+mevent.<span class="function-name">schedule</span>(<span class="number">30</span>, <span class="keyword">function</span>()
+ <span class="global">print</span>(<span class="string">'current value:'</span>, interp:<span class="function-name">sample</span>())
+<span class="keyword">end</span>)
+</pre>
+
+</p>
+
+
+<h2><a href="#Functions">Functions</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#nullsink.init">nullsink.init()</a></td>
+ <td class="summary">Create a nullsink.</td>
+ </tr>
+</table>
+<h2><a href="#Methods">Methods</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#nullsink:ref">nullsink:ref(input)</a></td>
+ <td class="summary">Connect an input signal.</td>
+ </tr>
+</table>
+
+<br/>
+<br/>
+
+
+ <h2 class="section-header "><a name="Functions"></a>Functions</h2>
+ Methods
+ <dl class="function">
+ <dt>
+ <a name = "nullsink.init"></a>
+ <strong>nullsink.init()</strong>
+ </dt>
+ <dd>
+ Create a nullsink.
+
+
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../classes/nullsink.html#">nullsink</a></span>
+ The newly created nullsink.
+ </ol>
+
+
+
+
+</dd>
+</dl>
+ <h2 class="section-header "><a name="Methods"></a>Methods</h2>
+
+ <dl class="function">
+ <dt>
+ <a name = "nullsink:ref"></a>
+ <strong>nullsink:ref(input)</strong>
+ </dt>
+ <dd>
+ Connect an input signal. </p>
+
+<p>This makes sure the input signal is processed
+even if it's not connected to anything else.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">input</span>
+ <span class="types"><span class="type">channel</span></span>
+ Input signal.
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../classes/nullsink.html#">nullsink</a></span>
+
+
+
+ </ol>
+
+
+
+
+</dd>
+</dl>
+
+
+</div> <!-- id="content" -->
+</div> <!-- id="main" -->
+<div id="about">
+<i>generated by <a href="http://github.com/lunarmodules/LDoc">LDoc 1.5.0</a></i>
+<i style="float:right;">Last updated 1980-01-01 00:00:00 </i>
+</div> <!-- id="about" -->
+</div> <!-- id="container" -->
+</body>
+</html>
diff --git a/doc/classes/oscil.html b/doc/classes/oscil.html
new file mode 100644
index 0000000..5aea703
--- /dev/null
+++ b/doc/classes/oscil.html
@@ -0,0 +1,392 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+<head>
+ <title>Melisma modules</title>
+ <link rel="stylesheet" href="../ldoc_fixed.css" type="text/css" />
+</head>
+<body>
+
+<div id="container">
+
+<div id="product">
+ <div id="product_logo"></div>
+ <div id="product_name"><big><b></b></big></div>
+ <div id="product_description"></div>
+</div> <!-- id="product" -->
+
+
+<div id="main">
+
+
+<!-- Menu -->
+
+<div id="navigation">
+<p><a href="/melisma-modules">Back to repository</a></p>
+<br/>
+<h1>Melisma modules</h1>
+
+
+<ul>
+ <li><a href="../index.html">Index</a></li>
+</ul>
+
+<h2>Contents</h2>
+<ul>
+<li><a href="#Functions">Functions</a></li>
+<li><a href="#Methods">Methods</a></li>
+<li><a href="#Class_wavetable">Class wavetable </a></li>
+</ul>
+
+
+<h2>Classes</h2>
+<ul class="nowrap">
+ <li><a href="../classes/buffer.html">buffer</a></li>
+ <li><a href="../classes/delay.html">delay</a></li>
+ <li><a href="../classes/gain.html">gain</a></li>
+ <li><a href="../classes/interp.html">interp</a></li>
+ <li><a href="../classes/ladspa.html">ladspa</a></li>
+ <li><a href="../classes/mixer.html">mixer</a></li>
+ <li><a href="../classes/nullsink.html">nullsink</a></li>
+ <li><strong>oscil</strong></li>
+ <li><a href="../classes/pan.html">pan</a></li>
+ <li><a href="../classes/playback.html">playback</a></li>
+ <li><a href="../classes/sample.html">sample</a></li>
+ <li><a href="../classes/serial.html">serial</a></li>
+ <li><a href="../classes/timer.html">timer</a></li>
+</ul>
+<h2>Modules</h2>
+<ul class="nowrap">
+ <li><a href="../modules/biquad.html">biquad</a></li>
+ <li><a href="../modules/expression.html">expression</a></li>
+ <li><a href="../modules/fs.html">fs</a></li>
+ <li><a href="../modules/keycallback.html">keycallback</a></li>
+ <li><a href="../modules/udp.html">udp</a></li>
+</ul>
+
+</div>
+
+<div id="content">
+
+<h1>Class <code>oscil</code></h1>
+<p>Wavetable backed oscillators.</p>
+<p>
+
+
+
+<pre>
+<span class="keyword">local</span> moscil = <span class="global">require</span> <span class="string">'melisma.oscil'</span>
+<span class="keyword">local</span> mexpression = <span class="global">require</span> <span class="string">'melisma.expression'</span>
+
+<span class="keyword">local</span> wt = moscil.wavetable.<span class="function-name">sin</span>()
+
+<span class="comment">-- Make an LFO for controlling the frequency.
+</span><span class="keyword">local</span> lfo_oscil = moscil.<span class="function-name">init</span>(wt,<span class="number">1</span>,<span class="number">1</span>)
+
+<span class="comment">-- Map [-1, 1] to [440, 880]
+</span><span class="keyword">local</span> lfo = (<span class="number">440</span> + (<span class="number">1</span> + mexpression.<span class="function-name">input</span>(lfo_oscil,<span class="number">0</span>) / <span class="number">2</span>) * <span class="number">440</span>):<span class="function-name">build</span>()
+
+<span class="keyword">local</span> oscil = moscil.<span class="function-name">init</span>(wt,<span class="number">0</span>,<span class="number">0.5</span>)
+oscil:<span class="function-name">mapf</span>(lfo)
+</pre>
+
+</p>
+
+
+<h2><a href="#Functions">Functions</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#oscil.init">oscil.init(wt, freq, amp)</a></td>
+ <td class="summary">Initialize an oscillator.</td>
+ </tr>
+</table>
+<h2><a href="#Methods">Methods</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#oscil:mapa">oscil:mapa(sig)</a></td>
+ <td class="summary">Map a signal to the amplitude.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#oscil:mapf">oscil:mapf(sig)</a></td>
+ <td class="summary">Map a signal to the frequency.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#oscil:sample">oscil:sample()</a></td>
+ <td class="summary">Get the current oscillator value.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#oscil:seta">oscil:seta(amp)</a></td>
+ <td class="summary">Set oscillator amplitude.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#oscil:setf">oscil:setf(freq)</a></td>
+ <td class="summary">Set oscillator frequency.</td>
+ </tr>
+</table>
+<h2><a href="#Class_wavetable">Class wavetable </a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#wavetable.saw">wavetable.saw()</a></td>
+ <td class="summary">Saw wave.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#wavetable.sin">wavetable.sin()</a></td>
+ <td class="summary">Sine wave.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#wavetable.square">wavetable.square()</a></td>
+ <td class="summary">Square wave.</td>
+ </tr>
+</table>
+
+<br/>
+<br/>
+
+
+ <h2 class="section-header "><a name="Functions"></a>Functions</h2>
+ Methods
+ <dl class="function">
+ <dt>
+ <a name = "oscil.init"></a>
+ <strong>oscil.init(wt, freq, amp)</strong>
+ </dt>
+ <dd>
+ Initialize an oscillator.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">wt</span>
+ <span class="types"><a class="type" href="../classes/oscil.html#wavetable">wavetable</a></span>
+
+
+
+ </li>
+ <li><span class="parameter">freq</span>
+ <span class="types"><span class="type">number</span></span>
+
+
+
+ </li>
+ <li><span class="parameter">amp</span>
+ <span class="types"><span class="type">number</span></span>
+
+
+
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../classes/oscil.html#">oscil</a></span>
+
+
+
+ </ol>
+
+
+
+
+</dd>
+</dl>
+ <h2 class="section-header "><a name="Methods"></a>Methods</h2>
+
+ <dl class="function">
+ <dt>
+ <a name = "oscil:mapa"></a>
+ <strong>oscil:mapa(sig)</strong>
+ </dt>
+ <dd>
+ Map a signal to the amplitude.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">sig</span>
+ <span class="types"><span class="type">channel</span></span>
+
+
+
+ </li>
+ </ul>
+
+
+
+
+
+</dd>
+ <dt>
+ <a name = "oscil:mapf"></a>
+ <strong>oscil:mapf(sig)</strong>
+ </dt>
+ <dd>
+ Map a signal to the frequency.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">sig</span>
+ <span class="types"><span class="type">channel</span></span>
+
+
+
+ </li>
+ </ul>
+
+
+
+
+
+</dd>
+ <dt>
+ <a name = "oscil:sample"></a>
+ <strong>oscil:sample()</strong>
+ </dt>
+ <dd>
+ Get the current oscillator value.
+
+
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><span class="type">number</span></span>
+
+
+
+ </ol>
+
+
+
+
+</dd>
+ <dt>
+ <a name = "oscil:seta"></a>
+ <strong>oscil:seta(amp)</strong>
+ </dt>
+ <dd>
+ Set oscillator amplitude.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">amp</span>
+ <span class="types"><span class="type">number</span></span>
+
+
+
+ </li>
+ </ul>
+
+
+
+
+
+</dd>
+ <dt>
+ <a name = "oscil:setf"></a>
+ <strong>oscil:setf(freq)</strong>
+ </dt>
+ <dd>
+ Set oscillator frequency.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">freq</span>
+ <span class="types"><span class="type">number</span></span>
+
+
+
+ </li>
+ </ul>
+
+
+
+
+
+</dd>
+</dl>
+ <h2 class="section-header "><a name="Class_wavetable"></a>Class wavetable </h2>
+
+ <dl class="function">
+ <dt>
+ <a name = "wavetable.saw"></a>
+ <strong>wavetable.saw()</strong>
+ </dt>
+ <dd>
+ Saw wave.
+
+
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../classes/oscil.html#wavetable">wavetable</a></span>
+
+
+
+ </ol>
+
+
+
+
+</dd>
+ <dt>
+ <a name = "wavetable.sin"></a>
+ <strong>wavetable.sin()</strong>
+ </dt>
+ <dd>
+ Sine wave.
+
+
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../classes/oscil.html#wavetable">wavetable</a></span>
+
+
+
+ </ol>
+
+
+
+
+</dd>
+ <dt>
+ <a name = "wavetable.square"></a>
+ <strong>wavetable.square()</strong>
+ </dt>
+ <dd>
+ Square wave.
+
+
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../classes/oscil.html#wavetable">wavetable</a></span>
+
+
+
+ </ol>
+
+
+
+
+</dd>
+</dl>
+
+
+</div> <!-- id="content" -->
+</div> <!-- id="main" -->
+<div id="about">
+<i>generated by <a href="http://github.com/lunarmodules/LDoc">LDoc 1.5.0</a></i>
+<i style="float:right;">Last updated 1980-01-01 00:00:00 </i>
+</div> <!-- id="about" -->
+</div> <!-- id="container" -->
+</body>
+</html>
diff --git a/doc/classes/pan.html b/doc/classes/pan.html
new file mode 100644
index 0000000..1728b7c
--- /dev/null
+++ b/doc/classes/pan.html
@@ -0,0 +1,240 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+<head>
+ <title>Melisma modules</title>
+ <link rel="stylesheet" href="../ldoc_fixed.css" type="text/css" />
+</head>
+<body>
+
+<div id="container">
+
+<div id="product">
+ <div id="product_logo"></div>
+ <div id="product_name"><big><b></b></big></div>
+ <div id="product_description"></div>
+</div> <!-- id="product" -->
+
+
+<div id="main">
+
+
+<!-- Menu -->
+
+<div id="navigation">
+<p><a href="/melisma-modules">Back to repository</a></p>
+<br/>
+<h1>Melisma modules</h1>
+
+
+<ul>
+ <li><a href="../index.html">Index</a></li>
+</ul>
+
+<h2>Contents</h2>
+<ul>
+<li><a href="#Functions">Functions</a></li>
+<li><a href="#Methods">Methods</a></li>
+</ul>
+
+
+<h2>Classes</h2>
+<ul class="nowrap">
+ <li><a href="../classes/buffer.html">buffer</a></li>
+ <li><a href="../classes/delay.html">delay</a></li>
+ <li><a href="../classes/gain.html">gain</a></li>
+ <li><a href="../classes/interp.html">interp</a></li>
+ <li><a href="../classes/ladspa.html">ladspa</a></li>
+ <li><a href="../classes/mixer.html">mixer</a></li>
+ <li><a href="../classes/nullsink.html">nullsink</a></li>
+ <li><a href="../classes/oscil.html">oscil</a></li>
+ <li><strong>pan</strong></li>
+ <li><a href="../classes/playback.html">playback</a></li>
+ <li><a href="../classes/sample.html">sample</a></li>
+ <li><a href="../classes/serial.html">serial</a></li>
+ <li><a href="../classes/timer.html">timer</a></li>
+</ul>
+<h2>Modules</h2>
+<ul class="nowrap">
+ <li><a href="../modules/biquad.html">biquad</a></li>
+ <li><a href="../modules/expression.html">expression</a></li>
+ <li><a href="../modules/fs.html">fs</a></li>
+ <li><a href="../modules/keycallback.html">keycallback</a></li>
+ <li><a href="../modules/udp.html">udp</a></li>
+</ul>
+
+</div>
+
+<div id="content">
+
+<h1>Class <code>pan</code></h1>
+<p>Panning</p>
+<p>
+
+<pre>
+<span class="keyword">local</span> mpan = <span class="global">require</span> <span class="string">'melisma.pan'</span>
+<span class="keyword">local</span> mexpression = <span class="global">require</span> <span class="string">'melisma.expression'</span>
+<span class="keyword">local</span> moscil = <span class="global">require</span> <span class="string">'melisma.oscil'</span>
+
+<span class="keyword">local</span> wt = moscil.wavetable.<span class="function-name">sin</span>()
+<span class="keyword">local</span> lfo_oscil = moscil.<span class="function-name">init</span>(wt,<span class="number">1</span>,<span class="number">1</span>)
+<span class="keyword">local</span> lfo = (<span class="number">1</span> + mexpression.<span class="function-name">input</span>(lfo_oscil,<span class="number">0</span>) / <span class="number">2</span>):<span class="function-name">build</span>()
+
+<span class="keyword">local</span> oscil = moscil.<span class="function-name">init</span>(wt,<span class="number">440</span>,<span class="number">0.5</span>)
+<span class="keyword">local</span> pan = mpan.<span class="function-name">init</span>(lfo,oscil)
+
+<span class="keyword">local</span> left = pan:<span class="function-name">left</span>()
+<span class="keyword">local</span> right = pan:<span class="function-name">right</span>()
+</pre>
+
+</p>
+
+
+<h2><a href="#Functions">Functions</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#pan.init">pan.init(x, sig)</a></td>
+ <td class="summary">Initialize a panning unit.</td>
+ </tr>
+</table>
+<h2><a href="#Methods">Methods</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#pan:left">pan:left()</a></td>
+ <td class="summary">Get the left output signal.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#pan:right">pan:right()</a></td>
+ <td class="summary">Get the right output signal.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#pan:set">pan:set(x)</a></td>
+ <td class="summary">Set the panning value.</td>
+ </tr>
+</table>
+
+<br/>
+<br/>
+
+
+ <h2 class="section-header "><a name="Functions"></a>Functions</h2>
+ Methods
+ <dl class="function">
+ <dt>
+ <a name = "pan.init"></a>
+ <strong>pan.init(x, sig)</strong>
+ </dt>
+ <dd>
+ Initialize a panning unit. </p>
+
+<p>A panning value ranges from 0 to 1, where a value of 0
+will pan everything to the left.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">x</span>
+ <span class="types"><span class="type">number</span> or <span class="type">channel</span></span>
+ Constant pan or a controlling signal.
+ </li>
+ <li><span class="parameter">sig</span>
+ <span class="types"><span class="type">channel</span></span>
+ Input signal.
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../classes/pan.html#">pan</a></span>
+ The newly created panning unit.
+ </ol>
+
+
+
+
+</dd>
+</dl>
+ <h2 class="section-header "><a name="Methods"></a>Methods</h2>
+
+ <dl class="function">
+ <dt>
+ <a name = "pan:left"></a>
+ <strong>pan:left()</strong>
+ </dt>
+ <dd>
+ Get the left output signal.
+
+
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><span class="type">channel</span></span>
+
+
+
+ </ol>
+
+
+
+
+</dd>
+ <dt>
+ <a name = "pan:right"></a>
+ <strong>pan:right()</strong>
+ </dt>
+ <dd>
+ Get the right output signal.
+
+
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><span class="type">channel</span></span>
+
+
+
+ </ol>
+
+
+
+
+</dd>
+ <dt>
+ <a name = "pan:set"></a>
+ <strong>pan:set(x)</strong>
+ </dt>
+ <dd>
+ Set the panning value.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">x</span>
+ <span class="types"><span class="type">number</span></span>
+
+
+
+ </li>
+ </ul>
+
+
+
+
+
+</dd>
+</dl>
+
+
+</div> <!-- id="content" -->
+</div> <!-- id="main" -->
+<div id="about">
+<i>generated by <a href="http://github.com/lunarmodules/LDoc">LDoc 1.5.0</a></i>
+<i style="float:right;">Last updated 1980-01-01 00:00:00 </i>
+</div> <!-- id="about" -->
+</div> <!-- id="container" -->
+</body>
+</html>
diff --git a/doc/classes/playback.html b/doc/classes/playback.html
new file mode 100644
index 0000000..fcee0d6
--- /dev/null
+++ b/doc/classes/playback.html
@@ -0,0 +1,331 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+<head>
+ <title>Melisma modules</title>
+ <link rel="stylesheet" href="../ldoc_fixed.css" type="text/css" />
+</head>
+<body>
+
+<div id="container">
+
+<div id="product">
+ <div id="product_logo"></div>
+ <div id="product_name"><big><b></b></big></div>
+ <div id="product_description"></div>
+</div> <!-- id="product" -->
+
+
+<div id="main">
+
+
+<!-- Menu -->
+
+<div id="navigation">
+<p><a href="/melisma-modules">Back to repository</a></p>
+<br/>
+<h1>Melisma modules</h1>
+
+
+<ul>
+ <li><a href="../index.html">Index</a></li>
+</ul>
+
+<h2>Contents</h2>
+<ul>
+<li><a href="#Functions">Functions</a></li>
+<li><a href="#Methods">Methods</a></li>
+</ul>
+
+
+<h2>Classes</h2>
+<ul class="nowrap">
+ <li><a href="../classes/buffer.html">buffer</a></li>
+ <li><a href="../classes/delay.html">delay</a></li>
+ <li><a href="../classes/gain.html">gain</a></li>
+ <li><a href="../classes/interp.html">interp</a></li>
+ <li><a href="../classes/ladspa.html">ladspa</a></li>
+ <li><a href="../classes/mixer.html">mixer</a></li>
+ <li><a href="../classes/nullsink.html">nullsink</a></li>
+ <li><a href="../classes/oscil.html">oscil</a></li>
+ <li><a href="../classes/pan.html">pan</a></li>
+ <li><strong>playback</strong></li>
+ <li><a href="../classes/sample.html">sample</a></li>
+ <li><a href="../classes/serial.html">serial</a></li>
+ <li><a href="../classes/timer.html">timer</a></li>
+</ul>
+<h2>Modules</h2>
+<ul class="nowrap">
+ <li><a href="../modules/biquad.html">biquad</a></li>
+ <li><a href="../modules/expression.html">expression</a></li>
+ <li><a href="../modules/fs.html">fs</a></li>
+ <li><a href="../modules/keycallback.html">keycallback</a></li>
+ <li><a href="../modules/udp.html">udp</a></li>
+</ul>
+
+</div>
+
+<div id="content">
+
+<h1>Class <code>playback</code></h1>
+<p>Audio sample playback.</p>
+<p>
+
+
+
+<pre>
+<span class="keyword">local</span> msample = <span class="global">require</span> <span class="string">'melisma.sample'</span>
+<span class="keyword">local</span> mplayback = <span class="global">require</span> <span class="string">'melisma.playback'</span>
+
+<span class="keyword">local</span> sample = msample.<span class="global">load</span>(<span class="string">'sample.wav'</span>)
+<span class="keyword">local</span> playb = mplayback.<span class="function-name">init</span>(sample)
+
+playb:<span class="function-name">loop</span>(<span class="keyword">true</span>)
+playb:<span class="function-name">spped</span>(<span class="number">2</span>)
+playb:<span class="function-name">play</span>()
+</pre>
+
+</p>
+
+
+<h2><a href="#Functions">Functions</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#playback.init">playback.init(smp)</a></td>
+ <td class="summary">Intialize a playback unit.</td>
+ </tr>
+</table>
+<h2><a href="#Methods">Methods</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#playback:loop">playback:loop(b)</a></td>
+ <td class="summary">Enable/disable automatic looping.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#playback:pause">playback:pause()</a></td>
+ <td class="summary">Pause playback.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#playback:phase">playback:phase(x)</a></td>
+ <td class="summary">Set playback phase.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#playback:play">playback:play()</a></td>
+ <td class="summary">Start playback.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#playback:reset">playback:reset()</a></td>
+ <td class="summary">Reset playback.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#playback:reset">playback:reset()</a></td>
+ <td class="summary">Set playback speed.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#playback:set_sample">playback:set_sample(smp)</a></td>
+ <td class="summary">Set playback sample.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#playback:stop">playback:stop()</a></td>
+ <td class="summary">Stop playback.</td>
+ </tr>
+</table>
+
+<br/>
+<br/>
+
+
+ <h2 class="section-header "><a name="Functions"></a>Functions</h2>
+ Methods
+ <dl class="function">
+ <dt>
+ <a name = "playback.init"></a>
+ <strong>playback.init(smp)</strong>
+ </dt>
+ <dd>
+ Intialize a playback unit.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">smp</span>
+ <span class="types"><a class="type" href="../classes/sample.html#">sample</a> or <span class="type">nil</span></span>
+
+
+
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../classes/playback.html#">playback</a></span>
+
+
+
+ </ol>
+
+
+
+
+</dd>
+</dl>
+ <h2 class="section-header "><a name="Methods"></a>Methods</h2>
+
+ <dl class="function">
+ <dt>
+ <a name = "playback:loop"></a>
+ <strong>playback:loop(b)</strong>
+ </dt>
+ <dd>
+ Enable/disable automatic looping.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">b</span>
+ <span class="types"><span class="type">bool</span></span>
+
+
+
+ </li>
+ </ul>
+
+
+
+
+
+</dd>
+ <dt>
+ <a name = "playback:pause"></a>
+ <strong>playback:pause()</strong>
+ </dt>
+ <dd>
+ Pause playback.
+
+
+
+
+
+
+
+</dd>
+ <dt>
+ <a name = "playback:phase"></a>
+ <strong>playback:phase(x)</strong>
+ </dt>
+ <dd>
+ Set playback phase.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">x</span>
+ <span class="types"><span class="type">number</span></span>
+ Phase ranging from 0 to 1.
+ </li>
+ </ul>
+
+
+
+
+
+</dd>
+ <dt>
+ <a name = "playback:play"></a>
+ <strong>playback:play()</strong>
+ </dt>
+ <dd>
+ Start playback.
+
+
+
+
+
+
+
+</dd>
+ <dt>
+ <a name = "playback:reset"></a>
+ <strong>playback:reset()</strong>
+ </dt>
+ <dd>
+ Reset playback. </p>
+
+<p>Equivalent to <a href="../classes/playback.html#playback:phase">playback.phase</a>(0).
+
+
+
+
+
+
+
+</dd>
+ <dt>
+ <a name = "playback:reset"></a>
+ <strong>playback:reset()</strong>
+ </dt>
+ <dd>
+ Set playback speed. </p>
+
+<p>A negative speed will play the sample in reverse.
+
+
+
+
+
+
+
+</dd>
+ <dt>
+ <a name = "playback:set_sample"></a>
+ <strong>playback:set_sample(smp)</strong>
+ </dt>
+ <dd>
+ Set playback sample.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">smp</span>
+ <span class="types"><a class="type" href="../classes/sample.html#">sample</a></span>
+
+
+
+ </li>
+ </ul>
+
+
+
+
+
+</dd>
+ <dt>
+ <a name = "playback:stop"></a>
+ <strong>playback:stop()</strong>
+ </dt>
+ <dd>
+ Stop playback. </p>
+
+<p>Equivalent to <a href="../classes/playback.html#playback:pause">playback.pause</a>; <a href="../classes/playback.html#playback:reset">playback.reset</a>.
+
+
+
+
+
+
+
+</dd>
+</dl>
+
+
+</div> <!-- id="content" -->
+</div> <!-- id="main" -->
+<div id="about">
+<i>generated by <a href="http://github.com/lunarmodules/LDoc">LDoc 1.5.0</a></i>
+<i style="float:right;">Last updated 1980-01-01 00:00:00 </i>
+</div> <!-- id="about" -->
+</div> <!-- id="container" -->
+</body>
+</html>
diff --git a/doc/classes/sample.html b/doc/classes/sample.html
new file mode 100644
index 0000000..313d14d
--- /dev/null
+++ b/doc/classes/sample.html
@@ -0,0 +1,203 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+<head>
+ <title>Melisma modules</title>
+ <link rel="stylesheet" href="../ldoc_fixed.css" type="text/css" />
+</head>
+<body>
+
+<div id="container">
+
+<div id="product">
+ <div id="product_logo"></div>
+ <div id="product_name"><big><b></b></big></div>
+ <div id="product_description"></div>
+</div> <!-- id="product" -->
+
+
+<div id="main">
+
+
+<!-- Menu -->
+
+<div id="navigation">
+<p><a href="/melisma-modules">Back to repository</a></p>
+<br/>
+<h1>Melisma modules</h1>
+
+
+<ul>
+ <li><a href="../index.html">Index</a></li>
+</ul>
+
+<h2>Contents</h2>
+<ul>
+<li><a href="#Functions">Functions</a></li>
+<li><a href="#Methods">Methods</a></li>
+</ul>
+
+
+<h2>Classes</h2>
+<ul class="nowrap">
+ <li><a href="../classes/buffer.html">buffer</a></li>
+ <li><a href="../classes/delay.html">delay</a></li>
+ <li><a href="../classes/gain.html">gain</a></li>
+ <li><a href="../classes/interp.html">interp</a></li>
+ <li><a href="../classes/ladspa.html">ladspa</a></li>
+ <li><a href="../classes/mixer.html">mixer</a></li>
+ <li><a href="../classes/nullsink.html">nullsink</a></li>
+ <li><a href="../classes/oscil.html">oscil</a></li>
+ <li><a href="../classes/pan.html">pan</a></li>
+ <li><a href="../classes/playback.html">playback</a></li>
+ <li><strong>sample</strong></li>
+ <li><a href="../classes/serial.html">serial</a></li>
+ <li><a href="../classes/timer.html">timer</a></li>
+</ul>
+<h2>Modules</h2>
+<ul class="nowrap">
+ <li><a href="../modules/biquad.html">biquad</a></li>
+ <li><a href="../modules/expression.html">expression</a></li>
+ <li><a href="../modules/fs.html">fs</a></li>
+ <li><a href="../modules/keycallback.html">keycallback</a></li>
+ <li><a href="../modules/udp.html">udp</a></li>
+</ul>
+
+</div>
+
+<div id="content">
+
+<h1>Class <code>sample</code></h1>
+<p>Load audio samples.</p>
+<p>
+
+
+
+<pre>
+<span class="keyword">local</span> msample = <span class="global">require</span> <span class="string">'melisma.sample'</span>
+
+<span class="keyword">local</span> smp = msample.<span class="global">load</span>(<span class="string">'sample.wav'</span>)
+
+<span class="global">print</span>(smp:<span class="function-name">length</span>())
+<span class="global">print</span>(smp:<span class="function-name">path</span>())
+</pre>
+
+</p>
+
+
+<h2><a href="#Functions">Functions</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#sample.load">sample.load(path)</a></td>
+ <td class="summary">Load an audio sample.</td>
+ </tr>
+</table>
+<h2><a href="#Methods">Methods</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#sample:length">sample:length()</a></td>
+ <td class="summary">Get sample length.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#sample:path">sample:path()</a></td>
+ <td class="summary">Get sample path.</td>
+ </tr>
+</table>
+
+<br/>
+<br/>
+
+
+ <h2 class="section-header "><a name="Functions"></a>Functions</h2>
+ Methods
+ <dl class="function">
+ <dt>
+ <a name = "sample.load"></a>
+ <strong>sample.load(path)</strong>
+ </dt>
+ <dd>
+ Load an audio sample. </p>
+
+<p>If necessary, the audio will be resampled to the current sample rate.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">path</span>
+ <span class="types"><a class="type" href="https://www.lua.org/manual/5.3/manual.html#6.4">string</a></span>
+
+
+
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../classes/sample.html#">sample</a></span>
+
+
+
+ </ol>
+
+
+
+
+</dd>
+</dl>
+ <h2 class="section-header "><a name="Methods"></a>Methods</h2>
+
+ <dl class="function">
+ <dt>
+ <a name = "sample:length"></a>
+ <strong>sample:length()</strong>
+ </dt>
+ <dd>
+ Get sample length.
+
+
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><span class="type">number</span></span>
+ The length of the sample in seconds.
+ </ol>
+
+
+
+
+</dd>
+ <dt>
+ <a name = "sample:path"></a>
+ <strong>sample:path()</strong>
+ </dt>
+ <dd>
+ Get sample path.
+
+
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="https://www.lua.org/manual/5.3/manual.html#6.4">string</a></span>
+ The path that the sample was loaded from.
+ </ol>
+
+
+
+
+</dd>
+</dl>
+
+
+</div> <!-- id="content" -->
+</div> <!-- id="main" -->
+<div id="about">
+<i>generated by <a href="http://github.com/lunarmodules/LDoc">LDoc 1.5.0</a></i>
+<i style="float:right;">Last updated 1980-01-01 00:00:00 </i>
+</div> <!-- id="about" -->
+</div> <!-- id="container" -->
+</body>
+</html>
diff --git a/doc/classes/serial.html b/doc/classes/serial.html
new file mode 100644
index 0000000..ca72d57
--- /dev/null
+++ b/doc/classes/serial.html
@@ -0,0 +1,161 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+<head>
+ <title>Melisma modules</title>
+ <link rel="stylesheet" href="../ldoc_fixed.css" type="text/css" />
+</head>
+<body>
+
+<div id="container">
+
+<div id="product">
+ <div id="product_logo"></div>
+ <div id="product_name"><big><b></b></big></div>
+ <div id="product_description"></div>
+</div> <!-- id="product" -->
+
+
+<div id="main">
+
+
+<!-- Menu -->
+
+<div id="navigation">
+<p><a href="/melisma-modules">Back to repository</a></p>
+<br/>
+<h1>Melisma modules</h1>
+
+
+<ul>
+ <li><a href="../index.html">Index</a></li>
+</ul>
+
+<h2>Contents</h2>
+<ul>
+<li><a href="#Functions">Functions</a></li>
+</ul>
+
+
+<h2>Classes</h2>
+<ul class="nowrap">
+ <li><a href="../classes/buffer.html">buffer</a></li>
+ <li><a href="../classes/delay.html">delay</a></li>
+ <li><a href="../classes/gain.html">gain</a></li>
+ <li><a href="../classes/interp.html">interp</a></li>
+ <li><a href="../classes/ladspa.html">ladspa</a></li>
+ <li><a href="../classes/mixer.html">mixer</a></li>
+ <li><a href="../classes/nullsink.html">nullsink</a></li>
+ <li><a href="../classes/oscil.html">oscil</a></li>
+ <li><a href="../classes/pan.html">pan</a></li>
+ <li><a href="../classes/playback.html">playback</a></li>
+ <li><a href="../classes/sample.html">sample</a></li>
+ <li><strong>serial</strong></li>
+ <li><a href="../classes/timer.html">timer</a></li>
+</ul>
+<h2>Modules</h2>
+<ul class="nowrap">
+ <li><a href="../modules/biquad.html">biquad</a></li>
+ <li><a href="../modules/expression.html">expression</a></li>
+ <li><a href="../modules/fs.html">fs</a></li>
+ <li><a href="../modules/keycallback.html">keycallback</a></li>
+ <li><a href="../modules/udp.html">udp</a></li>
+</ul>
+
+</div>
+
+<div id="content">
+
+<h1>Class <code>serial</code></h1>
+<p>Read data on serial ports.</p>
+<p>
+
+
+
+<pre>
+<span class="keyword">local</span> mserial = <span class="global">require</span> <span class="string">'melisma.serial'</span>
+
+mserial.<span class="function-name">open</span> {
+ path = <span class="string">'/dev/ttyACM0'</span>,
+ callback = <span class="global">print</span>,
+}
+</pre>
+
+</p>
+
+
+<h2><a href="#Functions">Functions</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#serial.open">serial.open(conf)</a></td>
+ <td class="summary">Open a serial port.</td>
+ </tr>
+</table>
+
+<br/>
+<br/>
+
+
+ <h2 class="section-header "><a name="Functions"></a>Functions</h2>
+ Methods
+ <dl class="function">
+ <dt>
+ <a name = "serial.open"></a>
+ <strong>serial.open(conf)</strong>
+ </dt>
+ <dd>
+
+<p>Open a serial port. </p>
+
+<p><code>conf</code> is a table containing the following keys:</p>
+
+<ul>
+ <li><code>path</code> (<strong>string, required</strong>): Path to serial port (e.g. <code>/dev/ttyACM0</code>).</li>
+ <li><code>callback</code> (<strong>function, required</strong>): Callback function for handling messages.</li>
+ <li><code>baud</code> (<strong>integer</strong>); baud rate (default <code>9600</code>).</li>
+ <li><code>format</code> (<strong>string</strong>): frame format (default <code>&apos;8N1&apos;</code>).</li>
+ <li><code>encoding</code> (<strong>string</strong>): method for encoding messages (default <code>&apos;cstring&apos;</code>). May be one of:
+ <ul>
+ <li><code>&apos;cstring&apos;</code>: <code>NULL</code>-terminated strings.</li>
+ </ul></li>
+</ul>
+
+
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">conf</span>
+ <span class="types"><a class="type" href="https://www.lua.org/manual/5.3/manual.html#6.6">table</a></span>
+
+
+
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../classes/serial.html#">serial</a></span>
+
+
+
+ </ol>
+
+
+
+
+</dd>
+</dl>
+
+
+</div> <!-- id="content" -->
+</div> <!-- id="main" -->
+<div id="about">
+<i>generated by <a href="http://github.com/lunarmodules/LDoc">LDoc 1.5.0</a></i>
+<i style="float:right;">Last updated 1980-01-01 00:00:00 </i>
+</div> <!-- id="about" -->
+</div> <!-- id="container" -->
+</body>
+</html>
diff --git a/doc/classes/timer.html b/doc/classes/timer.html
new file mode 100644
index 0000000..a6b16c8
--- /dev/null
+++ b/doc/classes/timer.html
@@ -0,0 +1,226 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+<head>
+ <title>Melisma modules</title>
+ <link rel="stylesheet" href="../ldoc_fixed.css" type="text/css" />
+</head>
+<body>
+
+<div id="container">
+
+<div id="product">
+ <div id="product_logo"></div>
+ <div id="product_name"><big><b></b></big></div>
+ <div id="product_description"></div>
+</div> <!-- id="product" -->
+
+
+<div id="main">
+
+
+<!-- Menu -->
+
+<div id="navigation">
+<p><a href="/melisma-modules">Back to repository</a></p>
+<br/>
+<h1>Melisma modules</h1>
+
+
+<ul>
+ <li><a href="../index.html">Index</a></li>
+</ul>
+
+<h2>Contents</h2>
+<ul>
+<li><a href="#Functions">Functions</a></li>
+<li><a href="#Methods">Methods</a></li>
+</ul>
+
+
+<h2>Classes</h2>
+<ul class="nowrap">
+ <li><a href="../classes/buffer.html">buffer</a></li>
+ <li><a href="../classes/delay.html">delay</a></li>
+ <li><a href="../classes/gain.html">gain</a></li>
+ <li><a href="../classes/interp.html">interp</a></li>
+ <li><a href="../classes/ladspa.html">ladspa</a></li>
+ <li><a href="../classes/mixer.html">mixer</a></li>
+ <li><a href="../classes/nullsink.html">nullsink</a></li>
+ <li><a href="../classes/oscil.html">oscil</a></li>
+ <li><a href="../classes/pan.html">pan</a></li>
+ <li><a href="../classes/playback.html">playback</a></li>
+ <li><a href="../classes/sample.html">sample</a></li>
+ <li><a href="../classes/serial.html">serial</a></li>
+ <li><strong>timer</strong></li>
+</ul>
+<h2>Modules</h2>
+<ul class="nowrap">
+ <li><a href="../modules/biquad.html">biquad</a></li>
+ <li><a href="../modules/expression.html">expression</a></li>
+ <li><a href="../modules/fs.html">fs</a></li>
+ <li><a href="../modules/keycallback.html">keycallback</a></li>
+ <li><a href="../modules/udp.html">udp</a></li>
+</ul>
+
+</div>
+
+<div id="content">
+
+<h1>Class <code>timer</code></h1>
+<p>Time measurement.</p>
+<p>
+
+
+
+<pre>
+<span class="keyword">local</span> mtimer = <span class="global">require</span> <span class="string">'melisma.timer'</span>
+<span class="keyword">local</span> mkeycallback = <span class="global">require</span> <span class="string">'melisma.keycallback'</span>
+
+<span class="keyword">local</span> timer = mtimer.<span class="function-name">init</span>()
+timer:<span class="function-name">start</span>()
+
+mkeycallback.<span class="function-name">set</span>(<span class="keyword">function</span>()
+ <span class="global">print</span>(timer:<span class="function-name">read</span>())
+ timer:<span class="function-name">reset</span>()
+<span class="keyword">end</span>)
+</pre>
+
+</p>
+
+
+<h2><a href="#Functions">Functions</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#timer.init">timer.init()</a></td>
+ <td class="summary">Create a new timer.</td>
+ </tr>
+</table>
+<h2><a href="#Methods">Methods</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#timer:read">timer:read()</a></td>
+ <td class="summary">Read timer.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#timer:reset">timer:reset()</a></td>
+ <td class="summary">Reset timer.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#timer:start">timer:start()</a></td>
+ <td class="summary">Start timer.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#timer:stop">timer:stop()</a></td>
+ <td class="summary">Stop timer.</td>
+ </tr>
+</table>
+
+<br/>
+<br/>
+
+
+ <h2 class="section-header "><a name="Functions"></a>Functions</h2>
+ Methods
+ <dl class="function">
+ <dt>
+ <a name = "timer.init"></a>
+ <strong>timer.init()</strong>
+ </dt>
+ <dd>
+ Create a new timer.
+
+
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../classes/timer.html#">timer</a></span>
+
+
+
+ </ol>
+
+
+
+
+</dd>
+</dl>
+ <h2 class="section-header "><a name="Methods"></a>Methods</h2>
+
+ <dl class="function">
+ <dt>
+ <a name = "timer:read"></a>
+ <strong>timer:read()</strong>
+ </dt>
+ <dd>
+ Read timer.
+
+
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><span class="type">number</span></span>
+ Elapsed seconds.
+ </ol>
+
+
+
+
+</dd>
+ <dt>
+ <a name = "timer:reset"></a>
+ <strong>timer:reset()</strong>
+ </dt>
+ <dd>
+ Reset timer.
+
+
+
+
+
+
+
+</dd>
+ <dt>
+ <a name = "timer:start"></a>
+ <strong>timer:start()</strong>
+ </dt>
+ <dd>
+ Start timer.
+
+
+
+
+
+
+
+</dd>
+ <dt>
+ <a name = "timer:stop"></a>
+ <strong>timer:stop()</strong>
+ </dt>
+ <dd>
+ Stop timer.
+
+
+
+
+
+
+
+</dd>
+</dl>
+
+
+</div> <!-- id="content" -->
+</div> <!-- id="main" -->
+<div id="about">
+<i>generated by <a href="http://github.com/lunarmodules/LDoc">LDoc 1.5.0</a></i>
+<i style="float:right;">Last updated 1980-01-01 00:00:00 </i>
+</div> <!-- id="about" -->
+</div> <!-- id="container" -->
+</body>
+</html>
diff --git a/doc/index.html b/doc/index.html
new file mode 100644
index 0000000..f7d3c3e
--- /dev/null
+++ b/doc/index.html
@@ -0,0 +1,153 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+<head>
+ <title>Melisma modules</title>
+ <link rel="stylesheet" href="ldoc_fixed.css" type="text/css" />
+</head>
+<body>
+
+<div id="container">
+
+<div id="product">
+ <div id="product_logo"></div>
+ <div id="product_name"><big><b></b></big></div>
+ <div id="product_description"></div>
+</div> <!-- id="product" -->
+
+
+<div id="main">
+
+
+<!-- Menu -->
+
+<div id="navigation">
+<p><a href="/melisma-modules">Back to repository</a></p>
+<br/>
+<h1>Melisma modules</h1>
+
+
+
+
+
+<h2>Modules</h2>
+<ul class="nowrap">
+ <li><a href="modules/biquad.html">biquad</a></li>
+ <li><a href="modules/expression.html">expression</a></li>
+ <li><a href="modules/fs.html">fs</a></li>
+ <li><a href="modules/keycallback.html">keycallback</a></li>
+ <li><a href="modules/udp.html">udp</a></li>
+</ul>
+<h2>Classes</h2>
+<ul class="nowrap">
+ <li><a href="classes/buffer.html">buffer</a></li>
+ <li><a href="classes/delay.html">delay</a></li>
+ <li><a href="classes/gain.html">gain</a></li>
+ <li><a href="classes/interp.html">interp</a></li>
+ <li><a href="classes/ladspa.html">ladspa</a></li>
+ <li><a href="classes/mixer.html">mixer</a></li>
+ <li><a href="classes/nullsink.html">nullsink</a></li>
+ <li><a href="classes/oscil.html">oscil</a></li>
+ <li><a href="classes/pan.html">pan</a></li>
+ <li><a href="classes/playback.html">playback</a></li>
+ <li><a href="classes/sample.html">sample</a></li>
+ <li><a href="classes/serial.html">serial</a></li>
+ <li><a href="classes/timer.html">timer</a></li>
+</ul>
+
+</div>
+
+<div id="content">
+
+
+ <h2>Melisma module reference</h2>
+
+<h2>Modules</h2>
+<table class="module_list">
+ <tr>
+ <td class="name" nowrap><a href="modules/biquad.html">biquad</a></td>
+ <td class="summary">Digital biquad filters.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="modules/expression.html">expression</a></td>
+ <td class="summary">Build filters with arithmetic expressions.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="modules/fs.html">fs</a></td>
+ <td class="summary">File system utilities.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="modules/keycallback.html">keycallback</a></td>
+ <td class="summary">Set callback for keyboard input.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="modules/udp.html">udp</a></td>
+ <td class="summary">UDP support.</td>
+ </tr>
+</table>
+<h2>Classes</h2>
+<table class="module_list">
+ <tr>
+ <td class="name" nowrap><a href="classes/buffer.html">buffer</a></td>
+ <td class="summary">Audio buffer for capture/playback.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="classes/delay.html">delay</a></td>
+ <td class="summary">Digital delay line.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="classes/gain.html">gain</a></td>
+ <td class="summary">Apply gain to signals.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="classes/interp.html">interp</a></td>
+ <td class="summary">Interpolation.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="classes/ladspa.html">ladspa</a></td>
+ <td class="summary">LADSPA plugin support.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="classes/mixer.html">mixer</a></td>
+ <td class="summary">Mix signals.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="classes/nullsink.html">nullsink</a></td>
+ <td class="summary">Reference otherwise disconnected nodes.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="classes/oscil.html">oscil</a></td>
+ <td class="summary">Wavetable backed oscillators.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="classes/pan.html">pan</a></td>
+ <td class="summary">Panning</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="classes/playback.html">playback</a></td>
+ <td class="summary">Audio sample playback.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="classes/sample.html">sample</a></td>
+ <td class="summary">Load audio samples.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="classes/serial.html">serial</a></td>
+ <td class="summary">Read data on serial ports.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="classes/timer.html">timer</a></td>
+ <td class="summary">Time measurement.</td>
+ </tr>
+</table>
+
+</div> <!-- id="content" -->
+</div> <!-- id="main" -->
+<div id="about">
+<i>generated by <a href="http://github.com/lunarmodules/LDoc">LDoc 1.5.0</a></i>
+<i style="float:right;">Last updated 1980-01-01 00:00:00 </i>
+</div> <!-- id="about" -->
+</div> <!-- id="container" -->
+</body>
+</html>
diff --git a/doc/ldoc_fixed.css b/doc/ldoc_fixed.css
new file mode 100644
index 0000000..d7c7a6e
--- /dev/null
+++ b/doc/ldoc_fixed.css
@@ -0,0 +1,312 @@
+/* BEGIN RESET
+
+Copyright (c) 2010, Yahoo! Inc. All rights reserved.
+Code licensed under the BSD License:
+http://developer.yahoo.com/yui/license.html
+version: 2.8.2r1
+*/
+html {
+ color: #000;
+ background: #FFF;
+}
+body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td {
+ margin: 0;
+ padding: 0;
+}
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+fieldset,img {
+ border: 0;
+}
+address,caption,cite,code,dfn,em,strong,th,var,optgroup {
+ font-style: inherit;
+ font-weight: inherit;
+}
+del,ins {
+ text-decoration: none;
+}
+li {
+ margin-left: 20px;
+}
+caption,th {
+ text-align: left;
+}
+h1,h2,h3,h4,h5,h6 {
+ font-size: 100%;
+ font-weight: bold;
+}
+q:before,q:after {
+ content: '';
+}
+abbr,acronym {
+ border: 0;
+ font-variant: normal;
+}
+sup {
+ vertical-align: baseline;
+}
+sub {
+ vertical-align: baseline;
+}
+legend {
+ color: #000;
+}
+input,button,textarea,select,optgroup,option {
+ font-family: inherit;
+ font-size: inherit;
+ font-style: inherit;
+ font-weight: inherit;
+}
+input,button,textarea,select {*font-size:100%;
+}
+/* END RESET */
+
+body {
+ margin-left: 1em;
+ margin-right: 1em;
+ font-family: arial, helvetica, geneva, sans-serif;
+ background-color: #ffffff; margin: 0px;
+}
+
+code, tt { font-family: monospace; font-size: 1.1em; }
+span.parameter { font-family:monospace; }
+span.parameter:after { content:":"; }
+span.types:before { content:"("; }
+span.types:after { content:")"; }
+.type { font-weight: bold; font-style:italic }
+
+body, p, td, th { font-size: .95em; line-height: 1.2em;}
+
+p, ul { margin: 10px 0 0 0px;}
+
+strong { font-weight: bold;}
+
+em { font-style: italic;}
+
+h1 {
+ font-size: 1.5em;
+ margin: 0 0 20px 0;
+}
+h2, h3, h4 { margin: 15px 0 10px 0; }
+h2 { font-size: 1.25em; }
+h3 { font-size: 1.15em; }
+h4 { font-size: 1.06em; }
+
+a:link { font-weight: bold; color: #004080; text-decoration: none; }
+a:visited { font-weight: bold; color: #006699; text-decoration: none; }
+a:link:hover { text-decoration: underline; }
+
+hr {
+ color:#cccccc;
+ background: #00007f;
+ height: 1px;
+}
+
+blockquote { margin-left: 3em; }
+
+ul { list-style-type: disc; }
+
+p.name {
+ font-family: "Andale Mono", monospace;
+ padding-top: 1em;
+}
+
+pre {
+ background-color: rgb(245, 245, 245);
+ border: 1px solid #C0C0C0; /* silver */
+ padding: 10px;
+ margin: 10px 0 10px 0;
+ overflow: auto;
+ font-family: "Andale Mono", monospace;
+}
+
+pre.example {
+ font-size: .85em;
+}
+
+table.index { border: 1px #00007f; }
+table.index td { text-align: left; vertical-align: top; }
+
+#container {
+ margin-left: 1em;
+ margin-right: 1em;
+ background-color: #ffffff;
+}
+
+#product {
+ text-align: center;
+ border-bottom: 1px solid #cccccc;
+ background-color: #ffffff;
+}
+
+#product big {
+ font-size: 2em;
+}
+
+#main {
+ background-color:#FFFFFF; // #f0f0f0;
+ border-left: 1px solid #cccccc;
+}
+
+#navigation {
+ position: fixed;
+ top: 0;
+ left: 0;
+ float: left;
+ width: 14em;
+ vertical-align: top;
+ background-color:#FFFFFF; // #f0f0f0;
+ border-right: 2px solid #cccccc;
+ overflow: visible;
+ overflow-y: scroll;
+ height: 100%;
+ padding-left: 1em;
+}
+
+#navigation h2 {
+ background-color:#FFFFFF;//:#e7e7e7;
+ font-size:1.1em;
+ color:#000000;
+ text-align: left;
+ padding:0.2em;
+ border-bottom:1px solid #dddddd;
+}
+
+#navigation ul
+{
+ font-size:1em;
+ list-style-type: none;
+ margin: 1px 1px 10px 1px;
+}
+
+#navigation li {
+ text-indent: -1em;
+ display: block;
+ margin: 3px 0px 0px 22px;
+}
+
+#navigation li li a {
+ margin: 0px 3px 0px -1em;
+}
+
+#content {
+ margin-left: 14em;
+ padding: 1em;
+ padding-left: 2em;
+ width: 700px;
+ border-left: 2px solid #cccccc;
+ // border-right: 2px solid #cccccc;
+ background-color: #ffffff;
+}
+
+#about {
+ clear: both;
+ padding-left: 1em;
+ margin-left: 14em; // avoid the damn sidebar!
+ border-top: 2px solid #cccccc;
+ border-left: 2px solid #cccccc;
+ background-color: #ffffff;
+}
+
+@media print {
+ body {
+ font: 12pt "Times New Roman", "TimeNR", Times, serif;
+ }
+ a { font-weight: bold; color: #004080; text-decoration: underline; }
+
+ #main {
+ background-color: #ffffff;
+ border-left: 0px;
+ }
+
+ #container {
+ margin-left: 2%;
+ margin-right: 2%;
+ background-color: #ffffff;
+ }
+
+ #content {
+ padding: 1em;
+ background-color: #ffffff;
+ }
+
+ #navigation {
+ display: none;
+ }
+ pre.example {
+ font-family: "Andale Mono", monospace;
+ font-size: 10pt;
+ page-break-inside: avoid;
+ }
+}
+
+table.module_list {
+ border-width: 1px;
+ border-style: solid;
+ border-color: #cccccc;
+ border-collapse: collapse;
+}
+table.module_list td {
+ border-width: 1px;
+ padding: 3px;
+ border-style: solid;
+ border-color: #cccccc;
+}
+table.module_list td.name { background-color: #f0f0f0; ; min-width: 200px; }
+table.module_list td.summary { width: 100%; }
+
+table.function_list {
+ border-width: 1px;
+ border-style: solid;
+ border-color: #cccccc;
+ border-collapse: collapse;
+}
+table.function_list td {
+ border-width: 1px;
+ padding: 3px;
+ border-style: solid;
+ border-color: #cccccc;
+}
+table.function_list td.name { background-color: #f6f6ff; ; min-width: 200px; }
+table.function_list td.summary { width: 100%; }
+
+dl.table dt, dl.function dt {border-top: 1px solid #ccc; padding-top: 1em;}
+dl.table dd, dl.function dd {padding-bottom: 1em; margin: 10px 0 0 20px;}
+dl.table h3, dl.function h3 {font-size: .95em;}
+
+ul.nowrap {
+ overflow:auto;
+ whitespace:nowrap;
+}
+
+/* stop sublists from having initial vertical space */
+ul ul { margin-top: 0px; }
+ol ul { margin-top: 0px; }
+ol ol { margin-top: 0px; }
+ul ol { margin-top: 0px; }
+
+/* make the target distinct; helps when we're navigating to a function */
+a:target + * {
+ background-color: #FF9;
+}
+
+
+/* styles for prettification of source */
+pre .comment { color: #558817; }
+pre .constant { color: #a8660d; }
+pre .escape { color: #844631; }
+pre .keyword { color: #aa5050; font-weight: bold; }
+pre .library { color: #0e7c6b; }
+pre .marker { color: #512b1e; background: #fedc56; font-weight: bold; }
+pre .string { color: #8080ff; }
+pre .number { color: #f8660d; }
+pre .function-name { color: #60447f; }
+pre .operator { color: #2239a8; font-weight: bold; }
+pre .preprocessor, pre .prepro { color: #a33243; }
+pre .global { color: #800080; }
+pre .user-keyword { color: #800080; }
+pre .prompt { color: #558817; }
+pre .url { color: #272fc2; text-decoration: underline; }
+
diff --git a/doc/modules/biquad.html b/doc/modules/biquad.html
new file mode 100644
index 0000000..4f89b1a
--- /dev/null
+++ b/doc/modules/biquad.html
@@ -0,0 +1,189 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+<head>
+ <title>Melisma modules</title>
+ <link rel="stylesheet" href="../ldoc_fixed.css" type="text/css" />
+</head>
+<body>
+
+<div id="container">
+
+<div id="product">
+ <div id="product_logo"></div>
+ <div id="product_name"><big><b></b></big></div>
+ <div id="product_description"></div>
+</div> <!-- id="product" -->
+
+
+<div id="main">
+
+
+<!-- Menu -->
+
+<div id="navigation">
+<p><a href="/melisma-modules">Back to repository</a></p>
+<br/>
+<h1>Melisma modules</h1>
+
+
+<ul>
+ <li><a href="../index.html">Index</a></li>
+</ul>
+
+<h2>Contents</h2>
+<ul>
+<li><a href="#Class_biquad_lowpass">Class biquad.lowpass </a></li>
+</ul>
+
+
+<h2>Modules</h2>
+<ul class="nowrap">
+ <li><strong>biquad</strong></li>
+ <li><a href="../modules/expression.html">expression</a></li>
+ <li><a href="../modules/fs.html">fs</a></li>
+ <li><a href="../modules/keycallback.html">keycallback</a></li>
+ <li><a href="../modules/udp.html">udp</a></li>
+</ul>
+<h2>Classes</h2>
+<ul class="nowrap">
+ <li><a href="../classes/buffer.html">buffer</a></li>
+ <li><a href="../classes/delay.html">delay</a></li>
+ <li><a href="../classes/gain.html">gain</a></li>
+ <li><a href="../classes/interp.html">interp</a></li>
+ <li><a href="../classes/ladspa.html">ladspa</a></li>
+ <li><a href="../classes/mixer.html">mixer</a></li>
+ <li><a href="../classes/nullsink.html">nullsink</a></li>
+ <li><a href="../classes/oscil.html">oscil</a></li>
+ <li><a href="../classes/pan.html">pan</a></li>
+ <li><a href="../classes/playback.html">playback</a></li>
+ <li><a href="../classes/sample.html">sample</a></li>
+ <li><a href="../classes/serial.html">serial</a></li>
+ <li><a href="../classes/timer.html">timer</a></li>
+</ul>
+
+</div>
+
+<div id="content">
+
+<h1>Module <code>biquad</code></h1>
+<p>Digital biquad filters.</p>
+<p>
+
+</p>
+
+
+<h2><a href="#Class_biquad_lowpass">Class biquad.lowpass </a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#biquad.lowpass.init">biquad.lowpass.init(input)</a></td>
+ <td class="summary">Create a lowpass filter.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#lowpass:cutoff">lowpass:cutoff(cut)</a></td>
+ <td class="summary">Set filter cutoff frequency.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#lowpass:resonance">lowpass:resonance(res)</a></td>
+ <td class="summary">Set filter resonance.</td>
+ </tr>
+</table>
+
+<br/>
+<br/>
+
+
+ <h2 class="section-header has-description"><a name="Class_biquad_lowpass"></a>Class biquad.lowpass </h2>
+
+ <div class="section-description">
+ Lowpass filter.
+ </div>
+ <h3>Usage:</h3>
+ <pre class="example"><span class="keyword">local</span> mbiquad = <span class="global">require</span> <span class="string">'melisma.biquad'</span>
+<span class="keyword">local</span> input = ...
+<span class="keyword">local</span> lowpass = mbiquad.lowpass.<span class="function-name">init</span>(node, <span class="number">2000</span>, <span class="number">0.6</span>)
+lowpass:<span class="function-name">resonance</span>(<span class="number">1000</span>)
+lowpass:<span class="function-name">cutoff</span>(<span class="number">0.25</span>)</pre>
+ <dl class="function">
+ <dt>
+ <a name = "biquad.lowpass.init"></a>
+ <strong>biquad.lowpass.init(input)</strong>
+ </dt>
+ <dd>
+ Create a lowpass filter.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">input</span>
+ <span class="types"><span class="type">channel</span></span>
+ Input signal.
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../modules/biquad.html#biquad.lowpass">biquad.lowpass</a></span>
+ The newly created lowpass filter.
+ </ol>
+
+
+
+
+</dd>
+ <dt>
+ <a name = "lowpass:cutoff"></a>
+ <strong>lowpass:cutoff(cut)</strong>
+ </dt>
+ <dd>
+ Set filter cutoff frequency.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">cut</span>
+ <span class="types"><span class="type">number</span></span>
+ Cutoff frequency.
+ </li>
+ </ul>
+
+
+
+
+
+</dd>
+ <dt>
+ <a name = "lowpass:resonance"></a>
+ <strong>lowpass:resonance(res)</strong>
+ </dt>
+ <dd>
+ Set filter resonance.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">res</span>
+ <span class="types"><span class="type">number</span></span>
+ Resonance.
+ </li>
+ </ul>
+
+
+
+
+
+</dd>
+</dl>
+
+
+</div> <!-- id="content" -->
+</div> <!-- id="main" -->
+<div id="about">
+<i>generated by <a href="http://github.com/lunarmodules/LDoc">LDoc 1.5.0</a></i>
+<i style="float:right;">Last updated 1980-01-01 00:00:00 </i>
+</div> <!-- id="about" -->
+</div> <!-- id="container" -->
+</body>
+</html>
diff --git a/doc/modules/expression.html b/doc/modules/expression.html
new file mode 100644
index 0000000..e0f2f6e
--- /dev/null
+++ b/doc/modules/expression.html
@@ -0,0 +1,221 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+<head>
+ <title>Melisma modules</title>
+ <link rel="stylesheet" href="../ldoc_fixed.css" type="text/css" />
+</head>
+<body>
+
+<div id="container">
+
+<div id="product">
+ <div id="product_logo"></div>
+ <div id="product_name"><big><b></b></big></div>
+ <div id="product_description"></div>
+</div> <!-- id="product" -->
+
+
+<div id="main">
+
+
+<!-- Menu -->
+
+<div id="navigation">
+<p><a href="/melisma-modules">Back to repository</a></p>
+<br/>
+<h1>Melisma modules</h1>
+
+
+<ul>
+ <li><a href="../index.html">Index</a></li>
+</ul>
+
+<h2>Contents</h2>
+<ul>
+<li><a href="#Functions">Functions</a></li>
+<li><a href="#Class_script">Class script </a></li>
+</ul>
+
+
+<h2>Modules</h2>
+<ul class="nowrap">
+ <li><a href="../modules/biquad.html">biquad</a></li>
+ <li><strong>expression</strong></li>
+ <li><a href="../modules/fs.html">fs</a></li>
+ <li><a href="../modules/keycallback.html">keycallback</a></li>
+ <li><a href="../modules/udp.html">udp</a></li>
+</ul>
+<h2>Classes</h2>
+<ul class="nowrap">
+ <li><a href="../classes/buffer.html">buffer</a></li>
+ <li><a href="../classes/delay.html">delay</a></li>
+ <li><a href="../classes/gain.html">gain</a></li>
+ <li><a href="../classes/interp.html">interp</a></li>
+ <li><a href="../classes/ladspa.html">ladspa</a></li>
+ <li><a href="../classes/mixer.html">mixer</a></li>
+ <li><a href="../classes/nullsink.html">nullsink</a></li>
+ <li><a href="../classes/oscil.html">oscil</a></li>
+ <li><a href="../classes/pan.html">pan</a></li>
+ <li><a href="../classes/playback.html">playback</a></li>
+ <li><a href="../classes/sample.html">sample</a></li>
+ <li><a href="../classes/serial.html">serial</a></li>
+ <li><a href="../classes/timer.html">timer</a></li>
+</ul>
+
+</div>
+
+<div id="content">
+
+<h1>Module <code>expression</code></h1>
+<p>Build filters with arithmetic expressions.</p>
+<p>
+
+
+
+<pre>
+<span class="keyword">local</span> mexpression = <span class="global">require</span> <span class="string">'melisma.expression'</span>
+<span class="keyword">local</span> input = mepxression.input
+
+<span class="keyword">local</span> lhs = ...
+<span class="keyword">local</span> rhs = ...
+
+<span class="keyword">local</span> script = <span class="function-name">input</span>(lhs,<span class="number">0</span>) + <span class="number">0.5</span> * <span class="function-name">input</span>(rhs,<span class="number">10</span>)
+<span class="keyword">local</span> expression = script:<span class="function-name">build</span>()
+</pre>
+
+</p>
+
+
+<h2><a href="#Functions">Functions</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#input">input(sig, off)</a></td>
+ <td class="summary">Introduce an input signal in the expression.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#output">output(off)</a></td>
+ <td class="summary">Introduce the output signal in the expression.</td>
+ </tr>
+</table>
+<h2><a href="#Class_script">Class script </a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#script:build">script:build()</a></td>
+ <td class="summary">Compile an expression script.</td>
+ </tr>
+</table>
+
+<br/>
+<br/>
+
+
+ <h2 class="section-header "><a name="Functions"></a>Functions</h2>
+ Methods
+ <dl class="function">
+ <dt>
+ <a name = "input"></a>
+ <strong>input(sig, off)</strong>
+ </dt>
+ <dd>
+ Introduce an input signal in the expression.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">sig</span>
+ <span class="types"><span class="type">channel</span></span>
+ Input signal.
+ </li>
+ <li><span class="parameter">off</span>
+ <span class="types"><span class="type">integer</span></span>
+ Offset in no. of samples.
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../modules/expression.html#script">script</a></span>
+
+
+
+ </ol>
+
+
+
+
+</dd>
+ <dt>
+ <a name = "output"></a>
+ <strong>output(off)</strong>
+ </dt>
+ <dd>
+ Introduce the output signal in the expression.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">off</span>
+ <span class="types"><span class="type">integer</span></span>
+ Offset in no. of samples.
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../modules/expression.html#script">script</a></span>
+
+
+
+ </ol>
+
+
+
+
+</dd>
+</dl>
+ <h2 class="section-header has-description"><a name="Class_script"></a>Class script </h2>
+
+ <div class="section-description">
+ Represents an expression before compilation.
+Implements <code>__add</code>, <code>__sub</code>, <code>__mul</code> and <code>__div</code> for
+combining scripts in to larger ones.
+ </div>
+ <dl class="function">
+ <dt>
+ <a name = "script:build"></a>
+ <strong>script:build()</strong>
+ </dt>
+ <dd>
+ Compile an expression script.
+
+
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><span class="type">expression</span></span>
+
+
+
+ </ol>
+
+
+
+
+</dd>
+</dl>
+
+
+</div> <!-- id="content" -->
+</div> <!-- id="main" -->
+<div id="about">
+<i>generated by <a href="http://github.com/lunarmodules/LDoc">LDoc 1.5.0</a></i>
+<i style="float:right;">Last updated 1980-01-01 00:00:00 </i>
+</div> <!-- id="about" -->
+</div> <!-- id="container" -->
+</body>
+</html>
diff --git a/doc/modules/fs.html b/doc/modules/fs.html
new file mode 100644
index 0000000..bfe5be3
--- /dev/null
+++ b/doc/modules/fs.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+<head>
+ <title>Melisma modules</title>
+ <link rel="stylesheet" href="../ldoc_fixed.css" type="text/css" />
+</head>
+<body>
+
+<div id="container">
+
+<div id="product">
+ <div id="product_logo"></div>
+ <div id="product_name"><big><b></b></big></div>
+ <div id="product_description"></div>
+</div> <!-- id="product" -->
+
+
+<div id="main">
+
+
+<!-- Menu -->
+
+<div id="navigation">
+<p><a href="/melisma-modules">Back to repository</a></p>
+<br/>
+<h1>Melisma modules</h1>
+
+
+<ul>
+ <li><a href="../index.html">Index</a></li>
+</ul>
+
+<h2>Contents</h2>
+<ul>
+<li><a href="#Functions">Functions</a></li>
+</ul>
+
+
+<h2>Modules</h2>
+<ul class="nowrap">
+ <li><a href="../modules/biquad.html">biquad</a></li>
+ <li><a href="../modules/expression.html">expression</a></li>
+ <li><strong>fs</strong></li>
+ <li><a href="../modules/keycallback.html">keycallback</a></li>
+ <li><a href="../modules/udp.html">udp</a></li>
+</ul>
+<h2>Classes</h2>
+<ul class="nowrap">
+ <li><a href="../classes/buffer.html">buffer</a></li>
+ <li><a href="../classes/delay.html">delay</a></li>
+ <li><a href="../classes/gain.html">gain</a></li>
+ <li><a href="../classes/interp.html">interp</a></li>
+ <li><a href="../classes/ladspa.html">ladspa</a></li>
+ <li><a href="../classes/mixer.html">mixer</a></li>
+ <li><a href="../classes/nullsink.html">nullsink</a></li>
+ <li><a href="../classes/oscil.html">oscil</a></li>
+ <li><a href="../classes/pan.html">pan</a></li>
+ <li><a href="../classes/playback.html">playback</a></li>
+ <li><a href="../classes/sample.html">sample</a></li>
+ <li><a href="../classes/serial.html">serial</a></li>
+ <li><a href="../classes/timer.html">timer</a></li>
+</ul>
+
+</div>
+
+<div id="content">
+
+<h1>Module <code>fs</code></h1>
+<p>File system utilities.</p>
+<p>
+
+</p>
+
+
+<h2><a href="#Functions">Functions</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#readdir">readdir(path)</a></td>
+ <td class="summary">List directory contents.</td>
+ </tr>
+</table>
+
+<br/>
+<br/>
+
+
+ <h2 class="section-header "><a name="Functions"></a>Functions</h2>
+ Methods
+ <dl class="function">
+ <dt>
+ <a name = "readdir"></a>
+ <strong>readdir(path)</strong>
+ </dt>
+ <dd>
+ List directory contents.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">path</span>
+ <span class="types"><a class="type" href="https://www.lua.org/manual/5.3/manual.html#6.4">string</a></span>
+ Directory path.
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="https://www.lua.org/manual/5.3/manual.html#6.6">table</a></span>
+ Directory contents.
+ </ol>
+
+
+
+ <h3>Usage:</h3>
+ <ul>
+ <pre class="example"><span class="keyword">local</span> fs = <span class="global">require</span> <span class="string">'melisma.fs'</span>
+<span class="keyword">local</span> contents = fs.<span class="function-name">readdir</span>(<span class="string">'.'</span>)
+<span class="keyword">for</span> _,entry <span class="keyword">in</span> <span class="global">ipairs</span>(contents) <span class="keyword">do</span>
+ <span class="global">print</span>(entry.name, entry.<span class="global">type</span>)
+<span class="keyword">end</span></pre>
+ </ul>
+
+</dd>
+</dl>
+
+
+</div> <!-- id="content" -->
+</div> <!-- id="main" -->
+<div id="about">
+<i>generated by <a href="http://github.com/lunarmodules/LDoc">LDoc 1.5.0</a></i>
+<i style="float:right;">Last updated 1980-01-01 00:00:00 </i>
+</div> <!-- id="about" -->
+</div> <!-- id="container" -->
+</body>
+</html>
diff --git a/doc/modules/keycallback.html b/doc/modules/keycallback.html
new file mode 100644
index 0000000..af74b90
--- /dev/null
+++ b/doc/modules/keycallback.html
@@ -0,0 +1,133 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+<head>
+ <title>Melisma modules</title>
+ <link rel="stylesheet" href="../ldoc_fixed.css" type="text/css" />
+</head>
+<body>
+
+<div id="container">
+
+<div id="product">
+ <div id="product_logo"></div>
+ <div id="product_name"><big><b></b></big></div>
+ <div id="product_description"></div>
+</div> <!-- id="product" -->
+
+
+<div id="main">
+
+
+<!-- Menu -->
+
+<div id="navigation">
+<p><a href="/melisma-modules">Back to repository</a></p>
+<br/>
+<h1>Melisma modules</h1>
+
+
+<ul>
+ <li><a href="../index.html">Index</a></li>
+</ul>
+
+<h2>Contents</h2>
+<ul>
+<li><a href="#Functions">Functions</a></li>
+</ul>
+
+
+<h2>Modules</h2>
+<ul class="nowrap">
+ <li><a href="../modules/biquad.html">biquad</a></li>
+ <li><a href="../modules/expression.html">expression</a></li>
+ <li><a href="../modules/fs.html">fs</a></li>
+ <li><strong>keycallback</strong></li>
+ <li><a href="../modules/udp.html">udp</a></li>
+</ul>
+<h2>Classes</h2>
+<ul class="nowrap">
+ <li><a href="../classes/buffer.html">buffer</a></li>
+ <li><a href="../classes/delay.html">delay</a></li>
+ <li><a href="../classes/gain.html">gain</a></li>
+ <li><a href="../classes/interp.html">interp</a></li>
+ <li><a href="../classes/ladspa.html">ladspa</a></li>
+ <li><a href="../classes/mixer.html">mixer</a></li>
+ <li><a href="../classes/nullsink.html">nullsink</a></li>
+ <li><a href="../classes/oscil.html">oscil</a></li>
+ <li><a href="../classes/pan.html">pan</a></li>
+ <li><a href="../classes/playback.html">playback</a></li>
+ <li><a href="../classes/sample.html">sample</a></li>
+ <li><a href="../classes/serial.html">serial</a></li>
+ <li><a href="../classes/timer.html">timer</a></li>
+</ul>
+
+</div>
+
+<div id="content">
+
+<h1>Module <code>keycallback</code></h1>
+<p>Set callback for keyboard input.</p>
+<p>
+
+
+<pre>
+<span class="keyword">local</span> mkeycallback = <span class="global">require</span> <span class="string">'melisma.keycallback'</span>
+
+mkeycallback.<span class="function-name">set</span>(<span class="keyword">function</span>(c)
+ <span class="global">print</span>(<span class="string">'you pressed'</span>, c)
+<span class="keyword">end</span>)
+</pre>
+
+</p>
+
+
+<h2><a href="#Functions">Functions</a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#set">set(fn)</a></td>
+ <td class="summary">Set the callback.</td>
+ </tr>
+</table>
+
+<br/>
+<br/>
+
+
+ <h2 class="section-header "><a name="Functions"></a>Functions</h2>
+ Methods
+ <dl class="function">
+ <dt>
+ <a name = "set"></a>
+ <strong>set(fn)</strong>
+ </dt>
+ <dd>
+ Set the callback.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">fn</span>
+ <span class="types"><span class="type">function</span></span>
+ Callback function.
+ </li>
+ </ul>
+
+
+
+
+
+</dd>
+</dl>
+
+
+</div> <!-- id="content" -->
+</div> <!-- id="main" -->
+<div id="about">
+<i>generated by <a href="http://github.com/lunarmodules/LDoc">LDoc 1.5.0</a></i>
+<i style="float:right;">Last updated 1980-01-01 00:00:00 </i>
+</div> <!-- id="about" -->
+</div> <!-- id="container" -->
+</body>
+</html>
diff --git a/doc/modules/udp.html b/doc/modules/udp.html
new file mode 100644
index 0000000..f19c300
--- /dev/null
+++ b/doc/modules/udp.html
@@ -0,0 +1,311 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+<head>
+ <title>Melisma modules</title>
+ <link rel="stylesheet" href="../ldoc_fixed.css" type="text/css" />
+</head>
+<body>
+
+<div id="container">
+
+<div id="product">
+ <div id="product_logo"></div>
+ <div id="product_name"><big><b></b></big></div>
+ <div id="product_description"></div>
+</div> <!-- id="product" -->
+
+
+<div id="main">
+
+
+<!-- Menu -->
+
+<div id="navigation">
+<p><a href="/melisma-modules">Back to repository</a></p>
+<br/>
+<h1>Melisma modules</h1>
+
+
+<ul>
+ <li><a href="../index.html">Index</a></li>
+</ul>
+
+<h2>Contents</h2>
+<ul>
+<li><a href="#Class_sockaddr">Class sockaddr </a></li>
+<li><a href="#Class_socket">Class socket </a></li>
+</ul>
+
+
+<h2>Modules</h2>
+<ul class="nowrap">
+ <li><a href="../modules/biquad.html">biquad</a></li>
+ <li><a href="../modules/expression.html">expression</a></li>
+ <li><a href="../modules/fs.html">fs</a></li>
+ <li><a href="../modules/keycallback.html">keycallback</a></li>
+ <li><strong>udp</strong></li>
+</ul>
+<h2>Classes</h2>
+<ul class="nowrap">
+ <li><a href="../classes/buffer.html">buffer</a></li>
+ <li><a href="../classes/delay.html">delay</a></li>
+ <li><a href="../classes/gain.html">gain</a></li>
+ <li><a href="../classes/interp.html">interp</a></li>
+ <li><a href="../classes/ladspa.html">ladspa</a></li>
+ <li><a href="../classes/mixer.html">mixer</a></li>
+ <li><a href="../classes/nullsink.html">nullsink</a></li>
+ <li><a href="../classes/oscil.html">oscil</a></li>
+ <li><a href="../classes/pan.html">pan</a></li>
+ <li><a href="../classes/playback.html">playback</a></li>
+ <li><a href="../classes/sample.html">sample</a></li>
+ <li><a href="../classes/serial.html">serial</a></li>
+ <li><a href="../classes/timer.html">timer</a></li>
+</ul>
+
+</div>
+
+<div id="content">
+
+<h1>Module <code>udp</code></h1>
+<p>UDP support.</p>
+<p>
+
+
+
+<pre>
+<span class="keyword">local</span> mudp = <span class="global">require</span> <span class="string">'melisma.udp'</span>
+
+<span class="keyword">local</span> addr = mudp.sockaddr.<span class="function-name">inet</span>(<span class="string">'127.0.0.1'</span>,<span class="number">8000</span>)
+<span class="keyword">local</span> sock = mudp.socket.<span class="function-name">inet</span>()
+
+sock:<span class="function-name">send</span>(addr,<span class="string">'hello'</span>)
+sock:<span class="function-name">bind</span>(addr,<span class="global">print</span>)
+</pre>
+
+</p>
+
+
+<h2><a href="#Class_sockaddr">Class sockaddr </a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#sockaddr.inet">sockaddr.inet(host, port)</a></td>
+ <td class="summary">Create a internet domain address description.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#sockaddr.unix">sockaddr.unix(path)</a></td>
+ <td class="summary">Create a unix domain address description.</td>
+ </tr>
+</table>
+<h2><a href="#Class_socket">Class socket </a></h2>
+<table class="function_list">
+ <tr>
+ <td class="name" nowrap><a href="#socket.inet">socket.inet()</a></td>
+ <td class="summary">Create an internet domain socket.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#socket.unix">socket.unix()</a></td>
+ <td class="summary">Create a unix domain socket.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#socket:bind">socket:bind(addr, fn)</a></td>
+ <td class="summary">Bind a socket to an address.</td>
+ </tr>
+ <tr>
+ <td class="name" nowrap><a href="#socket:send">socket:send(addr, pkt)</a></td>
+ <td class="summary">Send a packet on a socket.</td>
+ </tr>
+</table>
+
+<br/>
+<br/>
+
+
+ <h2 class="section-header has-description"><a name="Class_sockaddr"></a>Class sockaddr </h2>
+
+ <div class="section-description">
+ Socket address description.
+ </div>
+ <dl class="function">
+ <dt>
+ <a name = "sockaddr.inet"></a>
+ <strong>sockaddr.inet(host, port)</strong>
+ </dt>
+ <dd>
+ Create a internet domain address description.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">host</span>
+ <span class="types"><a class="type" href="https://www.lua.org/manual/5.3/manual.html#6.4">string</a></span>
+
+
+
+ </li>
+ <li><span class="parameter">port</span>
+ <span class="types"><span class="type">integer</span></span>
+
+
+
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../modules/udp.html#sockaddr">sockaddr</a></span>
+
+
+
+ </ol>
+
+
+
+
+</dd>
+ <dt>
+ <a name = "sockaddr.unix"></a>
+ <strong>sockaddr.unix(path)</strong>
+ </dt>
+ <dd>
+ Create a unix domain address description.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">path</span>
+ <span class="types"><a class="type" href="https://www.lua.org/manual/5.3/manual.html#6.4">string</a></span>
+
+
+
+ </li>
+ </ul>
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../modules/udp.html#sockaddr">sockaddr</a></span>
+
+
+
+ </ol>
+
+
+
+
+</dd>
+</dl>
+ <h2 class="section-header has-description"><a name="Class_socket"></a>Class socket </h2>
+
+ <div class="section-description">
+ Handle to a socket.
+ </div>
+ <dl class="function">
+ <dt>
+ <a name = "socket.inet"></a>
+ <strong>socket.inet()</strong>
+ </dt>
+ <dd>
+ Create an internet domain socket.
+
+
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../modules/udp.html#socket">socket</a></span>
+
+
+
+ </ol>
+
+
+
+
+</dd>
+ <dt>
+ <a name = "socket.unix"></a>
+ <strong>socket.unix()</strong>
+ </dt>
+ <dd>
+ Create a unix domain socket.
+
+
+
+ <h3>Returns:</h3>
+ <ol>
+
+ <span class="types"><a class="type" href="../modules/udp.html#socket">socket</a></span>
+
+
+
+ </ol>
+
+
+
+
+</dd>
+ <dt>
+ <a name = "socket:bind"></a>
+ <strong>socket:bind(addr, fn)</strong>
+ </dt>
+ <dd>
+ Bind a socket to an address.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">addr</span>
+ <span class="types"><a class="type" href="../modules/udp.html#sockaddr">sockaddr</a></span>
+ Address description.
+ </li>
+ <li><span class="parameter">fn</span>
+ <span class="types"><span class="type">function</span></span>
+ Packet callback function.
+ </li>
+ </ul>
+
+
+
+
+
+</dd>
+ <dt>
+ <a name = "socket:send"></a>
+ <strong>socket:send(addr, pkt)</strong>
+ </dt>
+ <dd>
+ Send a packet on a socket.
+
+
+ <h3>Parameters:</h3>
+ <ul>
+ <li><span class="parameter">addr</span>
+ <span class="types"><a class="type" href="../modules/udp.html#sockaddr">sockaddr</a></span>
+ Address description.
+ </li>
+ <li><span class="parameter">pkt</span>
+ <span class="types"><a class="type" href="https://www.lua.org/manual/5.3/manual.html#6.4">string</a></span>
+ Packet to send.
+ </li>
+ </ul>
+
+
+
+
+
+</dd>
+</dl>
+
+
+</div> <!-- id="content" -->
+</div> <!-- id="main" -->
+<div id="about">
+<i>generated by <a href="http://github.com/lunarmodules/LDoc">LDoc 1.5.0</a></i>
+<i style="float:right;">Last updated 1980-01-01 00:00:00 </i>
+</div> <!-- id="about" -->
+</div> <!-- id="container" -->
+</body>
+</html>
diff --git a/external/ladspa.h b/external/ladspa.h
new file mode 100644
index 0000000..b1a9c4e
--- /dev/null
+++ b/external/ladspa.h
@@ -0,0 +1,603 @@
+/* ladspa.h
+
+ Linux Audio Developer's Simple Plugin API Version 1.1[LGPL].
+ Copyright (C) 2000-2002 Richard W.E. Furse, Paul Barton-Davis,
+ Stefan Westerfeld.
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public License
+ as published by the Free Software Foundation; either version 2.1 of
+ the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ USA. */
+
+#ifndef LADSPA_INCLUDED
+#define LADSPA_INCLUDED
+
+#define LADSPA_VERSION "1.1"
+#define LADSPA_VERSION_MAJOR 1
+#define LADSPA_VERSION_MINOR 1
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*****************************************************************************/
+
+/* Overview:
+
+ There is a large number of synthesis packages in use or development
+ on the Linux platform at this time. This API (`The Linux Audio
+ Developer's Simple Plugin API') attempts to give programmers the
+ ability to write simple `plugin' audio processors in C/C++ and link
+ them dynamically (`plug') into a range of these packages (`hosts').
+ It should be possible for any host and any plugin to communicate
+ completely through this interface.
+
+ This API is deliberately short and simple. To achieve compatibility
+ with a range of promising Linux sound synthesis packages it
+ attempts to find the `greatest common divisor' in their logical
+ behaviour. Having said this, certain limiting decisions are
+ implicit, notably the use of a fixed type (LADSPA_Data) for all
+ data transfer and absence of a parameterised `initialisation'
+ phase. See below for the LADSPA_Data typedef.
+
+ Plugins are expected to distinguish between control and audio
+ data. Plugins have `ports' that are inputs or outputs for audio or
+ control data and each plugin is `run' for a `block' corresponding
+ to a short time interval measured in samples. Audio data is
+ communicated using arrays of LADSPA_Data, allowing a block of audio
+ to be processed by the plugin in a single pass. Control data is
+ communicated using single LADSPA_Data values. Control data has a
+ single value at the start of a call to the `run()' or `run_adding()'
+ function, and may be considered to remain this value for its
+ duration. The plugin may assume that all its input and output ports
+ have been connected to the relevant data location (see the
+ `connect_port()' function below) before it is asked to run.
+
+ Plugins will reside in shared object files suitable for dynamic
+ linking by dlopen() and family. The file will provide a number of
+ `plugin types' that can be used to instantiate actual plugins
+ (sometimes known as `plugin instances') that can be connected
+ together to perform tasks.
+
+ This API contains very limited error-handling. */
+
+/*****************************************************************************/
+
+/* Fundamental data type passed in and out of plugin. This data type
+ is used to communicate audio samples and control values. It is
+ assumed that the plugin will work sensibly given any numeric input
+ value although it may have a preferred range (see hints below).
+
+ For audio it is generally assumed that 1.0f is the `0dB' reference
+ amplitude and is a `normal' signal level. */
+
+typedef float LADSPA_Data;
+
+/*****************************************************************************/
+
+/* Special Plugin Properties:
+
+ Optional features of the plugin type are encapsulated in the
+ LADSPA_Properties type. This is assembled by ORing individual
+ properties together. */
+
+typedef int LADSPA_Properties;
+
+/* Property LADSPA_PROPERTY_REALTIME indicates that the plugin has a
+ real-time dependency (e.g. listens to a MIDI device) and so its
+ output must not be cached or subject to significant latency. */
+#define LADSPA_PROPERTY_REALTIME 0x1
+
+/* Property LADSPA_PROPERTY_INPLACE_BROKEN indicates that the plugin
+ may cease to work correctly if the host elects to use the same data
+ location for both input and output (see connect_port()). This
+ should be avoided as enabling this flag makes it impossible for
+ hosts to use the plugin to process audio `in-place.' */
+#define LADSPA_PROPERTY_INPLACE_BROKEN 0x2
+
+/* Property LADSPA_PROPERTY_HARD_RT_CAPABLE indicates that the plugin
+ is capable of running not only in a conventional host but also in a
+ `hard real-time' environment. To qualify for this the plugin must
+ satisfy all of the following:
+
+ (1) The plugin must not use malloc(), free() or other heap memory
+ management within its run() or run_adding() functions. All new
+ memory used in run() must be managed via the stack. These
+ restrictions only apply to the run() function.
+
+ (2) The plugin will not attempt to make use of any library
+ functions with the exceptions of functions in the ANSI standard C
+ and C maths libraries, which the host is expected to provide.
+
+ (3) The plugin will not access files, devices, pipes, sockets, IPC
+ or any other mechanism that might result in process or thread
+ blocking.
+
+ (4) The plugin will take an amount of time to execute a run() or
+ run_adding() call approximately of form (A+B*SampleCount) where A
+ and B depend on the machine and host in use. This amount of time
+ may not depend on input signals or plugin state. The host is left
+ the responsibility to perform timings to estimate upper bounds for
+ A and B. */
+#define LADSPA_PROPERTY_HARD_RT_CAPABLE 0x4
+
+#define LADSPA_IS_REALTIME(x) ((x) & LADSPA_PROPERTY_REALTIME)
+#define LADSPA_IS_INPLACE_BROKEN(x) ((x) & LADSPA_PROPERTY_INPLACE_BROKEN)
+#define LADSPA_IS_HARD_RT_CAPABLE(x) ((x) & LADSPA_PROPERTY_HARD_RT_CAPABLE)
+
+/*****************************************************************************/
+
+/* Plugin Ports:
+
+ Plugins have `ports' that are inputs or outputs for audio or
+ data. Ports can communicate arrays of LADSPA_Data (for audio
+ inputs/outputs) or single LADSPA_Data values (for control
+ input/outputs). This information is encapsulated in the
+ LADSPA_PortDescriptor type which is assembled by ORing individual
+ properties together.
+
+ Note that a port must be an input or an output port but not both
+ and that a port must be a control or audio port but not both. */
+
+typedef int LADSPA_PortDescriptor;
+
+/* Property LADSPA_PORT_INPUT indicates that the port is an input. */
+#define LADSPA_PORT_INPUT 0x1
+
+/* Property LADSPA_PORT_OUTPUT indicates that the port is an output. */
+#define LADSPA_PORT_OUTPUT 0x2
+
+/* Property LADSPA_PORT_CONTROL indicates that the port is a control
+ port. */
+#define LADSPA_PORT_CONTROL 0x4
+
+/* Property LADSPA_PORT_AUDIO indicates that the port is a audio
+ port. */
+#define LADSPA_PORT_AUDIO 0x8
+
+#define LADSPA_IS_PORT_INPUT(x) ((x) & LADSPA_PORT_INPUT)
+#define LADSPA_IS_PORT_OUTPUT(x) ((x) & LADSPA_PORT_OUTPUT)
+#define LADSPA_IS_PORT_CONTROL(x) ((x) & LADSPA_PORT_CONTROL)
+#define LADSPA_IS_PORT_AUDIO(x) ((x) & LADSPA_PORT_AUDIO)
+
+/*****************************************************************************/
+
+/* Plugin Port Range Hints:
+
+ The host may wish to provide a representation of data entering or
+ leaving a plugin (e.g. to generate a GUI automatically). To make
+ this more meaningful, the plugin should provide `hints' to the host
+ describing the usual values taken by the data.
+
+ Note that these are only hints. The host may ignore them and the
+ plugin must not assume that data supplied to it is meaningful. If
+ the plugin receives invalid input data it is expected to continue
+ to run without failure and, where possible, produce a sensible
+ output (e.g. a high-pass filter given a negative cutoff frequency
+ might switch to an all-pass mode).
+
+ Hints are meaningful for all input and output ports but hints for
+ input control ports are expected to be particularly useful.
+
+ More hint information is encapsulated in the
+ LADSPA_PortRangeHintDescriptor type which is assembled by ORing
+ individual hint types together. Hints may require further
+ LowerBound and UpperBound information.
+
+ All the hint information for a particular port is aggregated in the
+ LADSPA_PortRangeHint structure. */
+
+typedef int LADSPA_PortRangeHintDescriptor;
+
+/* Hint LADSPA_HINT_BOUNDED_BELOW indicates that the LowerBound field
+ of the LADSPA_PortRangeHint should be considered meaningful. The
+ value in this field should be considered the (inclusive) lower
+ bound of the valid range. If LADSPA_HINT_SAMPLE_RATE is also
+ specified then the value of LowerBound should be multiplied by the
+ sample rate. */
+#define LADSPA_HINT_BOUNDED_BELOW 0x1
+
+/* Hint LADSPA_HINT_BOUNDED_ABOVE indicates that the UpperBound field
+ of the LADSPA_PortRangeHint should be considered meaningful. The
+ value in this field should be considered the (inclusive) upper
+ bound of the valid range. If LADSPA_HINT_SAMPLE_RATE is also
+ specified then the value of UpperBound should be multiplied by the
+ sample rate. */
+#define LADSPA_HINT_BOUNDED_ABOVE 0x2
+
+/* Hint LADSPA_HINT_TOGGLED indicates that the data item should be
+ considered a Boolean toggle. Data less than or equal to zero should
+ be considered `off' or `false,' and data above zero should be
+ considered `on' or `true.' LADSPA_HINT_TOGGLED may not be used in
+ conjunction with any other hint except LADSPA_HINT_DEFAULT_0 or
+ LADSPA_HINT_DEFAULT_1. */
+#define LADSPA_HINT_TOGGLED 0x4
+
+/* Hint LADSPA_HINT_SAMPLE_RATE indicates that any bounds specified
+ should be interpreted as multiples of the sample rate. For
+ instance, a frequency range from 0Hz to the Nyquist frequency (half
+ the sample rate) could be requested by this hint in conjunction
+ with LowerBound = 0 and UpperBound = 0.5. Hosts that support bounds
+ at all must support this hint to retain meaning. */
+#define LADSPA_HINT_SAMPLE_RATE 0x8
+
+/* Hint LADSPA_HINT_LOGARITHMIC indicates that it is likely that the
+ user will find it more intuitive to view values using a logarithmic
+ scale. This is particularly useful for frequencies and gains. */
+#define LADSPA_HINT_LOGARITHMIC 0x10
+
+/* Hint LADSPA_HINT_INTEGER indicates that a user interface would
+ probably wish to provide a stepped control taking only integer
+ values. Any bounds set should be slightly wider than the actual
+ integer range required to avoid floating point rounding errors. For
+ instance, the integer set {0,1,2,3} might be described as [-0.1,
+ 3.1]. */
+#define LADSPA_HINT_INTEGER 0x20
+
+/* The various LADSPA_HINT_HAS_DEFAULT_* hints indicate a `normal'
+ value for the port that is sensible as a default. For instance,
+ this value is suitable for use as an initial value in a user
+ interface or as a value the host might assign to a control port
+ when the user has not provided one. Defaults are encoded using a
+ mask so only one default may be specified for a port. Some of the
+ hints make use of lower and upper bounds, in which case the
+ relevant bound or bounds must be available and
+ LADSPA_HINT_SAMPLE_RATE must be applied as usual. The resulting
+ default must be rounded if LADSPA_HINT_INTEGER is present. Default
+ values were introduced in LADSPA v1.1. */
+#define LADSPA_HINT_DEFAULT_MASK 0x3C0
+
+/* This default values indicates that no default is provided. */
+#define LADSPA_HINT_DEFAULT_NONE 0x0
+
+/* This default hint indicates that the suggested lower bound for the
+ port should be used. */
+#define LADSPA_HINT_DEFAULT_MINIMUM 0x40
+
+/* This default hint indicates that a low value between the suggested
+ lower and upper bounds should be chosen. For ports with
+ LADSPA_HINT_LOGARITHMIC, this should be exp(log(lower) * 0.75 +
+ log(upper) * 0.25). Otherwise, this should be (lower * 0.75 + upper
+ * 0.25). */
+#define LADSPA_HINT_DEFAULT_LOW 0x80
+
+/* This default hint indicates that a middle value between the
+ suggested lower and upper bounds should be chosen. For ports with
+ LADSPA_HINT_LOGARITHMIC, this should be exp(log(lower) * 0.5 +
+ log(upper) * 0.5). Otherwise, this should be (lower * 0.5 + upper *
+ 0.5). */
+#define LADSPA_HINT_DEFAULT_MIDDLE 0xC0
+
+/* This default hint indicates that a high value between the suggested
+ lower and upper bounds should be chosen. For ports with
+ LADSPA_HINT_LOGARITHMIC, this should be exp(log(lower) * 0.25 +
+ log(upper) * 0.75). Otherwise, this should be (lower * 0.25 + upper
+ * 0.75). */
+#define LADSPA_HINT_DEFAULT_HIGH 0x100
+
+/* This default hint indicates that the suggested upper bound for the
+ port should be used. */
+#define LADSPA_HINT_DEFAULT_MAXIMUM 0x140
+
+/* This default hint indicates that the number 0 should be used. Note
+ that this default may be used in conjunction with
+ LADSPA_HINT_TOGGLED. */
+#define LADSPA_HINT_DEFAULT_0 0x200
+
+/* This default hint indicates that the number 1 should be used. Note
+ that this default may be used in conjunction with
+ LADSPA_HINT_TOGGLED. */
+#define LADSPA_HINT_DEFAULT_1 0x240
+
+/* This default hint indicates that the number 100 should be used. */
+#define LADSPA_HINT_DEFAULT_100 0x280
+
+/* This default hint indicates that the Hz frequency of `concert A'
+ should be used. This will be 440 unless the host uses an unusual
+ tuning convention, in which case it may be within a few Hz. */
+#define LADSPA_HINT_DEFAULT_440 0x2C0
+
+#define LADSPA_IS_HINT_BOUNDED_BELOW(x) ((x) & LADSPA_HINT_BOUNDED_BELOW)
+#define LADSPA_IS_HINT_BOUNDED_ABOVE(x) ((x) & LADSPA_HINT_BOUNDED_ABOVE)
+#define LADSPA_IS_HINT_TOGGLED(x) ((x) & LADSPA_HINT_TOGGLED)
+#define LADSPA_IS_HINT_SAMPLE_RATE(x) ((x) & LADSPA_HINT_SAMPLE_RATE)
+#define LADSPA_IS_HINT_LOGARITHMIC(x) ((x) & LADSPA_HINT_LOGARITHMIC)
+#define LADSPA_IS_HINT_INTEGER(x) ((x) & LADSPA_HINT_INTEGER)
+
+#define LADSPA_IS_HINT_HAS_DEFAULT(x) ((x) & LADSPA_HINT_DEFAULT_MASK)
+#define LADSPA_IS_HINT_DEFAULT_MINIMUM(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_MINIMUM)
+#define LADSPA_IS_HINT_DEFAULT_LOW(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_LOW)
+#define LADSPA_IS_HINT_DEFAULT_MIDDLE(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_MIDDLE)
+#define LADSPA_IS_HINT_DEFAULT_HIGH(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_HIGH)
+#define LADSPA_IS_HINT_DEFAULT_MAXIMUM(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_MAXIMUM)
+#define LADSPA_IS_HINT_DEFAULT_0(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_0)
+#define LADSPA_IS_HINT_DEFAULT_1(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_1)
+#define LADSPA_IS_HINT_DEFAULT_100(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_100)
+#define LADSPA_IS_HINT_DEFAULT_440(x) (((x) & LADSPA_HINT_DEFAULT_MASK) \
+ == LADSPA_HINT_DEFAULT_440)
+
+typedef struct _LADSPA_PortRangeHint {
+
+ /* Hints about the port. */
+ LADSPA_PortRangeHintDescriptor HintDescriptor;
+
+ /* Meaningful when hint LADSPA_HINT_BOUNDED_BELOW is active. When
+ LADSPA_HINT_SAMPLE_RATE is also active then this value should be
+ multiplied by the relevant sample rate. */
+ LADSPA_Data LowerBound;
+
+ /* Meaningful when hint LADSPA_HINT_BOUNDED_ABOVE is active. When
+ LADSPA_HINT_SAMPLE_RATE is also active then this value should be
+ multiplied by the relevant sample rate. */
+ LADSPA_Data UpperBound;
+
+} LADSPA_PortRangeHint;
+
+/*****************************************************************************/
+
+/* Plugin Handles:
+
+ This plugin handle indicates a particular instance of the plugin
+ concerned. It is valid to compare this to NULL (0 for C++) but
+ otherwise the host should not attempt to interpret it. The plugin
+ may use it to reference internal instance data. */
+
+typedef void * LADSPA_Handle;
+
+/*****************************************************************************/
+
+/* Descriptor for a Type of Plugin:
+
+ This structure is used to describe a plugin type. It provides a
+ number of functions to examine the type, instantiate it, link it to
+ buffers and workspaces and to run it. */
+
+typedef struct _LADSPA_Descriptor {
+
+ /* This numeric identifier indicates the plugin type
+ uniquely. Plugin programmers may reserve ranges of IDs from a
+ central body to avoid clashes. Hosts may assume that IDs are
+ below 0x1000000. */
+ unsigned long UniqueID;
+
+ /* This identifier can be used as a unique, case-sensitive
+ identifier for the plugin type within the plugin file. Plugin
+ types should be identified by file and label rather than by index
+ or plugin name, which may be changed in new plugin
+ versions. Labels must not contain white-space characters. */
+ const char * Label;
+
+ /* This indicates a number of properties of the plugin. */
+ LADSPA_Properties Properties;
+
+ /* This member points to the null-terminated name of the plugin
+ (e.g. "Sine Oscillator"). */
+ const char * Name;
+
+ /* This member points to the null-terminated string indicating the
+ maker of the plugin. This can be an empty string but not NULL. */
+ const char * Maker;
+
+ /* This member points to the null-terminated string indicating any
+ copyright applying to the plugin. If no Copyright applies the
+ string "None" should be used. */
+ const char * Copyright;
+
+ /* This indicates the number of ports (input AND output) present on
+ the plugin. */
+ unsigned long PortCount;
+
+ /* This member indicates an array of port descriptors. Valid indices
+ vary from 0 to PortCount-1. */
+ const LADSPA_PortDescriptor * PortDescriptors;
+
+ /* This member indicates an array of null-terminated strings
+ describing ports (e.g. "Frequency (Hz)"). Valid indices vary from
+ 0 to PortCount-1. */
+ const char * const * PortNames;
+
+ /* This member indicates an array of range hints for each port (see
+ above). Valid indices vary from 0 to PortCount-1. */
+ const LADSPA_PortRangeHint * PortRangeHints;
+
+ /* This may be used by the plugin developer to pass any custom
+ implementation data into an instantiate call. It must not be used
+ or interpreted by the host. It is expected that most plugin
+ writers will not use this facility as LADSPA_Handle should be
+ used to hold instance data. */
+ void * ImplementationData;
+
+ /* This member is a function pointer that instantiates a plugin. A
+ handle is returned indicating the new plugin instance. The
+ instantiation function accepts a sample rate as a parameter. The
+ plugin descriptor from which this instantiate function was found
+ must also be passed. This function must return NULL if
+ instantiation fails.
+
+ Note that instance initialisation should generally occur in
+ activate() rather than here. */
+ LADSPA_Handle (*instantiate)(const struct _LADSPA_Descriptor * Descriptor,
+ unsigned long SampleRate);
+
+ /* This member is a function pointer that connects a port on an
+ instantiated plugin to a memory location at which a block of data
+ for the port will be read/written. The data location is expected
+ to be an array of LADSPA_Data for audio ports or a single
+ LADSPA_Data value for control ports. Memory issues will be
+ managed by the host. The plugin must read/write the data at these
+ locations every time run() or run_adding() is called and the data
+ present at the time of this connection call should not be
+ considered meaningful.
+
+ connect_port() may be called more than once for a plugin instance
+ to allow the host to change the buffers that the plugin is
+ reading or writing. These calls may be made before or after
+ activate() or deactivate() calls.
+
+ connect_port() must be called at least once for each port before
+ run() or run_adding() is called. When working with blocks of
+ LADSPA_Data the plugin should pay careful attention to the block
+ size passed to the run function as the block allocated may only
+ just be large enough to contain the block of samples.
+
+ Plugin writers should be aware that the host may elect to use the
+ same buffer for more than one port and even use the same buffer
+ for both input and output (see LADSPA_PROPERTY_INPLACE_BROKEN).
+ However, overlapped buffers or use of a single buffer for both
+ audio and control data may result in unexpected behaviour. */
+ void (*connect_port)(LADSPA_Handle Instance,
+ unsigned long Port,
+ LADSPA_Data * DataLocation);
+
+ /* This member is a function pointer that initialises a plugin
+ instance and activates it for use. This is separated from
+ instantiate() to aid real-time support and so that hosts can
+ reinitialise a plugin instance by calling deactivate() and then
+ activate(). In this case the plugin instance must reset all state
+ information dependent on the history of the plugin instance
+ except for any data locations provided by connect_port() and any
+ gain set by set_run_adding_gain(). If there is nothing for
+ activate() to do then the plugin writer may provide a NULL rather
+ than an empty function.
+
+ When present, hosts must call this function once before run() (or
+ run_adding()) is called for the first time. This call should be
+ made as close to the run() call as possible and indicates to
+ real-time plugins that they are now live. Plugins should not rely
+ on a prompt call to run() after activate(). activate() may not be
+ called again unless deactivate() is called first. Note that
+ connect_port() may be called before or after a call to
+ activate(). */
+ void (*activate)(LADSPA_Handle Instance);
+
+ /* This method is a function pointer that runs an instance of a
+ plugin for a block. Two parameters are required: the first is a
+ handle to the particular instance to be run and the second
+ indicates the block size (in samples) for which the plugin
+ instance may run.
+
+ Note that if an activate() function exists then it must be called
+ before run() or run_adding(). If deactivate() is called for a
+ plugin instance then the plugin instance may not be reused until
+ activate() has been called again.
+
+ If the plugin has the property LADSPA_PROPERTY_HARD_RT_CAPABLE
+ then there are various things that the plugin should not do
+ within the run() or run_adding() functions (see above). */
+ void (*run)(LADSPA_Handle Instance,
+ unsigned long SampleCount);
+
+ /* This method is a function pointer that runs an instance of a
+ plugin for a block. This has identical behaviour to run() except
+ in the way data is output from the plugin. When run() is used,
+ values are written directly to the memory areas associated with
+ the output ports. However when run_adding() is called, values
+ must be added to the values already present in the memory
+ areas. Furthermore, output values written must be scaled by the
+ current gain set by set_run_adding_gain() (see below) before
+ addition.
+
+ run_adding() is optional. When it is not provided by a plugin,
+ this function pointer must be set to NULL. When it is provided,
+ the function set_run_adding_gain() must be provided also. */
+ void (*run_adding)(LADSPA_Handle Instance,
+ unsigned long SampleCount);
+
+ /* This method is a function pointer that sets the output gain for
+ use when run_adding() is called (see above). If this function is
+ never called the gain is assumed to default to 1. Gain
+ information should be retained when activate() or deactivate()
+ are called.
+
+ This function should be provided by the plugin if and only if the
+ run_adding() function is provided. When it is absent this
+ function pointer must be set to NULL. */
+ void (*set_run_adding_gain)(LADSPA_Handle Instance,
+ LADSPA_Data Gain);
+
+ /* This is the counterpart to activate() (see above). If there is
+ nothing for deactivate() to do then the plugin writer may provide
+ a NULL rather than an empty function.
+
+ Hosts must deactivate all activated units after they have been
+ run() (or run_adding()) for the last time. This call should be
+ made as close to the last run() call as possible and indicates to
+ real-time plugins that they are no longer live. Plugins should
+ not rely on prompt deactivation. Note that connect_port() may be
+ called before or after a call to deactivate().
+
+ Deactivation is not similar to pausing as the plugin instance
+ will be reinitialised when activate() is called to reuse it. */
+ void (*deactivate)(LADSPA_Handle Instance);
+
+ /* Once an instance of a plugin has been finished with it can be
+ deleted using the following function. The instance handle passed
+ ceases to be valid after this call.
+
+ If activate() was called for a plugin instance then a
+ corresponding call to deactivate() must be made before cleanup()
+ is called. */
+ void (*cleanup)(LADSPA_Handle Instance);
+
+} LADSPA_Descriptor;
+
+/**********************************************************************/
+
+/* Accessing a Plugin: */
+
+/* The exact mechanism by which plugins are loaded is host-dependent,
+ however all most hosts will need to know is the name of shared
+ object file containing the plugin types. To allow multiple hosts to
+ share plugin types, hosts may wish to check for environment
+ variable LADSPA_PATH. If present, this should contain a
+ colon-separated path indicating directories that should be searched
+ (in order) when loading plugin types.
+
+ A plugin programmer must include a function called
+ "ladspa_descriptor" with the following function prototype within
+ the shared object file. This function will have C-style linkage (if
+ you are using C++ this is taken care of by the `extern "C"' clause
+ at the top of the file).
+
+ A host will find the plugin shared object file by one means or
+ another, find the ladspa_descriptor() function, call it, and
+ proceed from there.
+
+ Plugin types are accessed by index (not ID) using values from 0
+ upwards. Out of range indexes must result in this function
+ returning NULL, so the plugin count can be determined by checking
+ for the least index that results in NULL being returned. */
+
+const LADSPA_Descriptor * ladspa_descriptor(unsigned long Index);
+
+/* Datatype corresponding to the ladspa_descriptor() function. */
+typedef const LADSPA_Descriptor *
+(*LADSPA_Descriptor_Function)(unsigned long Index);
+
+/**********************************************************************/
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* LADSPA_INCLUDED */
+
+/* EOF */
diff --git a/lib/distrib.lua b/lib/distrib.lua
new file mode 100644
index 0000000..e49ff88
--- /dev/null
+++ b/lib/distrib.lua
@@ -0,0 +1,38 @@
+--
+-- melisma - extensible and scriptable audio engine
+-- Copyright (C) 2026 Ludvig Gunne Lindström
+--
+-- 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 3 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program. If not, see <https://www.gnu.org/licenses/>.
+--
+
+local M = {}
+
+function M.uniform(lo,hi)
+ return lo + math.random()*(hi - lo)
+end
+
+function M.normal(mean,dev)
+ local u,v,s,t,z0,z1
+ repeat
+ u = M.uniform(-1,1)
+ v = M.uniform(-1,1)
+ s = u^2 + v^2
+ until s > 0 and s < 1
+ t = math.sqrt(-2*math.log(s) / s)
+ z0 = mean + u*t*dev
+ z1 = mean + v*t*dev
+ return z0,z1
+end
+
+return M
diff --git a/lib/functional.lua b/lib/functional.lua
new file mode 100644
index 0000000..9f50b7e
--- /dev/null
+++ b/lib/functional.lua
@@ -0,0 +1,171 @@
+--
+-- melisma - extensible and scriptable audio engine
+-- Copyright (C) 2026 Ludvig Gunne Lindström
+--
+-- 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 3 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program. If not, see <https://www.gnu.org/licenses/>.
+--
+
+local m_util = require 'melisma.util'
+
+local M = {}
+
+function M.fmap(f,l)
+ local res = {}
+ for k,v in pairs(l) do res[k] = f(v) end
+ return res
+end
+
+function M.compose(f,g,...)
+ local tail = table.pack(...)
+ if g == nil then return f end
+ return function(x) return f(M.compose(g,table.unpack(tail))(x)) end
+end
+
+function M.pipeline(...)
+ local args = table.pack(...)
+ return function(x)
+ for _,f in ipairs(args) do x = f(x) end
+ return x
+ end
+end
+
+function M.bind(f,...)
+ local bound = table.pack(...)
+ return function(...)
+ local free = table.pack(...)
+ local all = M.concat(bound,free)
+ return f(table.unpack(all))
+ end
+end
+
+function M.bind1(f,...)
+ local bound = table.pack(...)
+ return function(x)
+ return f(x,table.unpack(bound))
+ end
+end
+
+function M.constval(v) return function(...) return v end end
+
+function M.constfunc(f) return function(...) f() end end
+
+function M.call(f,...) return f(...) end
+
+function M.replicate(f,n)
+ local res = {}
+ for i=1,n do
+ if type(f) == 'function' then
+ table.insert(res,f())
+ else
+ table.insert(res,f)
+ end
+ end
+ return res
+end
+
+function M.id(...) return ... end
+
+function M.access(...)
+ local args = table.pack(...)
+ local x = args[#args]
+ args[#args] = nil
+
+ for _,key in ipairs(args) do
+ x = x[key]
+ end
+
+ return x
+end
+
+function M.filter(pred,list)
+ local result = {}
+
+ for _,elem in ipairs(list) do
+ if pred(elem) then
+ table.insert(result,elem)
+ end
+ end
+
+ return result
+end
+
+function M.void(...) end
+
+function M.zip(f,lhs,rhs)
+ local result = {}
+ local n = math.min(#lhs,#rhs)
+
+ for i = 1,n do
+ local v = f(lhs[i],rhs[i])
+ table.insert(result,v)
+ end
+
+ return result
+end
+
+function M.tap(f)
+ return function(...)
+ f(...)
+ return ...
+ end
+end
+
+function M.fold(mempty,mappend,tbl)
+ local result = mempty
+ for i = 1,#tbl do result = mappend(result,tbl) end
+ return result
+end
+
+function M.flatten(tbl)
+ if type(tbl) ~= 'table' then return tbl end
+ return M.fold({}, M.concat, fmap(M.flatten, tbl))
+end
+
+function M.concat(lhs,rhs)
+ local result = {}
+ for i = 1,#lhs do result[i] = lhs[i] end
+ for i = 1,#rhs do result[i + #lhs] = rhs[i] end
+ return result
+end
+
+function M.find(list,pred)
+ for i = 1,#list do
+ if pred(list[i]) then return list[i],i end
+ end
+ return nil,nil
+end
+
+function M.range(f,t,s)
+ local res = {}
+ for x = f,t,s or 1 do table.insert(res,x) end
+ return res
+end
+
+function M.image(a,b,c,d,x) return (x - a)/(b - a)*(d - c) + c end
+
+function M.apply(f,...) return f(...) end
+
+function M.once(f)
+ local ret
+ local called = false
+ return function(...)
+ if not called then
+ ret = f(...)
+ called = true
+ return ret
+ end
+ end
+end
+
+return M
diff --git a/lib/graphviz.lua b/lib/graphviz.lua
new file mode 100644
index 0000000..173612d
--- /dev/null
+++ b/lib/graphviz.lua
@@ -0,0 +1,48 @@
+--
+-- melisma - extensible and scriptable audio engine
+-- Copyright (C) 2026 Ludvig Gunne Lindström
+--
+-- 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 3 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program. If not, see <https://www.gnu.org/licenses/>.
+--
+
+local module = {}
+
+function module.render(layout)
+ local lines = {}
+
+ local function append(format,...)
+ local line = string.format(format,...)
+ table.insert(lines,line)
+ end
+
+ append('digraph {')
+ append('rankdir=BT')
+ append('node [shape=rect]')
+
+ for id,node in pairs(layout) do
+ append('"%s" [label="%s"]',id,node.label or '?')
+ end
+
+ for id,node in pairs(layout) do
+ for _,input in ipairs(node.inputs) do
+ append('"%s" -> "%s"',input,id)
+ end
+ end
+
+ append('}')
+
+ return table.concat(lines,'\n')
+end
+
+return module
diff --git a/lib/ladspa_interface.lua b/lib/ladspa_interface.lua
new file mode 100644
index 0000000..794f431
--- /dev/null
+++ b/lib/ladspa_interface.lua
@@ -0,0 +1,63 @@
+--
+-- melisma - extensible and scriptable audio engine
+-- Copyright (C) 2026 Ludvig Gunne Lindström
+--
+-- 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 3 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program. If not, see <https://www.gnu.org/licenses/>.
+--
+
+local m_ladspa = require 'melisma.ladspa'
+
+local M = {}
+
+function M.init(spec,...)
+ local interface = {}
+ local info = m_ladspa.info(spec)
+ local instance = m_ladspa.load(spec,...)
+ local audio_index = 1
+ local control_index = 1
+
+ for _,port in ipairs(info.ports) do
+ if port.type == 'control' and port.direction == 'input' then
+ local control_index_copy = control_index
+
+ interface[port.name] = function(self,value)
+ if port.hints.upper_bound and value > port.hints.upper_bound then
+ value = port.hints.upper_bound
+ end
+
+ if port.hints.lower_bound and value < port.hints.lower_bound then
+ value = port.hints.lower_bound
+ end
+
+ instance:control(control_index_copy,value)
+ return self
+ end
+
+ if port.hints.default then
+ interface[port.name](nil,port.hints.default)
+ end
+
+ control_index = control_index + 1
+ end
+
+ if port.type == 'audio' and port.direction == 'output' then
+ interface[port.name] = instance(audio_index)
+ audio_index = audio_index + 1
+ end
+ end
+
+ return interface
+end
+
+return M
diff --git a/lib/lazy.lua b/lib/lazy.lua
new file mode 100644
index 0000000..144ea3a
--- /dev/null
+++ b/lib/lazy.lua
@@ -0,0 +1,24 @@
+--
+-- melisma - extensible and scriptable audio engine
+-- Copyright (C) 2026 Ludvig Gunne Lindström
+--
+-- 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 3 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program. If not, see <https://www.gnu.org/licenses/>.
+--
+
+return setmetatable({}, {
+ __index = function(tbl,key)
+ tbl[key] = require('melisma.'..key)
+ return tbl[key]
+ end
+})
diff --git a/lib/midimap.lua b/lib/midimap.lua
new file mode 100644
index 0000000..f9f7a1d
--- /dev/null
+++ b/lib/midimap.lua
@@ -0,0 +1,34 @@
+--
+-- melisma - extensible and scriptable audio engine
+-- Copyright (C) 2026 Ludvig Gunne Lindström
+--
+-- 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 3 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program. If not, see <https://www.gnu.org/licenses/>.
+--
+
+local M = {}
+
+local function optget(map,key)
+ if map ~= nil then return nil end
+ if map[key] ~= nil then key = 'default' end
+ return map[key]
+end
+
+function M.lookup(map,msg)
+ map = optget(map,msg.status)
+ map = optget(map,msg.data1)
+ map = optget(map,msg.data2)
+ return map
+end
+
+return M
diff --git a/lib/osc.lua b/lib/osc.lua
new file mode 100644
index 0000000..66f378a
--- /dev/null
+++ b/lib/osc.lua
@@ -0,0 +1,76 @@
+--
+-- melisma - extensible and scriptable audio engine
+-- Copyright (C) 2026 Ludvig Gunne Lindström
+--
+-- 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 3 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program. If not, see <https://www.gnu.org/licenses/>.
+--
+
+local M = {}
+
+function M.decode(data)
+ local addr,ttag,offset = string.unpack('!4 z Xs4 z Xs4',data)
+ if offset == nil then error('invalid osc message') end
+
+ ttag = string.sub(ttag,2)
+ data = string.sub(data,offset)
+
+ local args = {}
+ for i = 1,#ttag do
+ local format
+ local c = string.sub(ttag,i,i)
+
+ if c == 's' then format = '!4 z Xs4'
+ elseif c == 'i' then format = '>i4'
+ elseif c == 'f' then format = '>f'
+ else error('invalid osc message') end
+
+ local arg,offset = string.unpack(format,data)
+ if offset == nil then error('invalid osc message') end
+
+ table.insert(args,arg)
+ data = string.sub(data,offset)
+ end
+
+ return {addr=addr, args=args}
+end
+
+function M.encode(msg)
+ local addr = msg.addr
+ local args = msg.args
+ local data = ''
+ local ttag = ','
+
+ for _,arg in pairs(args) do
+ if type(arg) == 'string' then
+ ttag = ttag..'s'
+ data = data..string.pack('!4 z Xs4',arg)
+ elseif type(arg) == 'number' then
+ if math.type(arg) == 'integer' then
+ ttag = ttag..'i'
+ data = data..string.pack('>i4',arg)
+ elseif math.type(arg) == 'float' then
+ ttag = ttag..'f'
+ data = data..string.pack('>f',arg)
+ else
+ error('unsupported argument type '..math.type(arg))
+ end
+ else
+ error('unsupported argument type '..type(arg))
+ end
+ end
+
+ return string.pack('!4 z Xs4',addr)..string.pack('!4 z Xs4',ttag)..data
+end
+
+return M
diff --git a/lib/util.lua b/lib/util.lua
new file mode 100644
index 0000000..3ceb4cf
--- /dev/null
+++ b/lib/util.lua
@@ -0,0 +1,139 @@
+--
+-- melisma - extensible and scriptable audio engine
+-- Copyright (C) 2026 Ludvig Gunne Lindström
+--
+-- 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 3 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program. If not, see <https://www.gnu.org/licenses/>.
+--
+
+local M = {}
+
+function M.deepstring(value,visited,depth,is_key)
+ if not is_key and type(value) == 'string' then return '\'' .. value .. '\'' end
+ if type(value) ~= 'table' then return tostring(value) end
+ if next(value) == nil then return '{}' end
+
+ visited = visited or {}
+ visited[value] = true
+
+ depth = depth or 0
+ local indent = string.rep(' ',depth)
+
+ local str = {}
+ local function append(...) table.insert(str,string.format(...)) end
+
+ append('{ ')
+ for k,v in pairs(value) do
+ if visited[k] then k='{...}' else k=M.deepstring(k,visited,depth+1,true) end
+ if visited[v] then v='{...}' else v=M.deepstring(v,visited,depth+1) end
+ append(indent .. ' ' .. '%s = %s, ',k,v)
+ end
+
+ append(indent .. '}')
+ return table.concat(str,'\n')
+end
+
+function M.deepcopy(x)
+ if type(x) ~= 'table' then return x end
+ local copy = {}
+ for k,v in pairs(x) do copy[k] = M.deepcopy(v) end
+ return copy
+end
+
+function M.size(x)
+ local s = 0
+ for _,_ in pairs(x) do
+ s = s + 1
+ end
+ return s
+end
+
+function M.image(a,b,c,d,x) return (x - a)/(b - a)*(d - c) + c end
+
+function M.select(list,prompt)
+ for i,entry in ipairs(list) do
+ print(i,entry)
+ end
+
+ while true do
+ io.write(prompt .. '> ')
+ io.flush()
+
+ local input = io.read('*l')
+ local i = tonumber(input)
+
+ if i == nil then
+ print()
+ error()
+ end
+
+ if i >= 1 and i <= #list then
+ return list[i]
+ end
+ end
+end
+
+function M.splitcmd(str)
+ local words = {}
+ local word = ''
+ local state = 'skip'
+
+ for i = 1,#str do
+ local c = string.sub(str,i,i)
+
+ if state == 'skip' then
+ if c == '"' then
+ state = 'string'
+ elseif c ~= ' ' then
+ state = 'take'
+ word = word .. c
+ end
+ elseif state == 'take' then
+ if c == ' ' then
+ state = 'skip'
+ table.insert(words,word)
+ word = ''
+ elseif c == '"' then
+ state = 'string'
+ else
+ word = word .. c
+ end
+ elseif state == 'string' then
+ if c == '"' then
+ state = 'take'
+ else
+ word = word .. c
+ end
+ end
+ end
+
+ if state ~= 'skip' then
+ table.insert(words,word)
+ end
+
+ return words
+end
+
+function M.openmodule(module)
+ if module.lazy_require_getmodule ~= nil then
+ module = module.lazy_require_getmodule()
+ end
+
+ for k,v in pairs(module) do
+ if string.sub(k,1,2) ~= '__' then
+ _G[k] = v
+ end
+ end
+end
+
+return M
diff --git a/mod/biquad.c b/mod/biquad.c
new file mode 100644
index 0000000..6591d87
--- /dev/null
+++ b/mod/biquad.c
@@ -0,0 +1,202 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <math.h>
+#include <lauxlib.h>
+#include "melisma.h"
+#include "mutil.h"
+
+#define LOWPASS_TYPENAME "biquad LPF"
+#define LOWPASS_DEFAULT_CUTOFF 4000
+#define LOWPASS_DEFAULT_RESONANCE 0.5
+
+struct biquad;
+
+typedef void (*recomp_fn)(struct biquad*);
+
+struct biquad
+{
+ ml_channel *out;
+ recomp_fn recomp;
+ double x_1,x_2,y_1,y_2;
+ double xc_0,xc_1,xc_2;
+ double yc_1,yc_2;
+ double Fs;
+ double f0;
+ double Q;
+};
+
+static inline void lowpass_recomp(struct biquad *bq)
+{
+ double w0,cosw0,sinw0,alpha,b0,b1,b2,a0,a1,a2;
+ w0 = 2*M_PI*bq->f0/bq->Fs;
+ cosw0 = cos(w0);
+ sinw0 = sin(w0);
+ alpha = sinw0/(2*bq->Q);
+ b0 = (1 - cosw0)/2;
+ b1 = 1 - cosw0;
+ b2 = (1 - cosw0)/2;
+ a0 = 1 + alpha;
+ a1 = -2*cosw0;
+ a2 = 1 - alpha;
+ bq->xc_0 = b0/a0;
+ bq->xc_1 = b1/a0;
+ bq->xc_2 = b2/a0;
+ bq->yc_1 = a1/a0;
+ bq->yc_2 = a2/a0;
+}
+
+static void proc(const ml_proc_args *args)
+{
+ struct biquad bq = *(struct biquad*) args->userp;
+
+ float *x = args->in[0];
+ float *y = args->out[0];
+
+ for (size_t i = 0; i < args->nframes; i++)
+ {
+ y[i] = bq.xc_0*x[i]
+ + bq.xc_1*bq.x_1
+ + bq.xc_2*bq.x_2
+ - bq.yc_1*bq.y_1
+ - bq.yc_2*bq.y_2;
+
+ bq.x_2 = bq.x_1;
+ bq.x_1 = x[i];
+ bq.y_2 = bq.y_1;
+ bq.y_1 = y[i];
+ }
+
+ *(struct biquad*) args->userp = bq;
+}
+
+static int l_lowpass_init(lua_State *s)
+{
+ ml_channel *src = mutil_get_channel(s,1);
+
+ struct biquad *bq = calloc(1,sizeof(*bq));
+ bq->recomp = lowpass_recomp;
+ bq->Fs = ml_sample_rate();
+ bq->f0 = LOWPASS_DEFAULT_CUTOFF;
+ bq->Q = LOWPASS_DEFAULT_RESONANCE;
+ bq->recomp(bq);
+
+ ml_node *node = ml_create_node(proc,bq);
+ ml_label_node(node,"lowpass");
+
+ ml_channel *dst = ml_add_input(node);
+ ml_connect(src,dst);
+
+ bq->out = ml_add_output(node);
+
+ *(void**) lua_newuserdata(s,sizeof(void*)) = bq;
+ luaL_setmetatable(s,LOWPASS_TYPENAME);
+
+ return 1;
+}
+
+static int l_lowpass_resonance(lua_State *s)
+{
+ struct biquad *bq = *(void**) luaL_checkudata(s,1,LOWPASS_TYPENAME);
+ bq->Q = luaL_checknumber(s,2);
+ bq->recomp(bq);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = bq;
+ luaL_setmetatable(s,LOWPASS_TYPENAME);
+ return 1;
+}
+
+static int l_lowpass_cutoff(lua_State *s)
+{
+ struct biquad *bq = *(void**) luaL_checkudata(s,1,LOWPASS_TYPENAME);
+ bq->f0 = luaL_checknumber(s,2);
+ bq->recomp(bq);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = bq;
+ luaL_setmetatable(s,LOWPASS_TYPENAME);
+ return 1;
+}
+
+static int l_lowpass_out(lua_State *s)
+{
+ struct biquad *bq = *(void**) luaL_checkudata(s,1,LOWPASS_TYPENAME);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = bq->out;
+ luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME);
+ return 1;
+}
+
+int luaopen_melisma_biquad(lua_State *s)
+{
+/*** Digital biquad filters.
+@module biquad
+*/
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+/*** Lowpass filter.
+@type biquad.lowpass
+@usage
+
+ local mbiquad = require 'melisma.biquad'
+ local input = ...
+ local lowpass = mbiquad.lowpass.init(node, 2000, 0.6)
+ lowpass:resonance(1000)
+ lowpass:cutoff(0.25)
+*/
+ lua_newtable(s);
+ int lowpass_tbl = lua_gettop(s);
+
+/*** Create a lowpass filter.
+@static
+@function init
+@tparam channel input Input signal.
+@treturn biquad.lowpass The newly created lowpass filter.
+*/
+ lua_pushcfunction(s,l_lowpass_init);
+ lua_setfield(s,lowpass_tbl,"init");
+
+/*** Set filter resonance.
+@function lowpass:resonance
+@tparam number res Resonance.
+*/
+ lua_pushcfunction(s,l_lowpass_resonance);
+ lua_setfield(s,lowpass_tbl,"resonance");
+
+
+/*** Set filter cutoff frequency.
+@function lowpass:cutoff
+@tparam number cut Cutoff frequency.
+*/
+ lua_pushcfunction(s,l_lowpass_cutoff);
+ lua_setfield(s,lowpass_tbl,"cutoff");
+
+ lua_pushcfunction(s,l_lowpass_out);
+ lua_setfield(s,lowpass_tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION);
+
+ luaL_newmetatable(s,LOWPASS_TYPENAME);
+ int lowpass_mt = lua_gettop(s);
+
+ lua_pushvalue(s,lowpass_tbl);
+ lua_setfield(s,lowpass_mt,"__index");
+
+ lua_pushvalue(s,lowpass_tbl);
+ lua_setfield(s,tbl,"lowpass");
+
+ lua_settop(s,tbl);
+ return 1;
+}
diff --git a/mod/buffer.c b/mod/buffer.c
new file mode 100644
index 0000000..64ba13e
--- /dev/null
+++ b/mod/buffer.c
@@ -0,0 +1,281 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <lauxlib.h>
+#include "melisma.h"
+#include "mutil.h"
+
+#define BUFFER_TYPENAME "buffer"
+
+enum mode {CAPTURE,PLAYBACK,IDLE};
+
+struct buffer
+{
+ ml_channel *chan;
+ enum mode mode;
+ double phase;
+ double speed;
+ size_t len;
+ size_t cap;
+ float data[];
+};
+
+static size_t capture(struct buffer *b, const float *in, size_t n)
+{
+ size_t i = 0;
+
+ for (; i < n; i++)
+ {
+ b->data[b->len++] = in[i];
+
+ if (b->len == b->cap)
+ {
+ b->mode = IDLE;
+ b->phase = 0;
+ break;
+ }
+ }
+
+ return i;
+}
+
+static size_t playback(struct buffer *b, float *out, size_t n)
+{
+ size_t i = 0;
+
+ for (; i < n; i++)
+ {
+ float t = b->phase*b->len;
+ size_t i0 = (size_t) t;
+ size_t i1 = i0 + 1;
+ float ip = t - i0;
+
+ i0 %= b->len;
+ i1 %= b->len;
+
+ float s0 = b->data[i0];
+ float s1 = b->data[i1];
+
+ out[i] = s0 + ip*(s1 - s0);
+
+ b->phase += b->speed/b->len;
+
+ if (b->phase >= 1)
+ {
+ b->phase = 0;
+ b->mode = IDLE;
+ }
+ }
+
+ return i;
+}
+
+static size_t idle(struct buffer *b, float *out, size_t n)
+{
+ memset(out,0,sizeof(*out)*n);
+ return n;
+}
+
+static void proc(const ml_proc_args *args)
+{
+ struct buffer *b = args->userp;
+ const float *in = args->in[0];
+ float *out = args->out[0];
+ size_t n = args->nframes;
+
+ while (n > 0)
+ {
+ size_t d;
+
+ switch (b->mode)
+ {
+ case CAPTURE:
+ d = capture(b,in,n);
+ break;
+ case PLAYBACK:
+ d = playback(b,out,n);
+ break;
+ case IDLE:
+ d = idle(b,out,n);
+ break;
+ }
+
+ in += d;
+ out += d;
+ n -= d;
+ }
+}
+
+static int l_init(lua_State *s)
+{
+ float t = luaL_checknumber(s,1);
+ ml_channel *src = mutil_get_channel(s,2);
+
+ if (t <= 0)
+ {
+ luaL_error(s,"invalid buffer capacity %.2f\n",t);
+ }
+
+ size_t sr = ml_sample_rate();
+ size_t cap = t*sr;
+
+ struct buffer *b;
+ size_t size = sizeof(*b) + sizeof(*b->data)*cap;
+ b = calloc(1,size);
+
+ ml_node *node = ml_create_node(proc,b);
+ ml_label_node(node,"buffer");
+
+ ml_channel *dst = ml_add_input(node);
+ ml_connect(src,dst);
+
+ b->mode = IDLE;
+ b->len = 0;
+ b->phase = 0;
+ b->cap = cap;
+ b->chan = ml_add_output(node);
+
+ *(void**) lua_newuserdata(s,sizeof(void*)) = b;
+ luaL_setmetatable(s,BUFFER_TYPENAME);
+
+ return 1;
+}
+
+static int l_setmode(lua_State *s)
+{
+ int mode_i = lua_upvalueindex(1);
+ struct buffer *b = *(void**) luaL_checkudata(s,1,BUFFER_TYPENAME);
+ enum mode mode = lua_tointeger(s,mode_i);
+ b->phase = 0;
+ b->mode = mode;
+
+ if (mode == CAPTURE)
+ {
+ b->len = 0;
+ }
+
+ return 0;
+}
+
+static int l_setspeed(lua_State *s)
+{
+ struct buffer *b = *(void**) luaL_checkudata(s,1,BUFFER_TYPENAME);
+ double speed = luaL_checknumber(s,2);
+
+ if (speed <= 0)
+ {
+ luaL_error(s,"invalid speed %.2f",speed);
+ }
+
+ b->speed = speed;
+ return 0;
+}
+
+static int l_getlength(lua_State *s)
+{
+ struct buffer *b = *(void**) luaL_checkudata(s,1,BUFFER_TYPENAME);
+ size_t sr = ml_sample_rate();
+ lua_pushnumber(s,(float)b->len / (float)sr / b->speed);
+ return 1;
+}
+
+static int l_out(lua_State *s)
+{
+ struct buffer *b = *(void**) luaL_checkudata(s,1,BUFFER_TYPENAME);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = b->chan;
+ luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME);
+ return 1;
+}
+
+int luaopen_melisma_buffer(lua_State *s)
+{
+/*** Audio buffer for capture/playback.
+
+ local mbuffer = require 'melisma.buffer'
+ local mevent = require 'melisma.event'
+
+ local input = ...
+
+ -- Create a buffer with capacity of 5 seconds
+ -- Capture stops automatically when buffer is full.
+ local len = 5
+ local buffer = mbuffer.init(input, len)
+
+ -- Capture audio
+ buffer:capture()
+
+ -- Playback after capture 3 seconds
+ mevent.schedule(3, buffer.playback, buffer)
+
+@classmod buffer
+*/
+ lua_newtable(s);
+ int tbl = lua_absindex(s,lua_gettop(s));
+
+/*** Create an audio buffer.
+@static
+@function buffer.init
+@tparam channel input Input signal.
+@tparam number len Buffer capacity in seconds.
+@treturn buffer The newly created audio buffer.
+*/
+ lua_pushcfunction(s,l_init);
+ lua_setfield(s,tbl,"init");
+
+/*** Set playback speed.
+@function buffer.speed
+@tparam number speed Playback speed.
+*/
+ lua_pushcfunction(s,l_setspeed);
+ lua_setfield(s,tbl,"speed");
+
+/*** Get length of captured audio.
+@function buffer.length
+@treturn number Length of captured audio in seconds.
+*/
+ lua_pushcfunction(s,l_getlength);
+ lua_setfield(s,tbl,"length");
+
+/*** Start playback mode.
+@function buffer.playback
+*/
+ lua_pushinteger(s,PLAYBACK);
+ lua_pushcclosure(s,l_setmode,1);
+ lua_setfield(s,tbl,"playback");
+
+/*** Start capture mode.
+@function buffer.capture
+*/
+ lua_pushinteger(s,CAPTURE);
+ lua_pushcclosure(s,l_setmode,1);
+ lua_setfield(s,tbl,"capture");
+
+ lua_pushcfunction(s,l_out);
+ lua_setfield(s,tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION);
+
+ luaL_newmetatable(s,BUFFER_TYPENAME);
+ int mt = lua_absindex(s,lua_gettop(s));
+
+ lua_pushvalue(s,tbl);
+ lua_setfield(s,mt,"__index");
+
+ lua_settop(s,tbl);
+ return 1;
+}
diff --git a/mod/delay.c b/mod/delay.c
new file mode 100644
index 0000000..44441cf
--- /dev/null
+++ b/mod/delay.c
@@ -0,0 +1,233 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <lauxlib.h>
+#include "melisma.h"
+#include "mutil.h"
+
+#define DELAY_TYPENAME "delay"
+
+#define MAX_DELAY 2.00f
+#define DEFAULT_DELAY 0.25f
+#define DEFAULT_GAIN 1.00f
+#define DEFAULT_FEEDBACK 0.25f
+#define DEFAULT_WETDRYMIX 0.50f
+
+#define CLAMP(v,min,max) ((v)<(min)?(min):((v)>(max)?(max):(v)))
+
+struct delay
+{
+ ml_channel *out;
+ float feedback;
+ float gain;
+ float wetdrymix;
+ float delay;
+ size_t len;
+ size_t write;
+ float *buffer;
+};
+
+static void proc(const ml_proc_args *args)
+{
+ struct delay *dl = args->userp;
+
+ size_t sr = ml_sample_rate();
+ size_t offset = sr*dl->delay;
+
+ const float *in = args->in[0];
+ float *out = args->out[0];
+
+ size_t read;
+ float dry,wet;
+
+ for (size_t i = 0; i < args->nframes; i++)
+ {
+ read = dl->write + dl->len - offset;
+
+ if (read >= dl->len)
+ {
+ read -= dl->len;
+ }
+
+ dry = in[i];
+ wet = dl->buffer[read]*dl->gain;
+
+ out[i] = wet*dl->wetdrymix + dry*(1 - dl->wetdrymix);
+ dl->buffer[dl->write++] = dry + wet*dl->feedback;
+
+ if (dl->write == dl->len)
+ {
+ dl->write = 0;
+ }
+ }
+}
+
+static void clamp(struct delay *dl)
+{
+ dl->delay = CLAMP(dl->delay,0,MAX_DELAY);
+ dl->gain = CLAMP(dl->gain,0,1);
+ dl->feedback = CLAMP(dl->feedback,0,1);
+ dl->wetdrymix = CLAMP(dl->wetdrymix,0,1);
+}
+
+static int l_init(lua_State *s)
+{
+ ml_channel *src = mutil_get_channel(s,1);
+
+ struct delay *dl = calloc(1,sizeof(*dl));
+
+ dl->delay = DEFAULT_DELAY;
+ dl->gain = DEFAULT_GAIN;
+ dl->feedback = DEFAULT_FEEDBACK;
+ dl->wetdrymix = DEFAULT_WETDRYMIX;
+
+ size_t sr = ml_sample_rate();
+ dl->len = sr*MAX_DELAY;
+ dl->buffer = calloc(1,dl->len*sizeof(dl->buffer[0]));
+
+ clamp(dl);
+
+ ml_node *node = ml_create_node(proc,dl);
+ ml_label_node(node,"delay");
+ ml_channel *dst = ml_add_input(node);
+ ml_connect(src,dst);
+
+ dl->out = ml_add_output(node);
+
+ *(void**) lua_newuserdata(s,sizeof(void*)) = dl;
+ luaL_setmetatable(s,DELAY_TYPENAME);
+
+ return 1;
+}
+
+static int l_out(lua_State *s)
+{
+ struct delay *dl = *(void**) luaL_checkudata(s,1,DELAY_TYPENAME);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = dl->out;
+ luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME);
+ return 1;
+}
+
+static int l_delay(lua_State *s)
+{
+ struct delay *dl = *(void**) luaL_checkudata(s,1,DELAY_TYPENAME);
+ float v = luaL_checknumber(s,2);
+ dl->delay = v;
+ clamp(dl);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = dl;
+ luaL_setmetatable(s,DELAY_TYPENAME);
+ return 1;
+}
+
+static int l_gain(lua_State *s)
+{
+ struct delay *dl = *(void**) luaL_checkudata(s,1,DELAY_TYPENAME);
+ float v = luaL_checknumber(s,2);
+ dl->gain = v;
+ clamp(dl);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = dl;
+ luaL_setmetatable(s,DELAY_TYPENAME);
+ return 1;
+}
+
+static int l_feedback(lua_State *s)
+{
+ struct delay *dl = *(void**) luaL_checkudata(s,1,DELAY_TYPENAME);
+ float v = luaL_checknumber(s,2);
+ dl->feedback = v;
+ clamp(dl);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = dl;
+ luaL_setmetatable(s,DELAY_TYPENAME);
+ return 1;
+}
+
+static int l_wetdrymix(lua_State *s)
+{
+ struct delay *dl = *(void**) luaL_checkudata(s,1,DELAY_TYPENAME);
+ float v = luaL_checknumber(s,2);
+ dl->wetdrymix = v;
+ clamp(dl);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = dl;
+ luaL_setmetatable(s,DELAY_TYPENAME);
+ return 1;
+}
+
+int luaopen_melisma_delay(lua_State *s)
+{
+ ml_check_version();
+
+/*** Digital delay line.
+@classmod delay
+*/
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+/*** Create a delay line.
+@static
+@function init
+@tparam channel input Input signal.
+@treturn delay Newly created delay line.
+*/
+ lua_pushcfunction(s,l_init);
+ lua_setfield(s,tbl,"init");
+
+/*** Set delay.
+@function delay
+@tparam number d delay in seconds.
+@treturn delay
+*/
+ lua_pushcfunction(s,l_delay);
+ lua_setfield(s,tbl,"delay");
+
+/*** Set gain.
+@function gain
+@tparam number g Gain (**TODO**: Use decibels).
+@treturn delay
+*/
+ lua_pushcfunction(s,l_gain);
+ lua_setfield(s,tbl,"gain");
+
+/*** Set feedback.
+@function feedback
+@tparam number f Feedback.
+@treturn delay
+*/
+ lua_pushcfunction(s,l_feedback);
+ lua_setfield(s,tbl,"feedback");
+
+/*** Set wet/dry mix.
+@function wetdrymix
+@tparam number wd Wet/dry mix.
+@treturn delay
+*/
+ lua_pushcfunction(s,l_wetdrymix);
+ lua_setfield(s,tbl,"wetdrymix");
+
+ lua_pushcfunction(s,l_out);
+ lua_setfield(s,tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION);
+
+ luaL_newmetatable(s,DELAY_TYPENAME);
+ int mt = lua_gettop(s);
+
+ lua_pushvalue(s,tbl);
+ lua_setfield(s,mt,"__index");
+
+ lua_settop(s,tbl);
+ return 1;
+}
diff --git a/mod/expression.c b/mod/expression.c
new file mode 100644
index 0000000..1b78199
--- /dev/null
+++ b/mod/expression.c
@@ -0,0 +1,500 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+#include <assert.h>
+#include <lauxlib.h>
+#include "melisma.h"
+#include "mutil.h"
+
+#define EXPR_TYPENAME "expression"
+#define SCRIPT_TYPENAME "expression script"
+
+enum tag {ADD,SUB,MUL,DIV,IN,OUT,CONST,HALT};
+
+struct op
+{
+ enum tag tag;
+ float value;
+ size_t index;
+ size_t offset;
+ ml_channel *src;
+};
+
+struct script
+{
+ size_t len;
+ struct op ops[];
+};
+
+struct expr
+{
+ size_t num_inputs;
+ size_t *buf_lens;
+ size_t *cursors;
+ float **bufs;
+ size_t out_buf_len;
+ size_t out_cursor;
+ float *out_buf;
+ float *stack;
+ size_t stack_len;
+ size_t stack_p;
+ struct script *script;
+ ml_channel *out;
+};
+
+static size_t script_alloc_size(size_t len)
+{
+ return sizeof(struct script) + len*sizeof(struct op);
+}
+
+static struct script *push_script(lua_State *s, size_t len)
+{
+ struct script *script = lua_newuserdata(s,script_alloc_size(len));
+ luaL_setmetatable(s,SCRIPT_TYPENAME);
+ script->len = len;
+ return script;
+}
+
+static int l_const(lua_State *s)
+{
+ float value = luaL_checknumber(s,1);
+ struct script *script = push_script(s,1);
+ script->ops[0].tag = CONST;
+ script->ops[0].value = value;
+ return 1;
+}
+
+static int l_input(lua_State *s)
+{
+ ml_channel *src = mutil_get_channel(s,1);
+ size_t offset = luaL_optinteger(s,2,0);
+ struct script *script = push_script(s,1);
+ script->ops[0].tag = IN;
+ script->ops[0].src = src;
+ script->ops[0].offset = offset;
+ return 1;
+}
+
+static int l_output(lua_State *s)
+{
+ size_t offset = luaL_checkinteger(s,1);
+ struct script *script = push_script(s,1);
+ script->ops[0].tag = OUT;
+ script->ops[0].offset = offset;
+ return 1;
+}
+
+static int convert(lua_State *s, int i)
+{
+ if (luaL_testudata(s,i,SCRIPT_TYPENAME))
+ {
+ return i;
+ }
+
+ if (lua_isnumber(s,i))
+ {
+ lua_pushcfunction(s,l_const);
+ lua_pushvalue(s,i);
+ lua_call(s,1,1);
+ return lua_gettop(s);
+ }
+
+ const char *typename = lua_typename(s,lua_type(s,i));
+ luaL_error(s,"expected number or expression script, got %s",typename);
+
+ return 0;
+}
+
+static int binop(lua_State *s, int tag)
+{
+ int lhs_i = convert(s,1);
+ int rhs_i = convert(s,2);
+
+ struct script *lhs = luaL_checkudata(s,lhs_i,SCRIPT_TYPENAME);
+ struct script *rhs = luaL_checkudata(s,rhs_i,SCRIPT_TYPENAME);
+
+ size_t len = lhs->len + rhs->len + 1;
+ struct script *res = push_script(s,len);
+
+ memcpy(&res->ops[0],lhs->ops,sizeof(res->ops[0])*lhs->len);
+ memcpy(&res->ops[lhs->len],rhs->ops,sizeof(res->ops[0])*rhs->len);
+
+ res->ops[len-1].tag = tag;
+
+ return 1;
+}
+
+static int l_add(lua_State *s)
+{
+ return binop(s,ADD);
+}
+
+static int l_sub(lua_State *s)
+{
+ return binop(s,SUB);
+}
+
+static int l_mul(lua_State *s)
+{
+ return binop(s,MUL);
+}
+
+static int l_div(lua_State *s)
+{
+ return binop(s,DIV);
+}
+
+static void prepare_inputs(ml_node *node, struct script *script, size_t **buf_lens)
+{
+ for (size_t i = 0; i < script->len; i++)
+ {
+ struct op *op = &script->ops[i];
+
+ if (op->tag != IN)
+ {
+ continue;
+ }
+
+ /* the same input channel may have been encountered already */
+ bool have_prev = false;
+ for (size_t j = 0; j < i; j++)
+ {
+ struct op *prev = &script->ops[j];
+
+ if (prev->tag != IN)
+ {
+ continue;
+ }
+
+ if (op->src == prev->src)
+ {
+ op->index = prev->index;
+ have_prev = true;
+
+ if (op->offset+1 > (*buf_lens)[op->index])
+ {
+ /* grow lookback buffer length */
+ (*buf_lens)[op->index] = op->offset+1;
+ }
+
+ break;
+ }
+ }
+
+ if (have_prev)
+ {
+ continue;
+ }
+
+ op->index = ml_num_inputs(node);
+ ml_channel *dst = ml_add_input(node);
+ ml_connect(op->src,dst);
+
+ /* add buffer length */
+ *buf_lens = realloc(*buf_lens,sizeof(**buf_lens)*ml_num_inputs(node));
+ (*buf_lens)[op->index] = op->offset+1;
+ }
+}
+
+static size_t max_output_offset(struct script *script)
+{
+ size_t max = 0;
+
+ for (size_t i = 0; i < script->len; i++)
+ {
+ struct op *op = &script->ops[i];
+
+ if (op->tag != OUT)
+ {
+ continue;
+ }
+
+ if (op->offset+1 > max)
+ {
+ max = op->offset+1;
+ }
+ }
+
+ return max;
+}
+
+static void proc(const ml_proc_args *args);
+
+static int l_build(lua_State *s)
+{
+ struct script *script = luaL_checkudata(s,1,SCRIPT_TYPENAME);
+
+ /* copy script and add a halt instruction */
+ struct script *copy = calloc(1,script_alloc_size(script->len+1));
+ copy->len = script->len + 1;
+ memcpy(copy->ops,script->ops,script->len*sizeof(copy->ops[0]));
+ copy->ops[script->len].tag = HALT;
+
+ struct expr *expr = calloc(1,sizeof(*expr));
+ expr->script = copy;
+
+ /* add output channel */
+ ml_node *node = ml_create_node(proc,expr);
+ ml_label_node(node,"expression");
+ expr->out = ml_add_output(node);
+
+ /* set input ops indices and determine lookback buffer lengths */
+ prepare_inputs(node,copy,&expr->buf_lens);
+ expr->num_inputs = ml_num_inputs(node);
+
+ /* allocate lookback buffers for inputs */
+ expr->bufs = calloc(1,sizeof(*expr->bufs)*expr->num_inputs);
+ expr->cursors = calloc(1,sizeof(*expr->cursors)*expr->num_inputs);
+ for (size_t i = 0; i < expr->num_inputs; i++)
+ {
+ expr->bufs[i] = calloc(1,sizeof(*expr->bufs[i])*expr->buf_lens[i]);
+ }
+
+ /* allocate lookback buffer for output */
+ expr->out_buf_len = max_output_offset(copy);
+ expr->out_buf = calloc(1,sizeof(*expr->out_buf)*expr->out_buf_len);
+
+ *(void**) lua_newuserdata(s,sizeof(void*)) = expr;
+ luaL_setmetatable(s,EXPR_TYPENAME);
+
+ return 1;
+}
+
+static int l_out(lua_State *s)
+{
+ struct expr *e = *(void**) luaL_checkudata(s,1,EXPR_TYPENAME);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = e->out;
+ luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME);
+ return 1;
+}
+
+static void push(struct expr *expr, float v)
+{
+ if (expr->stack_p == expr->stack_len)
+ {
+ expr->stack_len = expr->stack_len == 0 ? 12 : 2*expr->stack_len;
+ expr->stack = realloc(expr->stack,sizeof(expr->stack[0])*expr->stack_len);
+ }
+
+ expr->stack[expr->stack_p++] = v;
+}
+
+static float pop(struct expr *expr)
+{
+ assert(expr->stack_p > 0);
+ return expr->stack[--expr->stack_p];
+}
+
+static void proc(const ml_proc_args *args)
+{
+ struct expr *e = args->userp;
+
+ static const void *tbl[] = {
+ [ADD] = &&op_add,
+ [SUB] = &&op_sub,
+ [MUL] = &&op_mul,
+ [DIV] = &&op_div,
+ [IN] = &&op_in,
+ [OUT] = &&op_out,
+ [CONST] = &&op_const,
+ [HALT] = &&op_halt,
+ };
+
+ float *out = args->out[0];
+
+ for (size_t i = 0; i < args->nframes; i++)
+ {
+ /* update lookback buffers */
+ for (size_t j = 0; j < e->num_inputs; j++)
+ {
+ e->bufs[j][e->cursors[j]] = args->in[j][i];
+ }
+
+ /* execute script */
+ size_t pc = 0;
+ struct op *op;
+ float rhs,lhs,res;
+ size_t l;
+
+ #define NEXT()\
+ op = &e->script->ops[pc++];\
+ goto *tbl[op->tag];
+
+ NEXT();
+
+ op_add:
+ rhs = pop(e);
+ lhs = pop(e);
+ push(e,lhs+rhs);
+ NEXT();
+
+ op_sub:
+ rhs = pop(e);
+ lhs = pop(e);
+ push(e,lhs-rhs);
+ NEXT();
+
+ op_mul:
+ rhs = pop(e);
+ lhs = pop(e);
+ push(e,lhs*rhs);
+ NEXT();
+
+ op_div:
+ rhs = pop(e);
+ lhs = pop(e);
+ push(e,lhs/rhs);
+ NEXT();
+
+ op_in:
+ l = e->cursors[op->index] + e->buf_lens[op->index] - op->offset;
+ while (l >= e->buf_lens[op->index])
+ {
+ l -= e->buf_lens[op->index];
+ }
+ push(e,e->bufs[op->index][l]);
+ NEXT();
+
+ op_out:
+ l = e->out_cursor + e->out_buf_len - op->offset;
+ while (l >= e->out_buf_len)
+ {
+ l -= e->out_buf_len;
+ }
+ push(e,e->out_buf[l]);
+ NEXT();
+
+ op_const:
+ push(e,op->value);
+ NEXT();
+
+ op_halt:
+ assert(e->stack_p == 1);
+ out[i] = pop(e);
+
+ /* update output lookback buffer */
+ if (e->out_buf_len > 0)
+ {
+ e->out_buf[e->out_cursor++] = out[i];
+ if (e->out_cursor == e->out_buf_len)
+ {
+ e->out_cursor = 0;
+ }
+ }
+
+ /* update input looback buffer cursors */
+ for (size_t j = 0; j < e->num_inputs; j++)
+ {
+ e->cursors[j]++;
+ if (e->cursors[j] == e->buf_lens[j])
+ {
+ e->cursors[j] = 0;
+ }
+ }
+ }
+}
+
+int luaopen_melisma_expression(lua_State *s)
+{
+/*** Build filters with arithmetic expressions.
+
+ local mexpression = require 'melisma.expression'
+ local input = mepxression.input
+
+ local lhs = ...
+ local rhs = ...
+
+ local script = input(lhs,0) + 0.5 * input(rhs,10)
+ local expression = script:build()
+
+@module expression
+*/
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+ lua_pushcfunction(s,l_const);
+ lua_setfield(s,tbl,"const");
+
+/*** Introduce an input signal in the expression.
+@static
+@function input
+@tparam channel sig Input signal.
+@tparam integer off Offset in no. of samples.
+@treturn script
+*/
+ lua_pushcfunction(s,l_input);
+ lua_setfield(s,tbl,"input");
+
+
+/*** Introduce the output signal in the expression.
+@static
+@function output
+@tparam integer off Offset in no. of samples.
+@treturn script
+*/
+ lua_pushcfunction(s,l_output);
+ lua_setfield(s,tbl,"output");
+
+/*** Represents an expression before compilation.
+Implements `__add`, `__sub`, `__mul` and `__div` for
+combining scripts in to larger ones.
+@type script
+*/
+/*** Compile an expression script.
+@function script:build
+@treturn expression
+*/
+ lua_pushcfunction(s,l_build);
+ lua_setfield(s,tbl,"build");
+
+ luaL_newmetatable(s,SCRIPT_TYPENAME);
+ int script_mt = lua_gettop(s);
+
+ lua_pushvalue(s,tbl);
+ lua_setfield(s,script_mt,"__index");
+
+ lua_pushcfunction(s,l_add);
+ lua_setfield(s,script_mt,"__add");
+
+ lua_pushcfunction(s,l_sub);
+ lua_setfield(s,script_mt,"__sub");
+
+ lua_pushcfunction(s,l_mul);
+ lua_setfield(s,script_mt,"__mul");
+
+ lua_pushcfunction(s,l_div);
+ lua_setfield(s,script_mt,"__div");
+
+ lua_newtable(s);
+ int expr_tbl = lua_gettop(s);
+
+ luaL_newmetatable(s,EXPR_TYPENAME);
+ int expr_mt = lua_gettop(s);
+
+ lua_pushvalue(s,expr_tbl);
+ lua_setfield(s,expr_mt,"__index");
+
+ lua_pushcfunction(s,l_out);
+ lua_setfield(s,expr_tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION);
+
+ lua_settop(s,tbl);
+ return 1;
+}
diff --git a/mod/fs.c b/mod/fs.c
new file mode 100644
index 0000000..e08dfda
--- /dev/null
+++ b/mod/fs.c
@@ -0,0 +1,103 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+#include <errno.h>
+#include <dirent.h>
+#include <lauxlib.h>
+
+static const char *dirent_typestr(struct dirent *de)
+{
+ switch (de->d_type)
+ {
+ case DT_BLK: return "b";
+ case DT_CHR: return "c";
+ case DT_DIR: return "d";
+ case DT_FIFO: return "p";
+ case DT_REG: return "f";
+ case DT_LNK: return "l";
+ case DT_SOCK: return "s";
+ default: return "?";
+ }
+}
+
+static void push_dirent(lua_State *s, struct dirent *de)
+{
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+ lua_pushstring(s,de->d_name);
+ lua_setfield(s,tbl,"name");
+
+ lua_pushstring(s,dirent_typestr(de));
+ lua_setfield(s,tbl,"type");
+
+ lua_settop(s,tbl);
+}
+
+static int l_readdir(lua_State *s)
+{
+ const char *path = luaL_checkstring(s,1);
+ DIR *dirp = opendir(path);
+
+ if (dirp == NULL)
+ {
+ luaL_error(s,strerror(errno));
+ }
+
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+ int i = 1;
+ struct dirent *de;
+ while ((de = readdir(dirp)) != NULL)
+ {
+ push_dirent(s,de);
+ lua_rawseti(s,tbl,i++);
+ }
+
+ closedir(dirp);
+ lua_settop(s,tbl);
+ return 1;
+}
+
+int luaopen_melisma_fs(lua_State *s)
+{
+/*** File system utilities.
+@module fs
+*/
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+/*** List directory contents.
+@function readdir
+@tparam string path Directory path.
+@treturn table Directory contents.
+@usage
+local fs = require 'melisma.fs'
+local contents = fs.readdir('.')
+for _,entry in ipairs(contents) do
+ print(entry.name, entry.type)
+end
+*/
+ lua_pushcfunction(s,l_readdir);
+ lua_setfield(s,tbl,"readdir");
+
+ lua_settop(s,tbl);
+ return 1;
+}
diff --git a/mod/gain.c b/mod/gain.c
new file mode 100644
index 0000000..994efcf
--- /dev/null
+++ b/mod/gain.c
@@ -0,0 +1,164 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+#include <stdlib.h>
+#include <math.h>
+
+#include <lauxlib.h>
+
+#include "melisma.h"
+#include "mutil.h"
+
+#define GAIN_TYPENAME "gain"
+
+struct gain
+{
+ ml_node *node;
+ float value;
+ int mapped;
+ int db;
+};
+
+static void proc(const ml_proc_args *args)
+{
+ struct gain *gain = args->userp;
+
+ float *in = args->in[0];
+ float *out = args->out[0];
+ float *mapped = gain->mapped ? args->in[1] : NULL;
+ float v;
+
+ for (size_t i = 0; i < args->nframes; i++)
+ {
+ v = mapped ? mapped[i] : gain->value;
+
+ if (gain->db)
+ {
+ v = powf(10,v/20.f);
+ }
+
+ out[i] = v*in[i];
+ }
+}
+
+static int l_init(lua_State *s)
+{
+ int db_i = lua_upvalueindex(1);
+ int db = lua_tointeger(s,db_i);
+ ml_channel *audio_src, *mapped_src = NULL;
+ ml_channel *audio_dst, *mapped_dst;
+ float value = 0;
+
+
+ if (lua_isuserdata(s,1))
+ {
+ mapped_src = mutil_get_channel(s,1);
+ }
+ else
+ {
+ value = luaL_checknumber(s,1);
+ }
+
+ audio_src = mutil_get_channel(s,2);
+
+ struct gain *gain = calloc(1,sizeof(*gain));
+ gain->value = value;
+ gain->mapped = mapped_src != NULL;
+ gain->db = db;
+
+ ml_node *node = ml_create_node(proc,gain);
+ ml_label_node(node,"gain");
+ gain->node = node;
+
+ ml_add_output(node);
+
+ audio_dst = ml_add_input(node);
+ ml_connect(audio_src,audio_dst);
+
+ if (mapped_src)
+ {
+ mapped_dst = ml_add_input(node);
+ ml_connect(mapped_src,mapped_dst);
+ }
+
+ *(void**) lua_newuserdata(s,sizeof(void*)) = gain;
+ luaL_setmetatable(s,GAIN_TYPENAME);
+
+ return 1;
+}
+
+static int l_out(lua_State *s)
+{
+ struct gain *gain = *(void**) luaL_checkudata(s,1,GAIN_TYPENAME);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = ml_get_output(gain->node,0);
+ luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME);
+ return 1;
+}
+
+int luaopen_melisma_gain(lua_State *s)
+{
+ ml_check_version();
+
+/*** Apply gain to signals.
+ local mgain = require 'melisma.gain'
+
+ -- Interprets `cntl` as decibels and applies it to `input`
+ local input = ...
+ local cntl = ...
+ local gain = mgain.db(input,cntl)
+
+@classmod gain
+*/
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+/*** Creates a linear gain unit.
+@static
+@function linear
+@tparam channel input Input signal.
+@tparam number|channel gain Constant gain or a controlling signal.
+@treturn gain The newly created gain unit.
+*/
+ lua_pushinteger(s,0);
+ lua_pushcclosure(s,l_init,1);
+ lua_setfield(s,tbl,"linear");
+
+/*** Creates a decibel gain unit.
+@static
+@function db
+@tparam channel input Input signal.
+@tparam number|channel gain Constant gain in decibels or a controlling signal.
+@treturn gain The newly created gain unit.
+*/
+ lua_pushinteger(s,1);
+ lua_pushcclosure(s,l_init,1);
+ lua_setfield(s,tbl,"db");
+
+ lua_pushcfunction(s,l_out);
+ lua_setfield(s,tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION);
+
+ luaL_newmetatable(s,GAIN_TYPENAME);
+ int mt = lua_gettop(s);
+
+ lua_pushvalue(s,tbl);
+ lua_setfield(s,mt,"__index");
+
+ lua_settop(s,tbl);
+ return 1;
+}
diff --git a/mod/interp.c b/mod/interp.c
new file mode 100644
index 0000000..c5bbefb
--- /dev/null
+++ b/mod/interp.c
@@ -0,0 +1,257 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <lauxlib.h>
+#include "melisma.h"
+#include "mutil.h"
+
+#define INTERP_TYPENAME "interp"
+#define DEFAULT_METHOD "linear"
+
+struct interp;
+typedef float (*smp_fn)(float);
+
+enum {
+ LINEAR,
+ EASEIN,
+ EASEOUT,
+ SMOOTHSTEP,
+};
+
+struct interp
+{
+ ml_channel *out;
+ smp_fn smp;
+ float src;
+ float dst;
+ size_t t0;
+ size_t t;
+ int method;
+};
+
+static float sample(struct interp *ip)
+{
+ float x = ip->t0 == 0 ? 0 : (float)ip->t / (float)ip->t0;
+ switch (ip->method)
+ {
+ case LINEAR:
+ break;
+ case EASEIN:
+ x = x*x;
+ break;
+ case EASEOUT:
+ x = 1 -x;
+ x = 1 - x*x;
+ break;
+ case SMOOTHSTEP:
+ x = x*x*(3 - 2*x);
+ break;
+ }
+ return ip->src + x*(ip->dst - ip->src);
+}
+
+static void proc(const ml_proc_args *args)
+{
+ struct interp *ip = args->userp;
+
+ float *out = args->out[0];
+
+ for (size_t i = 0; i < args->nframes; i++)
+ {
+ out[i] = sample(ip);
+
+ if (ip->t == ip->t0)
+ {
+ ip->t = 0;
+ ip->t0 = 0;
+ ip->src = ip->dst;
+ }
+ else
+ {
+ ip->t++;
+ }
+ }
+}
+
+static int l_init(lua_State *s)
+{
+ float v = luaL_checknumber(s,1);
+
+ struct interp *ip = calloc(1,sizeof(*ip));
+ ip->src = v;
+ ip->dst = v;
+ ip->method = LINEAR;
+
+ ml_node *node = ml_create_node(proc,ip);
+ ml_label_node(node,"interp");
+ ip->out = ml_add_output(node);
+
+ *(void**) lua_newuserdata(s,sizeof(void*)) = ip;
+ luaL_setmetatable(s,INTERP_TYPENAME);
+
+ return 1;
+}
+
+static int l_set(lua_State *s)
+{
+ struct interp *ip = *(void**) luaL_checkudata(s,1,INTERP_TYPENAME);
+ float v = luaL_checknumber(s,2);
+
+ ip->src = v;
+ ip->dst = v;
+ ip->t0 = 0;
+ ip->t = 0;
+
+ return 0;
+}
+
+static int l_target(lua_State *s)
+{
+ int method_i = lua_upvalueindex(1);
+ int method = luaL_checkinteger(s,method_i);
+ struct interp *ip = *(void**) luaL_checkudata(s,1,INTERP_TYPENAME);
+ float v = luaL_checknumber(s,2);
+ float t = luaL_checknumber(s,3);
+
+ size_t sr = ml_sample_rate();
+
+ ip->src = sample(ip);
+ ip->dst = v;
+ ip->t0 = sr*t;
+ ip->t = 0;
+ ip->method = method;
+
+ return 0;
+}
+
+static int l_sample(lua_State *s)
+{
+ struct interp *ip = *(void**) luaL_checkudata(s,1,INTERP_TYPENAME);
+ lua_pushnumber(s,sample(ip));
+ return 1;
+}
+
+static int l_out(lua_State *s)
+{
+ struct interp *ip = *(void**) luaL_checkudata(s,1,INTERP_TYPENAME);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = ip->out;
+ luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME);
+ return 1;
+}
+
+int luaopen_melisma_interp(lua_State *s)
+{
+ ml_check_version();
+
+/*** Interpolation.
+
+ local minterp = require 'melisma.interp'
+ local mevent = require 'melisma.event'
+ local mgain = require 'melisma.gain'
+
+ local interp = minterp.init(0)
+
+ interp:linear(1,1)
+
+ mevent.schedule(0.5, function()
+ print('current value:', interp:sample())
+ end)
+
+ local sig = ...
+ local gain = mgain.linear(sig,interp)
+
+@classmod interp
+*/
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+/*** Initialize an interpolation unit.
+@static
+@function init
+@tparam number x0 Initial value.
+@treturn interp The newly created interpolation unit.
+*/
+ lua_pushcfunction(s,l_init);
+ lua_setfield(s,tbl,"init");
+
+/*** Set value directly.
+@function set
+@tparam number x New value.
+*/
+ lua_pushcfunction(s,l_set);
+ lua_setfield(s,tbl,"set");
+
+/*** Interpolate using linear interpolation.
+@function linear
+@tparam number v Target value.
+@tparam number t Interpolation time.
+*/
+ lua_pushnumber(s,LINEAR);
+ lua_pushcclosure(s,l_target,1);
+ lua_setfield(s,tbl,"linear");
+
+
+/*** Interpolate using ease-in interpolation.
+@function ease_in
+@tparam number v Target value.
+@tparam number t Interpolation time.
+*/
+ lua_pushnumber(s,EASEIN);
+ lua_pushcclosure(s,l_target,1);
+ lua_setfield(s,tbl,"ease_in");
+
+
+/*** Interpolate using ease-out interpolation.
+@function ease_out
+@tparam number v Target value.
+@tparam number t Interpolation time.
+*/
+ lua_pushnumber(s,EASEOUT);
+ lua_pushcclosure(s,l_target,1);
+ lua_setfield(s,tbl,"ease_out");
+
+/*** Interpolate using smoothstep interpolation.
+@function smoothstep
+@tparam number v Target value.
+@tparam number t Interpolation time.
+*/
+ lua_pushnumber(s,SMOOTHSTEP);
+ lua_pushcclosure(s,l_target,1);
+ lua_setfield(s,tbl,"smoothstep");
+
+/*** Get the current value.
+@function sample
+@treturn number The current value.
+*/
+ lua_pushcfunction(s,l_sample);
+ lua_setfield(s,tbl,"sample");
+
+ lua_pushcfunction(s,l_out);
+ lua_setfield(s,tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION);
+
+ luaL_newmetatable(s,INTERP_TYPENAME);
+ int mt = lua_gettop(s);
+
+ lua_pushvalue(s,tbl);
+ lua_setfield(s,mt,"__index");
+
+ lua_settop(s,tbl);
+ return 1;
+}
diff --git a/mod/keycallback.c b/mod/keycallback.c
new file mode 100644
index 0000000..2352cbf
--- /dev/null
+++ b/mod/keycallback.c
@@ -0,0 +1,147 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <termios.h>
+#include <unistd.h>
+#include <lauxlib.h>
+#include "melisma.h"
+#include "melisma/ring.h"
+
+static int fn = LUA_NOREF;
+static struct termios tty = {0};
+static struct termios oldtty = {0};
+static lua_State *state;
+static ml_ring *ring = NULL;
+
+static void make_arg(lua_State *s, size_t len, const void *data)
+{
+ lua_pushlstring(s,data,len);
+}
+
+static void io_handler(int fd, int flags, void *args)
+{
+ char c;
+
+ if (read(fd,&c,sizeof c) <= 0)
+ {
+ return;
+ }
+
+ ml_ring_put_blocking(ring,*(uint8_t*) &c);
+}
+
+static void cleanup(void)
+{
+ tcsetattr(STDIN_FILENO,TCSANOW,&oldtty);
+}
+
+static void proc(const ml_proc_args *args)
+{
+ if (fn == LUA_NOREF)
+ {
+ return;
+ }
+
+ char buf[1];
+ while (ml_ring_read(ring,buf,sizeof buf) == sizeof buf)
+ {
+ lua_pushlstring(state,buf,sizeof buf);
+ int arg = luaL_ref(state,LUA_REGISTRYINDEX);
+ ml_schedule_event(0,fn,arg);
+ }
+}
+
+static void setup(lua_State *s)
+{
+ static bool init = false;
+
+ if (init)
+ {
+ return;
+ }
+
+ init = true;
+
+ tcgetattr(STDIN_FILENO,&oldtty);
+ tty = oldtty;
+ tty.c_lflag &= ~ECHO;
+ tty.c_lflag &= ~ICANON;
+ tcsetattr(STDIN_FILENO,TCSANOW,&tty);
+ tcflush(STDIN_FILENO,TCIFLUSH);
+
+ ml_add_io_handler(STDIN_FILENO,POLLIN,io_handler,NULL);
+ ring = ml_ring_init(4096);
+ state = s;
+
+ ml_node *node = ml_create_node(proc,NULL);
+ ml_channel *src = ml_add_output(node);
+ ml_channel *dst = ml_add_input(ml_get_root_node());
+ ml_connect(src,dst);
+ ml_label_node(node,"keycallback");
+
+ atexit(cleanup);
+}
+
+static int l_set(lua_State *s)
+{
+ if (fn != LUA_NOREF)
+ {
+ luaL_unref(s,LUA_REGISTRYINDEX,fn);
+ fn = LUA_NOREF;
+ }
+
+ if (lua_isnil(s,1))
+ {
+ return 0;
+ }
+
+ luaL_checktype(s,1,LUA_TFUNCTION);
+ lua_pushvalue(s,1);
+ fn = luaL_ref(s,LUA_REGISTRYINDEX);
+
+ setup(s);
+
+ return 0;
+}
+
+int luaopen_melisma_keycallback(lua_State *s)
+{
+/*** Set callback for keyboard input.
+ local mkeycallback = require 'melisma.keycallback'
+
+ mkeycallback.set(function(c)
+ print('you pressed', c)
+ end)
+@module keycallback
+*/
+ lua_newtable(s);
+ int tbl = lua_absindex(s,lua_gettop(s));
+
+/*** Set the callback.
+@function set
+@tparam function fn Callback function.
+*/
+ lua_pushcfunction(s,l_set);
+ lua_setfield(s,tbl,"set");
+
+ lua_settop(s,tbl);
+ return 1;
+}
diff --git a/mod/ladspa.c b/mod/ladspa.c
new file mode 100644
index 0000000..4b681b9
--- /dev/null
+++ b/mod/ladspa.c
@@ -0,0 +1,604 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+#include <dlfcn.h>
+#include <sys/stat.h>
+#include <lauxlib.h>
+#include "melisma.h"
+#include "mutil.h"
+#include "../external/ladspa.h"
+
+#define LADSPA_TYPENAME "ladspa plugin"
+
+struct cache
+{
+ const char *key;
+ void *ptr;
+ struct cache *next;
+};
+
+struct ladspa
+{
+ ml_node *node;
+ const LADSPA_Descriptor *dsc;
+ LADSPA_Handle *hdl;
+ float *cntl;
+};
+
+static struct cache *lib_cache = NULL;
+static struct cache *dsc_cache = NULL;
+
+static void *lookup(struct cache *c, const char *k)
+{
+ while (c)
+ {
+ if (strcmp(c->key,k) == 0)
+ {
+ return c->ptr;
+ }
+ c = c->next;
+ }
+ return NULL;
+}
+
+static void cache(struct cache **c, const char *k, void *p)
+{
+ struct cache *head = calloc(1,sizeof(*head));
+ head->key = strdup(k);
+ head->ptr = p;
+ head->next = *c;
+ *c = head;
+}
+
+static int exists(const char *path)
+{
+ struct stat st;
+ return stat(path,&st) == 0;
+}
+
+static const char *find_lib(const char *path)
+{
+ if (exists(path))
+ {
+ return path;
+ }
+
+ if (strchr(path,'/') != NULL)
+ {
+ return NULL;
+ }
+
+ char *lp = getenv("LADSPA_PATH");
+
+ if (lp == NULL)
+ {
+ return NULL;
+ }
+
+ lp = strdup(lp);
+ char *ptr = lp;
+ char *dir;
+
+ while ((dir = strtok(ptr,":")) != NULL)
+ {
+ ptr = NULL;
+
+ static char buf[PATH_MAX];
+ snprintf(buf,sizeof buf,"%s/%s",dir,path);
+
+ if (exists(buf))
+ {
+ free(lp);
+ return buf;
+ }
+ }
+
+ free(lp);
+
+ return NULL;
+}
+
+static void *get_lib(lua_State *s, const char *path)
+{
+ void *lib;
+
+ if ((lib = lookup(lib_cache,path)) != NULL)
+ {
+ return lib;
+ }
+
+ const char *found = find_lib(path);
+
+ if (found == NULL)
+ {
+ luaL_error(s,"failed to find library %s",path);
+ }
+
+ lib = dlopen(found,RTLD_NOW);
+
+ if (lib == NULL)
+ {
+ luaL_error(s,"failed to load library %s: %s",dlerror());
+ }
+
+ cache(&lib_cache,path,lib);
+
+ return lib;
+}
+
+static void parse_spec(lua_State *s, const char *spec, char *path, char *name)
+{
+ char buf[128];
+ strcpy(buf,spec);
+
+ const char *f;
+
+ if ((f = strtok(buf,":")) == NULL)
+ {
+ goto invalid;
+ }
+
+ strcpy(path,f);
+
+ if ((f = strtok(NULL,":")) == NULL)
+ {
+ goto invalid;
+ }
+
+ strcpy(name,f);
+
+ return;
+
+invalid:
+ luaL_error(s,"invalid ladspa spec: %s",spec);
+}
+
+static const LADSPA_Descriptor *get_descriptor(lua_State *s, const char *spec)
+{
+ const LADSPA_Descriptor *dsc;
+ LADSPA_Descriptor_Function ldf;
+ char path[128];
+ char name[128];
+ void *lib;
+ int i = 0;
+
+ parse_spec(s,spec,path,name);
+
+ if ((dsc = lookup(dsc_cache,name)) != NULL)
+ {
+ return dsc;
+ }
+
+ lib = get_lib(s,path);
+ ldf = dlsym(lib,"ladspa_descriptor");
+
+ if (ldf == NULL)
+ {
+ luaL_error(s,"failed to load ladspa_descriptor from %s: %s",path,dlerror());
+ }
+
+ while ((dsc = ldf(i++)) != NULL)
+ {
+ if (strcmp(dsc->Name,name) == 0)
+ {
+ break;
+ }
+
+ if (strcmp(dsc->Label,name) == 0)
+ {
+ break;
+ }
+ }
+
+ if (dsc == NULL)
+ {
+ luaL_error(s,"ladspa plugin library %s does not define plugin %s",path,name);
+ }
+
+ cache(&dsc_cache,spec,(void*)dsc);
+
+ return dsc;
+}
+
+static void instantiate(struct ladspa *l, const ml_proc_args *args)
+{
+ unsigned long sr = ml_sample_rate();
+
+ l->hdl = l->dsc->instantiate(l->dsc,sr);
+
+ if (l->dsc->activate)
+ {
+ l->dsc->activate(l->hdl);
+ }
+
+ float *ptr;
+ size_t ii = 0, oi = 0, ci = 0;
+
+ for (size_t i = 0; i < l->dsc->PortCount; i++)
+ {
+ LADSPA_PortDescriptor pd = l->dsc->PortDescriptors[i];
+
+ if (LADSPA_IS_PORT_CONTROL(pd) && LADSPA_IS_PORT_INPUT(pd))
+ {
+ l->dsc->connect_port(l->hdl,i,&l->cntl[ci++]);
+ continue;
+ }
+
+ if (LADSPA_IS_PORT_AUDIO(pd) && LADSPA_IS_PORT_INPUT(pd))
+ {
+ ptr = args->in[ii++];
+ l->dsc->connect_port(l->hdl,i,ptr);
+ continue;
+ }
+
+ if (LADSPA_IS_PORT_AUDIO(pd) && LADSPA_IS_PORT_OUTPUT(pd))
+ {
+ ptr = args->out[oi++];
+ l->dsc->connect_port(l->hdl,i,ptr);
+ continue;
+ }
+ }
+}
+
+static void proc(const ml_proc_args *args)
+{
+ struct ladspa *l = args->userp;
+
+ if (l->hdl == NULL)
+ {
+ instantiate(l,args);
+ }
+
+ l->dsc->run(l->hdl,args->nframes);
+}
+
+static int l_load(lua_State *s)
+{
+ const char *spec = luaL_checkstring(s,1);
+ const LADSPA_Descriptor *dsc = get_descriptor(s,spec);
+
+ struct ladspa *l = calloc(1,sizeof(*l));
+ l->dsc = dsc;
+ l->node = ml_create_node(proc,l);
+ ml_label_node(l->node,"LADSPA (%s)",spec);
+
+ size_t num_cntl = 0, arg = 2;
+ for (size_t i = 0; i < dsc->PortCount; i++)
+ {
+ LADSPA_PortDescriptor pd = dsc->PortDescriptors[i];
+
+ if (LADSPA_IS_PORT_CONTROL(pd) && LADSPA_IS_PORT_INPUT(pd))
+ {
+ num_cntl++;
+ }
+
+ if (LADSPA_IS_PORT_AUDIO(pd) && LADSPA_IS_PORT_INPUT(pd))
+ {
+ ml_channel *src = mutil_get_channel(s,arg++);
+ ml_channel *dst = ml_add_input(l->node);
+ ml_connect(src,dst);
+ }
+
+ if (LADSPA_IS_PORT_AUDIO(pd) && LADSPA_IS_PORT_OUTPUT(pd))
+ {
+ ml_add_output(l->node);
+ }
+ }
+
+ l->cntl = calloc(1,sizeof(*l->cntl)*num_cntl);
+
+ *(void**) lua_newuserdata(s,sizeof(void*)) = l;
+ luaL_setmetatable(s,LADSPA_TYPENAME);
+
+ return 1;
+}
+
+static int l_control(lua_State *s)
+{
+ struct ladspa *l = *(void**) luaL_checkudata(s,1,LADSPA_TYPENAME);
+ int i = luaL_checkinteger(s,2);
+ float v = luaL_checknumber(s,3);
+
+ if (i < 1 || i > l->dsc->PortCount)
+ {
+ luaL_error(s,"control port index out of bounds: %d",i);
+ }
+
+ if (!LADSPA_IS_PORT_CONTROL(l->dsc->PortDescriptors[i-1]))
+ {
+ luaL_error(s,"port %d is not a control port",i);
+ }
+
+ if (!LADSPA_IS_PORT_INPUT(l->dsc->PortDescriptors[i-1]))
+ {
+ luaL_error(s,"port %d is not an input port",i);
+ }
+
+ l->cntl[i-1] = v;
+ return 0;
+}
+
+static int l_info(lua_State *s)
+{
+ const char *spec = luaL_checkstring(s,1);
+ const LADSPA_Descriptor *dsc = get_descriptor(s,spec);
+
+ lua_newtable(s);
+ int info = lua_gettop(s);
+
+ lua_pushstring(s,dsc->Name);
+ lua_setfield(s,info,"name");
+
+ lua_pushstring(s,dsc->Label);
+ lua_setfield(s,info,"label");
+
+ lua_pushstring(s,dsc->Maker);
+ lua_setfield(s,info,"maker");
+
+ lua_pushstring(s,dsc->Copyright);
+ lua_setfield(s,info,"copyright");
+
+ lua_pushinteger(s,dsc->UniqueID);
+ lua_setfield(s,info,"id");
+
+ lua_newtable(s);
+ int ports = lua_gettop(s);
+
+ for (int i = 0; i < dsc->PortCount; i++)
+ {
+ const char *name = dsc->PortNames[i];
+ const LADSPA_PortDescriptor pd = dsc->PortDescriptors[i];
+ const LADSPA_PortRangeHint *prh = &dsc->PortRangeHints[i];
+ const LADSPA_PortRangeHintDescriptor prhd = prh->HintDescriptor;
+
+ const char *dir = LADSPA_IS_PORT_INPUT(pd) ? "input" : "output";
+ const char *type = LADSPA_IS_PORT_AUDIO(pd) ? "audio" : "control";
+ float lower = prh->LowerBound;
+ float upper = prh->UpperBound;
+ bool bounded_below = LADSPA_IS_HINT_BOUNDED_BELOW(prhd);
+ bool bounded_above = LADSPA_IS_HINT_BOUNDED_ABOVE(prhd);
+ bool toggled = LADSPA_IS_HINT_TOGGLED(prhd);
+ bool sample_rate = LADSPA_IS_HINT_SAMPLE_RATE(prhd);
+ bool logarithmic = LADSPA_IS_HINT_LOGARITHMIC(prhd);
+ bool integer = LADSPA_IS_HINT_INTEGER(prhd);
+ bool has_default = LADSPA_IS_HINT_HAS_DEFAULT(prhd);
+ bool default_minimum = LADSPA_IS_HINT_DEFAULT_MINIMUM(prhd);
+ bool default_low = LADSPA_IS_HINT_DEFAULT_LOW(prhd);
+ bool default_middle = LADSPA_IS_HINT_DEFAULT_MIDDLE(prhd);
+ bool default_high = LADSPA_IS_HINT_DEFAULT_HIGH(prhd);
+ bool default_maximum = LADSPA_IS_HINT_DEFAULT_MAXIMUM(prhd);
+ bool default_0 = LADSPA_IS_HINT_DEFAULT_0(prhd);
+ bool default_1 = LADSPA_IS_HINT_DEFAULT_1(prhd);
+ bool default_100 = LADSPA_IS_HINT_DEFAULT_100(prhd);
+ bool default_440 = LADSPA_IS_HINT_DEFAULT_440(prhd);
+
+ lua_newtable(s);
+ int port = lua_gettop(s);
+
+ lua_pushstring(s,name);
+ lua_setfield(s,port,"name");
+
+ lua_pushstring(s,dir);
+ lua_setfield(s,port,"direction");
+
+ lua_pushstring(s,type);
+ lua_setfield(s,port,"type");
+
+ lua_newtable(s);
+ int hints = lua_gettop(s);
+
+ // if (bounded_below) { lua_pushboolean(s,true); lua_setfield(s,hints,"bounded_below"); }
+ // if (bounded_above) { lua_pushboolean(s,true); lua_setfield(s,hints,"bounded_above"); }
+ if (toggled) { lua_pushboolean(s,true); lua_setfield(s,hints,"toggled"); }
+ if (sample_rate) { lua_pushboolean(s,true); lua_setfield(s,hints,"sample_rate"); }
+ if (logarithmic) { lua_pushboolean(s,true); lua_setfield(s,hints,"logarithmic"); }
+ if (integer) { lua_pushboolean(s,true); lua_setfield(s,hints,"integer"); }
+ if (bounded_below) { lua_pushnumber(s,lower); lua_setfield(s,hints,"lower_bound"); }
+ if (bounded_above) { lua_pushnumber(s,upper); lua_setfield(s,hints,"upper_bound"); }
+
+ if (has_default)
+ {
+ float def = 0;
+
+ if (default_minimum) def = lower;
+ else if (default_low) def = 0.25*lower + 0.75*upper;
+ else if (default_middle) def = 0.5*lower + 0.5*upper;
+ else if (default_high) def = 0.75*lower + 0.25*upper;
+ else if (default_maximum) def = upper;
+ else if (default_0) def = 0;
+ else if (default_1) def = 1;
+ else if (default_100) def = 100;
+ else if (default_440) def = 440;
+
+ lua_pushnumber(s,def);
+ lua_setfield(s,hints,"default");
+ }
+
+ lua_pushvalue(s,hints);
+ lua_setfield(s,port,"hints");
+
+ lua_pushvalue(s,port);
+ lua_rawseti(s,ports,i + 1);
+ lua_settop(s,ports);
+ }
+
+ lua_pushvalue(s,ports);
+ lua_setfield(s,info,"ports");
+
+ lua_settop(s,info);
+
+ return 1;
+}
+
+static int l_lib_info(lua_State *s)
+{
+ const char *path;
+ void *lib;
+ LADSPA_Descriptor_Function ldf;
+ const LADSPA_Descriptor *dsc;
+
+ path = luaL_checkstring(s,1);
+ lib= get_lib(s,path);
+ ldf = dlsym(lib,"ladspa_descriptor");
+
+ if (ldf == NULL)
+ {
+ luaL_error(s,"failed to load ladspa_descriptor from %s: %s",path,dlerror());
+ }
+
+ lua_newtable(s);
+ int info = lua_gettop(s);
+
+ int i = 0;
+ while ((dsc = ldf(i++)) != NULL)
+ {
+ lua_newtable(s);
+ int plugin = lua_gettop(s);
+
+ lua_pushstring(s,dsc->Name);
+ lua_setfield(s,plugin,"name");
+
+ lua_pushstring(s,dsc->Label);
+ lua_setfield(s,plugin,"label");
+
+ lua_pushstring(s,dsc->Maker);
+ lua_setfield(s,plugin,"maker");
+
+ lua_pushvalue(s,plugin);
+ lua_rawseti(s,info,i);
+ lua_settop(s,info);
+ }
+
+ lua_settop(s,info);
+ return 1;
+}
+
+static int l_out(lua_State *s)
+{
+ struct ladspa *l = *(void**) luaL_checkudata(s,1,LADSPA_TYPENAME);
+ size_t i = luaL_optinteger(s,2,1);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = ml_get_output(l->node,i - 1);
+ luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME);
+ return 1;
+}
+
+int luaopen_melisma_ladspa(lua_State *s)
+{
+/*** LADSPA plugin support.
+
+ local mladspa = require 'melisma.ladspa'
+
+ local spec = 'caps.so:Plate'
+ local info = mladspa.info(spec)
+
+ print('name:',info.name)
+ print('maker:',info.maker)
+
+ local input = ...
+ local plate = mladspa.load(spec,input)
+
+ plate:control(1,1)
+
+ local left = plate:out(6)
+ local right = plate:out(7)
+
+@classmod ladspa
+*/
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+/*** Load a ladspa plugin.
+
+Input signals are connected to the plugins audio input ports in the
+order that they are declared.
+
+@static
+@function load
+@tparam string spec Plugin specification.
+@tparam channel ... Input signals.
+*/
+ lua_pushcfunction(s,l_load);
+ lua_setfield(s,tbl,"load");
+
+/*** Get information about a specific plugin.
+
+Plugin specifications are written in the format `<path>:<plugin>`,
+where `<path>` is the full name of the plugin library
+(e.g. `caps.so`), and `<plugin>` is the name or label of the plugin.
+If `<path>` is not an absolute path, the library is searched for
+first in the current directory, then in `LADSPA_PATH`.
+
+This can be used to generate more ergonomic plugin interfaces.
+
+@static
+@function info
+@tparam string spec Plugin specification.
+@treturn table Plugin information.
+*/
+ lua_pushcfunction(s,l_info);
+ lua_setfield(s,tbl,"info");
+
+/*** List plugins in the specified plugin library.
+
+If `path` is not an absolute path, the library is searched for
+first in the current directory, then in `LADSPA_PATH`.
+
+@static
+@function lib_info
+@tparam string path Path to plugin library.
+@treturn table List of short plugin descriptions.
+*/
+ lua_pushcfunction(s,l_lib_info);
+ lua_setfield(s,tbl,"lib_info");
+
+/*** Set the value of an input control port.
+
+Ports are indexed based on the order they are declared by the plugin.
+This also means the indices of control ports are not necessarily consecutive.
+
+@function control
+@tparam integer i One-based port index.
+@tparam number v New control port value.
+*/
+ lua_pushcfunction(s,l_control);
+ lua_setfield(s,tbl,"control");
+
+/*** Get the audio output port at the specified index.
+
+Ports are indexed based on the order they are declared by the plugin.
+This also means the indices of audio ports are not necessarily consecutive.
+
+@function out
+@tparam integer i One-based port index.
+@treturn channel Audio output at specified index.
+*/
+ lua_pushcfunction(s,l_out);
+ lua_setfield(s,tbl,"out");
+
+ luaL_newmetatable(s,LADSPA_TYPENAME);
+ int mt = lua_gettop(s);
+
+ lua_pushvalue(s,tbl);
+ lua_setfield(s,mt,"__index");
+
+ lua_settop(s,tbl);
+ return 1;
+}
diff --git a/mod/lookback.c b/mod/lookback.c
new file mode 100644
index 0000000..115f823
--- /dev/null
+++ b/mod/lookback.c
@@ -0,0 +1,106 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+
+#include <lauxlib.h>
+
+#include "melisma.h"
+#include "mutil.h"
+
+#define LOOKBACK_TYPENAME "lookback"
+
+struct lookback
+{
+ ml_node *node;
+ size_t cursor;
+ size_t len;
+ float buf[];
+};
+
+static void proc(const ml_proc_args *args)
+{
+ struct lookback *lb = args->userp;
+
+ for (size_t i = 0; i < args->nframes; i++)
+ {
+ args->out[0][i] = lb->buf[lb->cursor];
+ lb->buf[lb->cursor] = args->in[0][i];
+
+ lb->cursor++;
+
+ if (lb->cursor == lb->len)
+ {
+ lb->cursor = 0;
+ }
+ }
+}
+
+static int l_init(lua_State *s)
+{
+ float t = luaL_checknumber(s,1);
+ ml_channel *src = mutil_get_channel(s,2);
+
+ unsigned long sr = ml_sample_rate();
+ unsigned long len = sr*t;
+
+ struct lookback *lb = calloc(1,sizeof(*lb) + len*sizeof(lb->buf[0]));
+ lb->len = len;
+
+ ml_node *node = ml_create_node(proc,lb);
+ ml_label_node(node,"lookback");
+ lb->node = node;
+
+ ml_channel *dst = ml_add_input(node);
+ ml_connect(src,dst);
+ ml_add_output(node);
+
+ *(void**) lua_newuserdata(s,sizeof(void*)) = lb;
+ luaL_setmetatable(s,LOOKBACK_TYPENAME);
+
+ return 1;
+}
+
+static int l_out(lua_State *s)
+{
+ struct lookback *lb = *(void**) luaL_checkudata(s,1,LOOKBACK_TYPENAME);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = ml_get_output(lb->node,0);
+ luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME);
+ return 1;
+}
+
+int luaopen_melisma_lookback(lua_State *s)
+{
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+ lua_pushcfunction(s,l_init);
+ lua_setfield(s,tbl,"init");
+
+ lua_pushcfunction(s,l_out);
+ lua_setfield(s,tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION);
+
+ luaL_newmetatable(s,LOOKBACK_TYPENAME);
+ int mt = lua_gettop(s);
+
+ lua_pushvalue(s,tbl);
+ lua_setfield(s,mt,"__index");
+
+ lua_settop(s,tbl);
+ return 1;
+}
diff --git a/mod/map.h b/mod/map.h
new file mode 100644
index 0000000..c56487e
--- /dev/null
+++ b/mod/map.h
@@ -0,0 +1,49 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifndef MAP_H
+#define MAP_H
+
+#include "melisma.h"
+#include <lauxlib.h>
+
+struct map
+{
+ float value;
+ int channel;
+};
+
+static void map_from_arg(lua_State *s, struct map *m, int arg)
+{
+ if (lua_isnumber(s,arg))
+ {
+ }
+
+ if (lua_isuserdata(s,arg))
+ {
+ }
+
+ luaL_argerror(s,arg,"expected number or output channel");
+}
+
+static float map_get(struct map *m, size_t i, const ml_proc_args *args)
+{
+ return m->channel >= 0 ? args->in[m->channel][i] : m->value;
+}
+
+#endif
diff --git a/mod/mixer.c b/mod/mixer.c
new file mode 100644
index 0000000..ab5ade2
--- /dev/null
+++ b/mod/mixer.c
@@ -0,0 +1,114 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+#include <lauxlib.h>
+#include "melisma.h"
+#include "mutil.h"
+
+#define MIXER_TYPENAME "mixer"
+
+static void proc(const ml_proc_args *args)
+{
+ float *out = args->out[0];
+
+ memset(out,0,args->nframes * sizeof(out[0]));
+
+ for (size_t i = 0; i < args->icnt; i++)
+ {
+ const float *in = args->in[i];
+
+ for (size_t j = 0; j < args->nframes; j++)
+ {
+ out[j] += in[j];
+ }
+ }
+}
+
+static int l_init(lua_State *s)
+{
+ int n = lua_gettop(s);
+
+ if (n == 0)
+ {
+ luaL_error(s,"need at least one input channel");
+ }
+
+ ml_node *node = ml_create_node(proc,NULL);
+ ml_label_node(node,"mix");
+
+ for (int i = 0; i < n; i++)
+ {
+ ml_channel *src = mutil_get_channel(s,i+1);
+ ml_channel *dst = ml_add_input(node);
+ ml_connect(src,dst);
+ }
+
+ ml_channel *out = ml_add_output(node);
+
+ *(void**) lua_newuserdata(s,sizeof(void*)) = out;
+ luaL_setmetatable(s,MIXER_TYPENAME);
+
+ return 1;
+}
+
+static int l_out(lua_State *s)
+{
+ ml_channel *c = *(void**) luaL_checkudata(s,1,MIXER_TYPENAME);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = c;
+ luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME);
+ return 1;
+}
+
+int luaopen_melisma_mixer(lua_State *s)
+{
+ ml_check_version();
+
+/*** Mix signals.
+
+ local mmixer = require 'melisma.mixer'
+
+ local signals = ...
+ local mixer = mmixer.init(table.unpack(signals))
+
+@classmod mixer
+*/
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+/*** Mix input signals in to a single output signal.
+@static
+@function init
+@tparam channel ... Input signals.
+@treturn mixer The newly created mixer unit.
+*/
+ lua_pushcfunction(s,l_init);
+ lua_setfield(s,tbl,"init");
+
+ lua_pushcfunction(s,l_out);
+ lua_setfield(s,tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION);
+
+ luaL_newmetatable(s,MIXER_TYPENAME);
+ int mt = lua_gettop(s);
+
+ lua_pushvalue(s,tbl);
+ lua_setfield(s,mt,"__index");
+
+ lua_settop(s,tbl);
+ return 1;
+}
diff --git a/mod/mutil.h b/mod/mutil.h
new file mode 100644
index 0000000..215508b
--- /dev/null
+++ b/mod/mutil.h
@@ -0,0 +1,69 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifndef MUTIL_H
+#define MUTIL_H
+
+/* TODO: Move to main repo? */
+
+#include "melisma.h"
+#include <lauxlib.h>
+
+#define MUTIL_OUTPUT_CHANNEL_FUNCTION "__ml_out"
+
+static ml_channel *mutil_get_optchannel(lua_State *s, int i)
+{
+ ml_channel **c;
+
+ if ((c = luaL_testudata(s,i,MELISMA_OUTPUT_CHANNEL_TYPENAME)) != NULL)
+ {
+ return *c;
+ }
+
+ lua_getfield(s,i,MUTIL_OUTPUT_CHANNEL_FUNCTION);
+
+ if (!lua_isfunction(s,-1))
+ {
+ lua_pop(s,1);
+ return NULL;
+ }
+
+ lua_pushvalue(s,i);
+ lua_call(s,1,1);
+
+ if ((c = luaL_testudata(s,-1,MELISMA_OUTPUT_CHANNEL_TYPENAME)) != NULL)
+ {
+ lua_pop(s,1);
+ return *c;
+ }
+
+ lua_pop(s,1);
+ return NULL;
+}
+
+static ml_channel *mutil_get_channel(lua_State *s, int i)
+{
+ ml_channel *c = mutil_get_optchannel(s,i);
+ if (c == NULL)
+ {
+ luaL_argerror(s,i,"expected output channel");
+ }
+ return c;
+}
+
+#endif
diff --git a/mod/null.c b/mod/null.c
new file mode 100644
index 0000000..815d622
--- /dev/null
+++ b/mod/null.c
@@ -0,0 +1,76 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <string.h>
+#include <lua.h>
+#include <lauxlib.h>
+#include "melisma.h"
+#include "mutil.h"
+
+#define NULL_TYPENAME "null"
+
+static void proc(const ml_proc_args *args)
+{
+ assert(args->ocnt == 1);
+ memset(args->out[0],0,sizeof(*args->out[0])*args->nframes);
+}
+
+static int l_init(lua_State *s)
+{
+ ml_node *node = ml_create_node(proc,NULL);
+ ml_label_node(node,"null");
+
+ (void) ml_add_output(node);
+
+ *(void**) lua_newuserdata(s,sizeof(void*)) = node;
+ luaL_setmetatable(s,NULL_TYPENAME);
+
+ return 1;
+}
+
+static int l_out(lua_State *s)
+{
+ ml_node *node = *(void**) luaL_checkudata(s,1,NULL_TYPENAME);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = ml_get_output(node,0);
+ luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME);
+ return 1;
+}
+
+int luaopen_melisma_null(lua_State *s)
+{
+ ml_check_version();
+
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+ lua_pushcfunction(s,l_init);
+ lua_setfield(s,tbl,"init");
+
+ lua_pushcfunction(s,l_out);
+ lua_setfield(s,tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION);
+
+ luaL_newmetatable(s,NULL_TYPENAME);
+ int mt = lua_gettop(s);
+
+ lua_pushvalue(s,tbl);
+ lua_setfield(s,mt,"__index");
+
+ lua_settop(s,tbl);
+ return 1;
+}
diff --git a/mod/nullsink.c b/mod/nullsink.c
new file mode 100644
index 0000000..0cbd810
--- /dev/null
+++ b/mod/nullsink.c
@@ -0,0 +1,111 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+#include <lua.h>
+#include <lauxlib.h>
+#include "melisma.h"
+#include "mutil.h"
+
+#define NULLSINK_TYPENAME "nullsink"
+
+static int l_init(lua_State *s)
+{
+ ml_node *node = ml_create_node(NULL,NULL);
+ ml_label_node(node,"nullsink");
+ ml_node *root = ml_get_root_node();
+
+ ml_channel *src = ml_add_output(node);
+ ml_channel *dst = ml_add_input(root);
+
+ ml_connect(src,dst);
+
+ *(void**) lua_newuserdata(s,sizeof(void*)) = node;
+ luaL_setmetatable(s,NULLSINK_TYPENAME);
+
+ return 1;
+}
+
+static int l_ref(lua_State *s)
+{
+ ml_node *node = *(void**) luaL_checkudata(s,1,NULLSINK_TYPENAME);
+ ml_channel *src = mutil_get_channel(s,2);
+ ml_channel *dst = ml_add_input(node);
+ ml_connect(src,dst);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = node;
+ luaL_setmetatable(s,NULLSINK_TYPENAME);
+ return 1;
+}
+
+int luaopen_melisma_nullsink(lua_State *s)
+{
+ ml_check_version();
+
+/*** Reference otherwise disconnected nodes.
+
+ local mnullsink = require 'melisma.nullsink'
+ local minterp = require 'melisma.interp'
+ local mevent = require 'melisma.event'
+
+ local interp = minterp.init(0)
+ interp:linear(1,60)
+
+ -- The output of interp is disconnected.
+ -- Need to connect it to a nullsink so we
+ -- can sample it in the event callback below.
+ local nullsink = mnullsink.init()
+ nullsink:ref(interp)
+
+ mevent.schedule(30, function()
+ print('current value:', interp:sample())
+ end)
+
+@classmod nullsink
+*/
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+/*** Create a nullsink.
+@static
+@function init
+@treturn nullsink The newly created nullsink.
+*/
+ lua_pushcfunction(s,l_init);
+ lua_setfield(s,tbl,"init");
+
+/*** Connect an input signal.
+
+This makes sure the input signal is processed
+even if it's not connected to anything else.
+
+@function ref
+@tparam channel input Input signal.
+@treturn nullsink
+*/
+ lua_pushcfunction(s,l_ref);
+ lua_setfield(s,tbl,"ref");
+
+ luaL_newmetatable(s,NULLSINK_TYPENAME);
+ int mt = lua_gettop(s);
+
+ lua_pushvalue(s,tbl);
+ lua_setfield(s,mt,"__index");
+
+ lua_settop(s,tbl);
+ return 1;
+}
diff --git a/mod/oscil.c b/mod/oscil.c
new file mode 100644
index 0000000..7bc39c7
--- /dev/null
+++ b/mod/oscil.c
@@ -0,0 +1,438 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+
+#include <lua.h>
+#include <lauxlib.h>
+
+#include "melisma.h"
+#include "mutil.h"
+
+#define WAVETABLE_TYPENAME "wavetable"
+#define OSCIL_TYPENAME "oscil"
+
+#ifndef WAVETABLE_SIZE
+#define WAVETABLE_SIZE 2048
+#endif
+
+#define WAVETABLE_ALLOC_SIZE (sizeof(float)*WAVETABLE_SIZE)
+
+struct oscil
+{
+ ml_node *node;
+ unsigned long clk;
+ float f;
+ float a;
+ int mf;
+ int ma;
+ float p;
+ float v;
+ float tbl[WAVETABLE_SIZE];
+};
+
+static float *push_new_wavetable(lua_State *s)
+{
+ float *ptr = lua_newuserdata(s,WAVETABLE_ALLOC_SIZE);
+ luaL_setmetatable(s,WAVETABLE_TYPENAME);
+ memset(ptr,0,WAVETABLE_ALLOC_SIZE);
+ return ptr;
+}
+
+static int l_const(lua_State *s)
+{
+ float v = luaL_checknumber(s,1);
+ float *tbl = push_new_wavetable(s);
+
+ for (size_t i = 0; i < WAVETABLE_SIZE; i++)
+ {
+ tbl[i] = v;
+ }
+
+ return 1;
+}
+
+static int l_sin(lua_State *s)
+{
+ float *tbl = push_new_wavetable(s);
+
+ for (size_t i = 0; i < WAVETABLE_SIZE; i++)
+ {
+ float f = 2.f * M_PI * (float) i / (float) WAVETABLE_SIZE;
+ tbl[i] = sinf(f);
+ }
+
+ return 1;
+}
+
+static int l_saw(lua_State *s)
+{
+ float *tbl = push_new_wavetable(s);
+
+ for (size_t i = 0; i < WAVETABLE_SIZE; i++)
+ {
+ tbl[i] = 2 * (float) i / (float) WAVETABLE_SIZE - 1;
+ }
+
+ return 1;
+}
+
+static int l_square(lua_State *s)
+{
+ float *tbl = push_new_wavetable(s);
+
+ for (size_t i = 0; i < WAVETABLE_SIZE; i++)
+ {
+ if (i >= WAVETABLE_SIZE/2)
+ {
+ tbl[i] = 1;
+ }
+ else
+ {
+ tbl[i] = -1;
+ }
+ }
+
+ return 1;
+}
+
+static int l_shift(lua_State *s)
+{
+ float *src = luaL_checkudata(s,1,WAVETABLE_TYPENAME);
+ float p = luaL_checknumber(s,2);
+
+ while (p < 0)
+ {
+ p += 2*M_PI;
+ }
+
+ while (p > 2*M_PI)
+ {
+ p -= 2*M_PI;
+ }
+
+ size_t offset = p/2.f/M_PI*WAVETABLE_SIZE;
+ float *dst = push_new_wavetable(s);
+
+ size_t i,d;
+ for (i=0, d=offset; i < WAVETABLE_SIZE; i++, d++)
+ {
+ dst[d % WAVETABLE_SIZE] = src[i];
+ }
+
+ return 1;
+}
+
+static int l_scale(lua_State *s)
+{
+ float *src = luaL_checkudata(s,1,WAVETABLE_TYPENAME);
+ int n = luaL_checkinteger(s,2);
+
+ // if (n <= 0 || (n & (n-1)) != 0)
+ // {
+ // luaL_error(s,"scaling factor must be a power of two");
+ // }
+
+ float *dst = push_new_wavetable(s);
+
+ for (size_t i = 0; i < WAVETABLE_SIZE; i++)
+ {
+ size_t j = (n*i) % WAVETABLE_SIZE;
+ dst[i] = src[j];
+ }
+
+ return 1;
+}
+
+static int l_add(lua_State *s)
+{
+ float *lhs = luaL_checkudata(s,1,WAVETABLE_TYPENAME);
+ float *rhs = luaL_checkudata(s,2,WAVETABLE_TYPENAME);
+ float *res = push_new_wavetable(s);
+
+ for (size_t i = 0; i < WAVETABLE_SIZE; i++)
+ {
+ res[i] = lhs[i] + rhs[i];
+ }
+
+ return 1;
+}
+
+static void proc(const ml_proc_args *args)
+{
+ struct oscil *oscil = args->userp;
+
+ unsigned long sr = ml_sample_rate();
+ float p = oscil->p;
+ float *dst = args->out[0];
+
+ float *mf = oscil->mf < 0 ? NULL : args->in[oscil->mf];
+ float *ma = oscil->ma < 0 ? NULL : args->in[oscil->ma];
+
+ for (size_t i = 0; i < args->nframes; i++)
+ {
+ float f = mf ? mf[i] : oscil->f;
+ float a = ma ? ma[i] : oscil->a;
+
+ float t = p*WAVETABLE_SIZE;
+
+ size_t i0 = (size_t) t;
+ size_t i1 = i0 + 1;
+
+ float ip = t - i0;
+
+ i0 &= (WAVETABLE_SIZE-1);
+ i1 &= (WAVETABLE_SIZE-1);
+
+ float s0 = oscil->tbl[i0];
+ float s1 = oscil->tbl[i1];
+
+ dst[i] = a*(s0 + ip*(s1 - s0));
+
+ p += f/sr;
+
+ if (p > 1)
+ {
+ p -= 1;
+ }
+ }
+
+ oscil->p = p;
+ oscil->v = dst[args->nframes-1];
+}
+
+static int l_init(lua_State *s)
+{
+ float *tbl = luaL_checkudata(s,1,WAVETABLE_TYPENAME);
+ float f = luaL_checknumber(s,2);
+ float a = luaL_checknumber(s,3);
+
+ struct oscil *oscil = calloc(1,sizeof(*oscil));
+ oscil->clk = 0;
+ oscil->f = f;
+ oscil->a = a;
+ oscil->mf = -1;
+ oscil->ma = -1;
+ oscil->p = 0;
+ oscil->v = a*tbl[0];
+
+ ml_node *node = ml_create_node(proc,oscil);
+ ml_label_node(node,"oscil");
+ ml_add_output(node);
+ oscil->node = node;
+
+ memcpy(oscil->tbl,tbl,WAVETABLE_ALLOC_SIZE);
+
+ *(void**) lua_newuserdata(s,sizeof(void*)) = oscil;
+ luaL_setmetatable(s,OSCIL_TYPENAME);
+
+ return 1;
+}
+
+static int l_setf(lua_State *s)
+{
+ struct oscil *oscil = *(void**) luaL_checkudata(s,1,OSCIL_TYPENAME);
+ float v = luaL_checknumber(s,2);
+ oscil->f = v;
+ return 0;
+}
+
+static int l_seta(lua_State *s)
+{
+ struct oscil *oscil = *(void**) luaL_checkudata(s,1,OSCIL_TYPENAME);
+ float v = luaL_checknumber(s,2);
+ oscil->a = v;
+ return 0;
+}
+
+static int l_mapf(lua_State *s)
+{
+ struct oscil *oscil = *(void**) luaL_checkudata(s,1,OSCIL_TYPENAME);
+ ml_channel *src = mutil_get_channel(s,2);
+
+ oscil->mf = ml_num_inputs(oscil->node);
+
+ ml_channel *dst = ml_add_input(oscil->node);
+ ml_connect(src,dst);
+
+ return 0;
+}
+
+static int l_mapa(lua_State *s)
+{
+ struct oscil *oscil = *(void**) luaL_checkudata(s,1,OSCIL_TYPENAME);
+ ml_channel *src = mutil_get_channel(s,2);
+
+ oscil->ma = ml_num_inputs(oscil->node);
+
+ ml_channel *dst = ml_add_input(oscil->node);
+ ml_connect(src,dst);
+
+ return 0;
+}
+
+static int l_sample(lua_State *s)
+{
+ struct oscil *oscil = *(void**) luaL_checkudata(s,1,OSCIL_TYPENAME);
+ lua_pushnumber(s,oscil->v);
+ return 1;
+}
+
+static int l_out(lua_State *s)
+{
+ struct oscil *oscil = *(void**) luaL_checkudata(s,1,OSCIL_TYPENAME);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = ml_get_output(oscil->node,0);
+ luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME);
+ return 1;
+}
+
+int luaopen_melisma_oscil(lua_State *s)
+{
+
+/*** Wavetable backed oscillators.
+
+ local moscil = require 'melisma.oscil'
+ local mexpression = require 'melisma.expression'
+
+ local wt = moscil.wavetable.sin()
+
+ -- Make an LFO for controlling the frequency.
+ local lfo_oscil = moscil.init(wt,1,1)
+
+ -- Map [-1, 1] to [440, 880]
+ local lfo = (440 + (1 + mexpression.input(lfo_oscil,0) / 2) * 440):build()
+
+ local oscil = moscil.init(wt,0,0.5)
+ oscil:mapf(lfo)
+
+@classmod oscil
+*/
+ lua_newtable(s);
+ int tbl = lua_absindex(s,lua_gettop(s));
+
+/*** Initialize an oscillator.
+@static
+@function init
+@tparam wavetable wt
+@tparam number freq
+@tparam number amp
+@treturn oscil
+*/
+ lua_pushcfunction(s,l_init);
+ lua_setfield(s,tbl,"init");
+
+/*** Set oscillator frequency.
+@function setf
+@tparam number freq
+*/
+ lua_pushcfunction(s,l_setf);
+ lua_setfield(s,tbl,"setf");
+
+/*** Set oscillator amplitude.
+@function seta
+@tparam number amp
+*/
+ lua_pushcfunction(s,l_seta);
+ lua_setfield(s,tbl,"seta");
+
+/*** Map a signal to the frequency.
+@function mapf
+@tparam channel sig
+*/
+ lua_pushcfunction(s,l_mapf);
+ lua_setfield(s,tbl,"mapf");
+
+/*** Map a signal to the amplitude.
+@function mapa
+@tparam channel sig
+*/
+ lua_pushcfunction(s,l_mapa);
+ lua_setfield(s,tbl,"mapa");
+
+/*** Get the current oscillator value.
+@function sample
+@treturn number
+*/
+ lua_pushcfunction(s,l_sample);
+ lua_setfield(s,tbl,"sample");
+
+ lua_pushcfunction(s,l_out);
+ lua_setfield(s,tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION);
+
+ luaL_newmetatable(s,OSCIL_TYPENAME);
+ int oscil_mt = lua_gettop(s);
+
+ lua_pushvalue(s,tbl);
+ lua_setfield(s,oscil_mt,"__index");
+
+/***
+@type wavetable
+*/
+ lua_newtable(s);
+ int wt_tbl = lua_absindex(s,lua_gettop(s));
+
+ lua_pushvalue(s,wt_tbl);
+ lua_setfield(s,tbl,"wavetable");
+
+ lua_pushcfunction(s,l_const);
+ lua_setfield(s,wt_tbl,"const");
+
+/*** Sine wave.
+@static
+@function wavetable.sin
+@treturn wavetable
+*/
+ lua_pushcfunction(s,l_sin);
+ lua_setfield(s,wt_tbl,"sin");
+
+/*** Saw wave.
+@static
+@function wavetable.saw
+@treturn wavetable
+*/
+ lua_pushcfunction(s,l_saw);
+ lua_setfield(s,wt_tbl,"saw");
+
+/*** Square wave.
+@static
+@function wavetable.square
+@treturn wavetable
+*/
+ lua_pushcfunction(s,l_square);
+ lua_setfield(s,wt_tbl,"square");
+
+ lua_pushcfunction(s,l_shift);
+ lua_setfield(s,wt_tbl,"shift");
+
+ lua_pushcfunction(s,l_scale);
+ lua_setfield(s,wt_tbl,"scale");
+
+ luaL_newmetatable(s,WAVETABLE_TYPENAME);
+ int wt_mt = lua_gettop(s);
+
+ lua_pushvalue(s,wt_tbl);
+ lua_setfield(s,wt_mt,"__index");
+
+ lua_pushcfunction(s,l_add);
+ lua_setfield(s,wt_mt,"__add");
+
+ lua_settop(s,tbl);
+ return 1;
+}
diff --git a/mod/pan.c b/mod/pan.c
new file mode 100644
index 0000000..84a0551
--- /dev/null
+++ b/mod/pan.c
@@ -0,0 +1,187 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <lauxlib.h>
+#include "melisma.h"
+#include "mutil.h"
+
+#define PAN_TYPENAME "pan"
+
+struct pan
+{
+ ml_channel *l_out;
+ ml_channel *r_out;
+ float val;
+ bool map;
+};
+
+static void proc(const ml_proc_args *args)
+{
+ struct pan *pan = args->userp;
+
+ float val,*in,*l_out,*r_out,*map;
+ in = args->in[0];
+ l_out = args->out[0];
+ r_out = args->out[1];
+ map = pan->map ? args->in[1] : NULL;
+
+ for (size_t i = 0; i < args->nframes; i++)
+ {
+ val = map ? map[i] : pan->val;
+ l_out[i] = val*in[i];
+ r_out[i] = (1 - val)*in[i];
+ }
+}
+
+static int l_init(lua_State *s)
+{
+ ml_channel *dst,*src,*map_dst,*map_src = NULL;
+ float val = 0;
+
+ if (lua_isuserdata(s,1))
+ {
+ map_src = mutil_get_channel(s,1);
+ }
+ else
+ {
+ val = luaL_checknumber(s,1);
+ }
+
+ src = mutil_get_channel(s,2);
+
+ struct pan *pan = calloc(1,sizeof(*pan));
+ pan->val = val;
+ pan->map = map_src != NULL;
+
+ ml_node *node = ml_create_node(proc,pan);
+ ml_label_node(node,"pan");
+
+ dst = ml_add_input(node);
+ pan->l_out = ml_add_output(node);
+ pan->r_out = ml_add_output(node);
+
+ ml_connect(src,dst);
+
+ if (pan->map)
+ {
+ map_dst = ml_add_input(node);
+ ml_connect(map_src,map_dst);
+ }
+
+ *(void**) lua_newuserdata(s,sizeof(void*)) = pan;
+ luaL_setmetatable(s,PAN_TYPENAME);
+
+ return 1;
+}
+
+static int l_left(lua_State *s)
+{
+ struct pan *pan = *(void**) luaL_checkudata(s,1,PAN_TYPENAME);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = pan->l_out;
+ luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME);
+ return 1;
+}
+
+static int l_right(lua_State *s)
+{
+ struct pan *pan = *(void**) luaL_checkudata(s,1,PAN_TYPENAME);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = pan->r_out;
+ luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME);
+ return 1;
+}
+
+static int l_set(lua_State *s)
+{
+ struct pan *pan = *(void**) luaL_checkudata(s,1,PAN_TYPENAME);
+ pan->val = luaL_checknumber(s,2);
+ return 0;
+}
+
+int luaopen_melisma_pan(lua_State *s)
+{
+ ml_check_version();
+
+/*** Panning
+
+ local mpan = require 'melisma.pan'
+ local mexpression = require 'melisma.expression'
+ local moscil = require 'melisma.oscil'
+
+ local wt = moscil.wavetable.sin()
+ local lfo_oscil = moscil.init(wt,1,1)
+ local lfo = (1 + mexpression.input(lfo_oscil,0) / 2):build()
+
+ local oscil = moscil.init(wt,440,0.5)
+ local pan = mpan.init(lfo,oscil)
+
+ local left = pan:left()
+ local right = pan:right()
+
+@classmod pan
+*/
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+/*** Initialize a panning unit.
+
+A panning value ranges from 0 to 1, where a value of 0
+will pan everything to the left.
+
+@static
+@function init
+@tparam number|channel x Constant pan or a controlling signal.
+@tparam channel sig Input signal.
+@treturn pan The newly created panning unit.
+*/
+ lua_pushcfunction(s,l_init);
+ lua_setfield(s,tbl,"init");
+
+/*** Get the left output signal.
+@function left
+@treturn channel
+*/
+ lua_pushcfunction(s,l_left);
+ lua_setfield(s,tbl,"left");
+
+
+/*** Get the right output signal.
+@function right
+@treturn channel
+*/
+ lua_pushcfunction(s,l_right);
+ lua_setfield(s,tbl,"right");
+
+/*** Set the panning value.
+@function set
+@tparam number x
+*/
+ lua_pushcfunction(s,l_set);
+ lua_setfield(s,tbl,"set");
+
+ luaL_newmetatable(s,PAN_TYPENAME);
+ int mt = lua_gettop(s);
+
+ lua_pushvalue(s,tbl);
+ lua_setfield(s,mt,"__index");
+
+ lua_settop(s,tbl);
+ return 1;
+}
diff --git a/mod/passthrough.c b/mod/passthrough.c
new file mode 100644
index 0000000..775c6fe
--- /dev/null
+++ b/mod/passthrough.c
@@ -0,0 +1,82 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <string.h>
+#include <lua.h>
+#include <lauxlib.h>
+#include "melisma.h"
+#include "mutil.h"
+
+#define PASSTHROUGH_TYPENAME "passthrough"
+
+static void proc(const ml_proc_args *args)
+{
+ assert(args->icnt == 1);
+ assert(args->ocnt == 1);
+ memcpy(args->out[0],args->in[0],args->nframes*sizeof(float));
+}
+
+static int l_init(lua_State *s)
+{
+ ml_channel *src = mutil_get_channel(s,1);
+
+ ml_node *node = ml_create_node(proc,NULL);
+ ml_label_node(node,"passthrough");
+
+ ml_channel *dst = ml_add_input(node);
+ ml_connect(src,dst);
+
+ (void) ml_add_output(node);
+
+ *(void**) lua_newuserdata(s,sizeof(void*)) = node;
+ luaL_setmetatable(s,PASSTHROUGH_TYPENAME);
+
+ return 1;
+}
+
+static int l_out(lua_State *s)
+{
+ ml_node *node = *(void**) luaL_checkudata(s,1,PASSTHROUGH_TYPENAME);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = ml_get_output(node,0);
+ luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME);
+ return 1;
+}
+
+int luaopen_melisma_passthrough(lua_State *s)
+{
+ ml_check_version();
+
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+ lua_pushcfunction(s,l_init);
+ lua_setfield(s,tbl,"init");
+
+ lua_pushcfunction(s,l_out);
+ lua_setfield(s,tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION);
+
+ luaL_newmetatable(s,PASSTHROUGH_TYPENAME);
+ int mt = lua_gettop(s);
+
+ lua_pushvalue(s,tbl);
+ lua_setfield(s,mt,"__index");
+
+ lua_settop(s,tbl);
+ return 1;
+}
diff --git a/mod/playback.c b/mod/playback.c
new file mode 100644
index 0000000..f9d22ca
--- /dev/null
+++ b/mod/playback.c
@@ -0,0 +1,342 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+#include <lauxlib.h>
+#include "melisma.h"
+#include "mutil.h"
+#include "sample.h"
+
+#define PLAYBACK_TYPENAME "playback"
+
+struct playback
+{
+ ml_node *node;
+ struct sample *smp;
+ double speed;
+ double phase;
+ bool active;
+ bool loop;
+ int fn;
+ int arg;
+};
+
+static void proc(const ml_proc_args *args)
+{
+ struct playback *pb = args->userp;
+
+ if (pb->smp == NULL)
+ {
+ memset(args->out[0],0,sizeof(*args->out[0])*args->nframes);
+ return;
+ }
+
+ unsigned long sr = ml_sample_rate();
+ double phase = pb->phase;
+ float *dst = args->out[0];
+ size_t len = pb->smp->len;
+ float *data = pb->smp->data;
+
+ for (size_t i = 0; i < args->nframes; i++)
+ {
+ if (!pb->active)
+ {
+ memset(&dst[i],0,(args->nframes - i)*sizeof(*dst));
+ return;
+ }
+
+ float t = phase*len;
+
+ size_t i0 = (size_t) t;
+ size_t i1 = i0 + 1;
+ float ip = t - i0;
+
+ i0 %= len;
+ i1 %= len;
+
+ float s0 = data[i0];
+ float s1 = data[i1];
+
+ dst[i] = s0 + ip*(s1 - s0);
+
+ phase += pb->speed/len;
+
+ while (phase > 1)
+ {
+ phase -= 1;
+
+ if (pb->fn != LUA_NOREF)
+ {
+ ml_schedule_event(0,pb->fn,pb->arg);
+
+ }
+
+ if (!pb->loop)
+ {
+ pb->active = false;
+ pb->phase = 0;
+ }
+ }
+
+ while (phase < 0)
+ {
+ phase += 1;
+
+ if (pb->fn != LUA_NOREF)
+ {
+ ml_schedule_event(0,pb->fn,pb->arg);
+
+ }
+
+ if (!pb->loop)
+ {
+ pb->active = false;
+ pb->phase = 0;
+ }
+ }
+ }
+
+ pb->phase = phase;
+}
+
+static int l_init(lua_State *s)
+{
+ struct sample *smp = NULL;
+
+ if (!lua_isnoneornil(s,1))
+ {
+ smp = *(void**) luaL_checkudata(s,1,SAMPLE_TYPENAME);
+ }
+
+ struct playback *pb = calloc(1,sizeof(*pb));
+
+ pb->smp = smp;
+ pb->speed = 1;
+ pb->fn = LUA_NOREF;
+ pb->arg = LUA_NOREF;
+ pb->node = ml_create_node(proc,pb);
+ ml_label_node(pb->node,"playback (%s)",smp ? smp->path : "NULL");
+
+ ml_add_output(pb->node);
+
+ *(void**) lua_newuserdata(s,sizeof(void*)) = pb;
+ luaL_setmetatable(s,PLAYBACK_TYPENAME);
+
+ return 1;
+}
+
+static int l_set_sample(lua_State *s)
+{
+ struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME);
+ struct sample *smp = *(void**) luaL_checkudata(s,2,SAMPLE_TYPENAME);
+ pb->smp = smp;
+ return 0;
+}
+
+static int l_play(lua_State *s)
+{
+ struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME);
+ pb->active = true;
+ return 0;
+}
+
+static int l_pause(lua_State *s)
+{
+ struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME);
+ pb->active = false;
+ return 0;
+}
+
+static int l_reset(lua_State *s)
+{
+ struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME);
+ pb->phase = 0;
+ return 0;
+}
+
+static int l_stop(lua_State *s)
+{
+ struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME);
+ pb->phase = 0;
+ pb->active = false;
+ return 0;
+}
+
+static int l_speed(lua_State *s)
+{
+ struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME);
+
+ if (!lua_isnil(s,2))
+ {
+ pb->speed = luaL_checknumber(s,2);
+ }
+
+ lua_pushnumber(s,pb->speed);
+ return 1;
+}
+
+static int l_loop(lua_State *s)
+{
+ struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME);
+
+ if (!lua_isnil(s,2))
+ {
+ luaL_checktype(s,2,LUA_TBOOLEAN);
+ pb->loop = lua_toboolean(s,2);
+ }
+
+ lua_pushboolean(s,pb->loop);
+ return 1;
+}
+
+static int l_phase(lua_State *s)
+{
+ struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME);
+
+ if (!lua_isnil(s,2))
+ {
+ double phase = luaL_checknumber(s,2);
+
+ if (phase > 1)
+ {
+ phase = 1;
+ }
+
+ if (phase < 0)
+ {
+ phase = 0;
+ }
+
+ pb->phase = phase;
+ }
+
+ lua_pushnumber(s,pb->phase);
+ return 1;
+}
+
+static int l_out(lua_State *s)
+{
+ struct playback *pb = *(void**) luaL_checkudata(s,1,PLAYBACK_TYPENAME);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = ml_get_output(pb->node,0);
+ luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME);
+ return 1;
+}
+
+int luaopen_melisma_playback(lua_State *s)
+{
+/*** Audio sample playback.
+
+ local msample = require 'melisma.sample'
+ local mplayback = require 'melisma.playback'
+
+ local sample = msample.load('sample.wav')
+ local playb = mplayback.init(sample)
+
+ playb:loop(true)
+ playb:spped(2)
+ playb:play()
+
+@classmod playback
+*/
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+/*** Intialize a playback unit.
+@static
+@function init
+@tparam sample|nil smp
+@treturn playback
+*/
+ lua_pushcfunction(s,l_init);
+ lua_setfield(s,tbl,"init");
+
+/*** Set playback sample.
+@function set_sample
+@tparam sample smp
+*/
+ lua_pushcfunction(s,l_set_sample);
+ lua_setfield(s,tbl,"set_sample");
+
+/*** Start playback.
+@function play
+*/
+ lua_pushcfunction(s,l_play);
+ lua_setfield(s,tbl,"play");
+
+/*** Pause playback.
+@function pause
+*/
+ lua_pushcfunction(s,l_pause);
+ lua_setfield(s,tbl,"pause");
+
+/*** Stop playback.
+
+Equivalent to @{playback.pause}; @{playback.reset}.
+
+@function stop
+*/
+ lua_pushcfunction(s,l_stop);
+ lua_setfield(s,tbl,"stop");
+
+
+/*** Reset playback.
+
+Equivalent to @{playback.phase}(0).
+
+@function reset
+*/
+ lua_pushcfunction(s,l_reset);
+ lua_setfield(s,tbl,"reset");
+
+/*** Set playback speed.
+
+A negative speed will play the sample in reverse.
+
+@function reset
+*/
+ lua_pushcfunction(s,l_speed);
+ lua_setfield(s,tbl,"speed");
+
+/*** Enable/disable automatic looping.
+@function loop
+@tparam bool b
+*/
+ lua_pushcfunction(s,l_loop);
+ lua_setfield(s,tbl,"loop");
+
+/*** Set playback phase.
+@function phase
+@tparam number x Phase ranging from 0 to 1.
+*/
+ lua_pushcfunction(s,l_phase);
+ lua_setfield(s,tbl,"phase");
+
+ lua_pushcfunction(s,l_out);
+ lua_setfield(s,tbl,MUTIL_OUTPUT_CHANNEL_FUNCTION);
+
+ luaL_newmetatable(s,PLAYBACK_TYPENAME);
+ int mt = lua_gettop(s);
+
+ lua_pushvalue(s,tbl);
+ lua_setfield(s,mt,"__index");
+
+ lua_settop(s,tbl);
+ return 1;
+}
diff --git a/mod/sample.c b/mod/sample.c
new file mode 100644
index 0000000..6a3a24b
--- /dev/null
+++ b/mod/sample.c
@@ -0,0 +1,180 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <lauxlib.h>
+#include <sndfile.h>
+#include <samplerate.h>
+#include "melisma.h"
+#include "sample.h"
+
+static struct sample *alloc_sample(size_t len)
+{
+ struct sample *s = malloc(sizeof(*s) + len*sizeof(s->data[0]));
+ s->len = len;
+ return s;
+}
+
+static struct sample *resample(struct sample *old, unsigned long from, unsigned long to)
+{
+ double ratio = (double)to / (double)from;
+ size_t new_len = ratio * old->len;
+ struct sample *new = alloc_sample(new_len);
+
+ SRC_DATA src_data = {
+ .data_in = old->data,
+ .data_out = new->data,
+ .input_frames = old->len,
+ .output_frames = new->len,
+ .src_ratio = ratio,
+ };
+
+ src_simple(&src_data,SRC_SINC_MEDIUM_QUALITY,1);
+
+ new->path = old->path;
+ free(old);
+
+ return new;
+}
+
+static int l_load(lua_State *s)
+{
+ const char *path = luaL_checkstring(s,1);
+
+ SF_INFO info;
+ SNDFILE *f;
+
+ f = sf_open(path,SFM_READ,&info);
+
+ if (f == NULL)
+ {
+ luaL_error(s,"failed to open audio file '%s': %s",path,sf_strerror(f));
+ }
+
+ size_t len = info.frames*info.channels;
+ float *data = malloc(sizeof(*data)*len);
+
+ size_t off = 0;
+ while (off < len)
+ {
+ ssize_t res = sf_read_float(f,&data[off],len - off);
+
+ if (res < 0)
+ {
+ luaL_error(s,"failed to open audio file '%s': %s",path,sf_strerror(f));
+ }
+
+ off += res;
+ }
+
+ sf_close(f);
+
+ size_t samplerate = ml_sample_rate();
+
+ for (size_t c = 0; c < info.channels; c++)
+ {
+ struct sample *smp = alloc_sample(info.frames);
+ smp->path = strdup(path);
+
+ for (size_t i = 0; i < info.frames; i++)
+ {
+ smp->data[i] = data[i*info.channels + c];
+ }
+
+ if (info.samplerate != samplerate)
+ {
+ smp = resample(smp,info.samplerate,samplerate);
+ }
+
+ *(void**) lua_newuserdata(s,sizeof(void*)) = smp;
+ luaL_setmetatable(s,SAMPLE_TYPENAME);
+ }
+
+ free(data);
+
+ return info.channels;
+}
+
+static int l_length(lua_State *s)
+{
+ struct sample *smp = *(void**) luaL_checkudata(s,1,SAMPLE_TYPENAME);
+ unsigned long sr = ml_sample_rate();
+ double l = smp->len / (double)sr;
+ lua_pushnumber(s,l);
+ return 1;
+}
+
+static int l_path(lua_State *s)
+{
+ struct sample *smp = *(void**) luaL_checkudata(s,1,SAMPLE_TYPENAME);
+ lua_pushstring(s,smp->path);
+ return 1;
+}
+
+int luaopen_melisma_sample(lua_State *s)
+{
+/*** Load audio samples.
+
+ local msample = require 'melisma.sample'
+
+ local smp = msample.load('sample.wav')
+
+ print(smp:length())
+ print(smp:path())
+
+@classmod sample
+*/
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+/*** Load an audio sample.
+
+If necessary, the audio will be resampled to the current sample rate.
+
+@static
+@function load
+@tparam string path
+@treturn sample
+*/
+ lua_pushcfunction(s,l_load);
+ lua_setfield(s,tbl,"load");
+
+/*** Get sample length.
+@function length
+@treturn number The length of the sample in seconds.
+*/
+ lua_pushcfunction(s,l_length);
+ lua_setfield(s,tbl,"length");
+
+/*** Get sample path.
+@function path
+@treturn string The path that the sample was loaded from.
+*/
+ lua_pushcfunction(s,l_path);
+ lua_setfield(s,tbl,"path");
+
+ luaL_newmetatable(s,SAMPLE_TYPENAME);
+ int mt = lua_gettop(s);
+
+ lua_pushvalue(s,tbl);
+ lua_setfield(s,mt,"__index");
+
+ lua_settop(s,tbl);
+ return 1;
+}
diff --git a/mod/sample.h b/mod/sample.h
new file mode 100644
index 0000000..af2ec83
--- /dev/null
+++ b/mod/sample.h
@@ -0,0 +1,33 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifndef SAMPLE_H_INCLUDED
+#define SAMPLE_H_INCLUDED
+
+#include <stddef.h>
+
+#define SAMPLE_TYPENAME "audio sample"
+
+struct sample
+{
+ char *path;
+ size_t len;
+ float data[];
+};
+
+#endif
diff --git a/mod/serial.c b/mod/serial.c
new file mode 100644
index 0000000..a7b51db
--- /dev/null
+++ b/mod/serial.c
@@ -0,0 +1,511 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+#include <assert.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <ctype.h>
+#include <unistd.h>
+#include <termios.h>
+#include <fcntl.h>
+#include <lauxlib.h>
+#include "melisma.h"
+#include "melisma/ring.h"
+
+#define SERIAL_TYPENAME "serial"
+#define DEFAULT_BAUD 9600
+#define DEFAULT_FORMAT "8N1"
+#define DEFAULT_ENCODING "cstring"
+#define DEFAULT_BUFSIZE 4096
+
+struct serial;
+
+typedef int (*decode_fn)(struct serial*);
+
+struct conf
+{
+ char *path;
+ char *format;
+ int datasize;
+ int stopbits;
+ int parity;
+ int baud;
+ int baudflag;
+ decode_fn decode;
+ int bufsize;
+ int callb;
+};
+
+struct serial
+{
+ struct conf conf;
+ int fd;
+ int synced;
+ size_t len;
+ size_t cap;
+ uint8_t *data;
+ uint8_t *tmpdata;
+ ml_ring *ring;
+ lua_State *state;
+};
+
+static int cstring(struct serial*);
+
+static const struct {
+ decode_fn decode;
+ const char *name;
+} encoding_map[] = {
+ { cstring, "cstring" },
+ { NULL, NULL },
+};
+
+static void free_conf(struct conf *c)
+{
+ free(c->path);
+ free(c->format);
+}
+
+static void put_byte(struct serial *se, int c)
+{
+ if (se->len == se->cap)
+ {
+ se->cap = se->cap == 0 ? 32 : se->cap*2;
+ se->data = realloc(se->data,se->cap);
+ se->tmpdata = realloc(se->tmpdata,se->cap);
+ }
+ se->data[se->len++] = c;
+}
+
+static void io_handler(int fd, int events, void *args)
+{
+ struct serial *se = args;
+ if (se->conf.decode(se) == 0)
+ {
+ if (ml_ring_has_space(se->ring,se->len + sizeof se->len))
+ {
+ ml_ring_write_blocking(se->ring,&se->len,sizeof se->len);
+ ml_ring_write_blocking(se->ring,se->data,se->len);
+ }
+ else
+ {
+ fprintf(stderr,"dropped %lu bytes\n",se->len);
+ }
+ }
+ else
+ {
+ fprintf(stderr,"failed to decode message\n");
+ }
+ se->len = 0;
+}
+
+static void read_conf(lua_State *s, int tbl, struct conf *c)
+{
+ int top = lua_gettop(s);
+ lua_getfield(s,tbl,"path");
+
+ if (lua_isnil(s,-1))
+ {
+ free_conf(c);
+ luaL_error(s,"missing 'path' field");
+ }
+
+ if (!lua_isstring(s,-1))
+ {
+ free_conf(c);
+ luaL_error(s,"'path' field is not a string");
+ }
+
+ c->path = strdup(lua_tostring(s,-1));
+ lua_settop(s,top);
+
+
+ lua_getfield(s,tbl,"baud");
+
+ if (lua_isnil(s,-1))
+ {
+ c->baud = DEFAULT_BAUD;
+ }
+ else if (lua_isinteger(s,-1))
+ {
+ c->baud = lua_tointeger(s,-1);
+ }
+ else
+ {
+ free_conf(c);
+ luaL_error(s,"'baud' field is not an integer");
+ }
+
+ switch (c->baud)
+ {
+ case 0: c->baudflag = B0; break;
+ case 50: c->baudflag = B50; break;
+ case 75: c->baudflag = B75; break;
+ case 110: c->baudflag = B110; break;
+ case 134: c->baudflag = B134; break;
+ case 150: c->baudflag = B150; break;
+ case 200: c->baudflag = B200; break;
+ case 300: c->baudflag = B300; break;
+ case 600: c->baudflag = B600; break;
+ case 1200: c->baudflag = B1200; break;
+ case 1800: c->baudflag = B1800; break;
+ case 2400: c->baudflag = B2400; break;
+ case 4800: c->baudflag = B4800; break;
+ case 9600: c->baudflag = B9600; break;
+ case 19200: c->baudflag = B19200; break;
+ case 38400: c->baudflag = B38400; break;
+ case 57600: c->baudflag = B57600; break;
+ case 115200: c->baudflag = B115200; break;
+ case 230400: c->baudflag = B230400; break;
+ case 460800: c->baudflag = B460800; break;
+ case 500000: c->baudflag = B500000; break;
+ case 576000: c->baudflag = B576000; break;
+ case 921600: c->baudflag = B921600; break;
+ case 1000000: c->baudflag = B1000000; break;
+ case 1152000: c->baudflag = B1152000; break;
+ case 1500000: c->baudflag = B1500000; break;
+ case 2000000: c->baudflag = B2000000; break;
+ default: c->baudflag = -1; break;
+ }
+
+ if (c->baudflag < 0)
+ {
+ free_conf(c);
+ luaL_error(s,"unsupported baud rate %d",c->baud);
+ }
+
+ lua_settop(s,top);
+
+ const char *format;
+ lua_getfield(s,tbl,"format");
+
+ if (lua_isnil(s,-1))
+ {
+ format = DEFAULT_FORMAT;
+ }
+ else if (lua_isstring(s,-1))
+ {
+ format = lua_tostring(s,-1);
+ }
+ else
+ {
+ free_conf(c);
+ luaL_error(s,"'format' field is not a string");
+ }
+
+ c->format = strdup(format);
+
+ if (strlen(format) != 3 ||
+ !strchr("5678",format[0]) ||
+ !strchr("NEOneo",format[1]) ||
+ !strchr("12",format[2]))
+ {
+ free_conf(c);
+ luaL_error(s,"invalid frame format '%s'",format);
+ }
+
+ c->datasize = format[0] - '0';
+ c->parity = toupper(format[1]);
+ c->stopbits = format[2] - '0';
+
+ lua_settop(s,top);
+
+ lua_getfield(s,tbl,"encoding");
+ const char *encoding = NULL;
+
+ if (lua_isnil(s,-1))
+ {
+ encoding = DEFAULT_ENCODING;
+ }
+ else if (lua_isstring(s,-1))
+ {
+ encoding = lua_tostring(s,-1);
+ }
+ else
+ {
+ free_conf(c);
+ luaL_error(s,"'encoding' field is not a string");
+ }
+
+ for (size_t i = 0;; i++)
+ {
+ c->decode = encoding_map[i].decode;
+ const char *name = encoding_map[i].name;
+
+ if (c->decode == NULL)
+ {
+ free_conf(c);
+ luaL_error(s,"unknown encoding '%s'",encoding);
+ }
+
+ if (strcmp(name,encoding) == 0)
+ {
+ break;
+ }
+ }
+
+ lua_getfield(s,tbl,"bufsize");
+
+ if (lua_isnil(s,-1))
+ {
+ c->bufsize = DEFAULT_BUFSIZE;
+ }
+ else if (lua_isinteger(s,-1))
+ {
+ c->bufsize = lua_tointeger(s,-1);
+
+ if (c->bufsize <= 0)
+ {
+ luaL_error(s,"invalid bufsize %d",c->bufsize);
+ }
+ }
+ else
+ {
+ free_conf(c);
+ luaL_error(s,"'bufsize' is not an integer");
+ }
+
+ lua_getfield(s,tbl,"callback");
+
+ if (lua_isnil(s,-1))
+ {
+ c->callb = LUA_NOREF;
+ }
+ else if (lua_isfunction(s,-1))
+ {
+ c->callb = luaL_ref(s,LUA_REGISTRYINDEX);
+ }
+ else
+ {
+ free_conf(c);
+ luaL_error(s,"'callback' is not a function");
+ }
+}
+
+static void setup_tty(struct termios *t, struct conf *c)
+{
+ cfsetispeed(t,c->baudflag);
+
+ switch (c->stopbits)
+ {
+ case 1: t->c_cflag &= ~CSTOPB; break;
+ case 2: t->c_cflag |= CSTOPB; break;
+ default: assert(0);
+ }
+
+ switch (c->parity)
+ {
+ case 'N':
+ case 'n':
+ t->c_cflag &= ~PARENB;
+ break;
+ case 'O':
+ case 'o':
+ t->c_cflag |= PARENB;
+ t->c_cflag |= PARODD;
+ break;
+ case 'E':
+ case 'e':
+ t->c_cflag |= PARENB;
+ t->c_cflag &= ~PARODD;
+ break;
+ default:
+ assert(0);
+ }
+
+ t->c_cflag &= ~CSIZE;
+
+ switch (c->datasize)
+ {
+ case 5: t->c_cflag |= CS5; break;
+ case 6: t->c_cflag |= CS6; break;
+ case 7: t->c_cflag |= CS7; break;
+ case 8: t->c_cflag |= CS8; break;
+ default: assert(0);
+ }
+
+ t->c_cflag |= CREAD|CLOCAL;
+ t->c_cflag &= ~CRTSCTS;
+ t->c_lflag &= ~(ICANON|ECHO|ISIG);
+ t->c_iflag &= ~(IXON|IXOFF|IXANY);
+ t->c_oflag &= ~OPOST;
+ t->c_cc[VMIN] = 1;
+ t->c_cc[VTIME] = 0;
+}
+
+static void proc(const ml_proc_args *args)
+{
+ struct serial *se = args->userp;
+ size_t len;
+
+ while (ml_ring_read(se->ring,&len,sizeof len) == sizeof len)
+ {
+ ml_ring_read_blocking(se->ring,se->tmpdata,len);
+
+ if (se->conf.callb != LUA_NOREF)
+ {
+ lua_pushlstring(se->state,(const char*)se->tmpdata,len);
+ int arg = luaL_ref(se->state,LUA_REGISTRYINDEX);
+ ml_schedule_event(0,se->conf.callb,arg);
+ }
+ }
+}
+
+static int l_open(lua_State *s)
+{
+ luaL_checktype(s,1,LUA_TTABLE);
+
+ struct conf conf = {0};
+
+ read_conf(s,1,&conf);
+
+ int fd = open(conf.path,O_RDONLY|O_NOCTTY);
+
+ if (fd < 0)
+ {
+ luaL_error(s,"failed to open %s: %s",conf.path,strerror(errno));
+ }
+
+ struct termios tty = {0};
+ tcgetattr(fd,&tty);
+
+ setup_tty(&tty,&conf);
+
+ tcsetattr(fd,TCSANOW,&tty);
+ tcflush(fd,TCIFLUSH);
+
+ struct serial *se = calloc(1,sizeof(*se));
+ se->fd = fd;
+ se->conf = conf;
+ se->ring = ml_ring_init(conf.bufsize);
+ se->state = s;
+
+ *(void**) lua_newuserdata(s,sizeof(void**)) = se;
+ luaL_setmetatable(s,SERIAL_TYPENAME);
+
+ ml_add_io_handler(fd,POLLIN,io_handler,se);
+
+ ml_node *node = ml_create_node(proc,se);
+ ml_channel *src = ml_add_output(node);
+ ml_channel *dst = ml_add_input(ml_get_root_node());
+ ml_connect(src,dst);
+ ml_label_node(node,"serial %s",conf.path);
+
+ return 1;
+}
+
+static int read_byte(struct serial *se)
+{
+ uint8_t c;
+
+ if (read(se->fd,&c,sizeof c) < sizeof c)
+ {
+ return -1;
+ }
+
+ return c;
+}
+
+static int sync_(struct serial *se)
+{
+ int c;
+
+ if (se->synced)
+ {
+ return 0;
+ }
+
+ while ((c = read_byte(se)) != 0)
+ {
+ if (c < 0)
+ {
+ return -1;
+ }
+ }
+
+ se->synced = 1;
+ return 0;
+}
+
+static int cstring(struct serial *se)
+{
+ if (sync_(se) < 0)
+ {
+ return -1;
+ }
+
+ int c;
+
+ while ((c = read_byte(se)) != 0)
+ {
+ if (c < 0)
+ {
+ se->synced = 0;
+ return -1;
+ }
+
+ put_byte(se,c);
+ }
+
+ return 0;
+}
+
+int luaopen_melisma_serial(lua_State *s)
+{
+/*** Read data on serial ports.
+
+ local mserial = require 'melisma.serial'
+
+ mserial.open {
+ path = '/dev/ttyACM0',
+ callback = print,
+ }
+
+@classmod serial
+*/
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+/*** Open a serial port.
+
+`conf` is a table containing the following keys:
+
+- `path` (**string, required**): Path to serial port (e.g. `/dev/ttyACM0`).
+- `callback` (**function, required**): Callback function for handling messages.
+- `baud` (**integer**); baud rate (default `9600`).
+- `format` (**string**): frame format (default `'8N1'`).
+- `encoding` (**string**): method for encoding messages (default `'cstring'`). May be one of:
+ - `'cstring'`: `NULL`-terminated strings.
+
+@static
+@function open
+@tparam table conf
+@treturn serial
+*/
+ lua_pushcfunction(s,l_open);
+ lua_setfield(s,tbl,"open");
+
+ luaL_newmetatable(s,SERIAL_TYPENAME);
+ int mt = lua_gettop(s);
+
+ lua_pushvalue(s,tbl);
+ lua_setfield(s,mt,"__index");
+
+ lua_pushvalue(s,tbl);
+ return 1;
+}
diff --git a/mod/spring.c b/mod/spring.c
new file mode 100644
index 0000000..a6f96ea
--- /dev/null
+++ b/mod/spring.c
@@ -0,0 +1,173 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <math.h>
+#include <lauxlib.h>
+#include "melisma.h"
+
+#define SPRING_TYPENAME "spring"
+
+struct spring
+{
+ ml_node *node;
+ float target;
+ float value;
+ float freq;
+ float damping;
+ float velocity;
+ bool mapped;
+};
+
+static void proc(const ml_proc_args *args)
+{
+ struct spring *spring = args->userp;
+
+ float *out = args->out[0];
+ float *mapped = spring->mapped ? args->in[0] : NULL;
+ float dt = 1.f/ml_sample_rate();
+
+ for (size_t i = 0; i < args->nframes; i++)
+ {
+ float target = mapped ? mapped[i] : spring->target;
+ float w = 2*M_PI*spring->freq;
+ float f = (target - spring->value)*w*w;
+ float d = 2*w*spring->velocity*spring->damping;
+ float a = f - d;
+
+ spring->velocity += a*dt;
+ spring->value += spring->velocity*dt;
+
+ out[i] = spring->value;
+ }
+}
+
+static int l_damping(lua_State *s)
+{
+ struct spring *spring = *(void**) luaL_checkudata(s,1,SPRING_TYPENAME);
+ float x = luaL_checknumber(s,2);
+ spring->damping = x;
+ return 0;
+}
+
+static int l_freq(lua_State *s)
+{
+ struct spring *spring = *(void**) luaL_checkudata(s,1,SPRING_TYPENAME);
+ float x = luaL_checknumber(s,2);
+ spring->freq = x;
+ return 0;
+}
+
+static int l_target(lua_State *s)
+{
+ struct spring *spring = *(void**) luaL_checkudata(s,1,SPRING_TYPENAME);
+ float x = luaL_checknumber(s,2);
+ spring->target = x;
+ return 0;
+}
+
+static int l_map(lua_State *s)
+{
+ struct spring *spring = *(void**) luaL_checkudata(s,1,SPRING_TYPENAME);
+
+ if (spring->mapped)
+ {
+ luaL_error(s,"already mapped");
+ }
+
+ ml_channel *src = *(void**) luaL_checkudata(s,2,MELISMA_OUTPUT_CHANNEL_TYPENAME);
+ ml_channel *dst = ml_add_input(spring->node);
+ ml_connect(src,dst);
+
+ spring->mapped = true;
+
+ return 0;
+}
+
+static int l_sample(lua_State *s)
+{
+ struct spring *spring = *(void**) luaL_checkudata(s,1,SPRING_TYPENAME);
+ lua_pushnumber(s,spring->value);
+ return 1;
+}
+
+static int l_call(lua_State *s)
+{
+ struct spring *spring = *(void**) luaL_checkudata(s,1,SPRING_TYPENAME);
+ *(void**) lua_newuserdata(s,sizeof(void*)) = ml_get_output(spring->node,0);
+ luaL_setmetatable(s,MELISMA_OUTPUT_CHANNEL_TYPENAME);
+ return 1;
+}
+
+static int l_init(lua_State *s)
+{
+ float value = luaL_checknumber(s,1);
+ float freq = luaL_checknumber(s,2);
+ float damping = luaL_checknumber(s,3);
+
+ struct spring *spring = calloc(1,sizeof(*spring));
+ spring->value = value;
+ spring->freq = freq;
+ spring->damping = damping;
+ spring->mapped = false;
+
+ spring->node = ml_create_node(proc,spring);
+ ml_add_output(spring->node);
+
+ *(void**) lua_newuserdata(s,sizeof(void*)) = spring;
+ luaL_setmetatable(s,SPRING_TYPENAME);
+
+ return 1;
+}
+
+int luaopen_melisma_spring(lua_State *s)
+{
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+ lua_pushcfunction(s,l_init);
+ lua_setfield(s,tbl,"init");
+
+ lua_pushcfunction(s,l_freq);
+ lua_setfield(s,tbl,"freq");
+
+ lua_pushcfunction(s,l_damping);
+ lua_setfield(s,tbl,"damping");
+
+ lua_pushcfunction(s,l_target);
+ lua_setfield(s,tbl,"target");
+
+ lua_pushcfunction(s,l_sample);
+ lua_setfield(s,tbl,"sample");
+
+ lua_pushcfunction(s,l_map);
+ lua_setfield(s,tbl,"map");
+
+ luaL_newmetatable(s,SPRING_TYPENAME);
+ int mt = lua_gettop(s);
+
+ lua_pushvalue(s,tbl);
+ lua_setfield(s,mt,"__index");
+
+ lua_pushcfunction(s,l_call);
+ lua_setfield(s,mt,"__call");
+
+ lua_settop(s,tbl);
+ return 1;
+}
diff --git a/mod/timer.c b/mod/timer.c
new file mode 100644
index 0000000..92659dd
--- /dev/null
+++ b/mod/timer.c
@@ -0,0 +1,152 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <lauxlib.h>
+#include "melisma.h"
+
+#define TIMER_TYPENAME "timer"
+
+struct timer
+{
+ bool on;
+ uint64_t t;
+};
+
+static void proc(const ml_proc_args *args)
+{
+ struct timer *tmr = args->userp;
+
+ if (tmr->on)
+ {
+ tmr->t += args->nframes;
+ }
+}
+
+static int l_init(lua_State *s)
+{
+ struct timer *tmr = calloc(1,sizeof(*tmr));
+
+ tmr->on = false;
+ tmr->t = 0;
+
+ ml_node *node = ml_create_node(proc,tmr);
+ ml_channel *src = ml_add_output(node);
+ ml_channel *dst = ml_add_input(ml_get_root_node());
+ ml_connect(src,dst);
+
+ *(void**) lua_newuserdata(s,sizeof(void*)) = tmr;
+ luaL_setmetatable(s,TIMER_TYPENAME);
+
+ return 1;
+}
+
+static int l_start(lua_State *s)
+{
+ struct timer *tmr = *(void**) luaL_checkudata(s,1,TIMER_TYPENAME);
+ tmr->on = true;
+ return 0;
+}
+
+static int l_stop(lua_State *s)
+{
+ struct timer *tmr = *(void**) luaL_checkudata(s,1,TIMER_TYPENAME);
+ tmr->on = false;
+ return 0;
+}
+
+static int l_reset(lua_State *s)
+{
+ struct timer *tmr = *(void**) luaL_checkudata(s,1,TIMER_TYPENAME);
+ tmr->t = 0;
+ return 0;
+}
+
+static int l_read(lua_State *s)
+{
+ struct timer *tmr = *(void**) luaL_checkudata(s,1,TIMER_TYPENAME);
+ double t = (double) tmr->t / (double) ml_sample_rate();
+ lua_pushnumber(s,t);
+ return 1;
+}
+
+int luaopen_melisma_timer(lua_State *s)
+{
+/*** Time measurement.
+
+ local mtimer = require 'melisma.timer'
+ local mkeycallback = require 'melisma.keycallback'
+
+ local timer = mtimer.init()
+ timer:start()
+
+ mkeycallback.set(function()
+ print(timer:read())
+ timer:reset()
+ end)
+
+@classmod timer
+*/
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+/*** Create a new timer.
+@static
+@function init
+@treturn timer
+*/
+ lua_pushcfunction(s,l_init);
+ lua_setfield(s,tbl,"init");
+
+/*** Start timer.
+@function start
+*/
+ lua_pushcfunction(s,l_start);
+ lua_setfield(s,tbl,"start");
+
+/*** Stop timer.
+@function stop
+*/
+ lua_pushcfunction(s,l_stop);
+ lua_setfield(s,tbl,"stop");
+
+/*** Reset timer.
+@function reset
+*/
+ lua_pushcfunction(s,l_reset);
+ lua_setfield(s,tbl,"reset");
+
+
+/*** Read timer.
+@function read
+@treturn number Elapsed seconds.
+*/
+ lua_pushcfunction(s,l_read);
+ lua_setfield(s,tbl,"read");
+
+ luaL_newmetatable(s,TIMER_TYPENAME);
+ int mt = lua_gettop(s);
+
+ lua_pushvalue(s,tbl);
+ lua_setfield(s,mt,"__index");
+
+ lua_settop(s,tbl);
+ return 1;
+}
diff --git a/mod/udp.c b/mod/udp.c
new file mode 100644
index 0000000..6448a56
--- /dev/null
+++ b/mod/udp.c
@@ -0,0 +1,391 @@
+/*
+ * melisma - extensible and scriptable audio engine
+ * Copyright (C) 2026 Ludvig Gunne Lindström
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <assert.h>
+
+#include <unistd.h>
+#include <sys/eventfd.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#include <lauxlib.h>
+
+#include "melisma.h"
+#include "melisma/ring.h"
+
+#define SOCKADDR_TYPENAME "sockaddr"
+#define UDP_SOCKET_TYPENAME "udp socket"
+#define UDP_PACKET_SIZE 65536
+
+struct udp_socket
+{
+ int fd;
+ int family;
+ int event;
+ int bound;
+ int callb;
+ lua_State *state;
+ ml_ring *recvring;
+ ml_ring *sendring;
+ uint8_t pollbuf[UDP_PACKET_SIZE];
+ uint8_t procbuf[UDP_PACKET_SIZE];
+};
+
+// TODO: CHECK BUFFER SIZES FOR ADDRESSES
+
+static int l_sockaddr_unix(lua_State *s)
+{
+ size_t len;
+ const char *path = luaL_checklstring(s,1,&len);
+
+ struct sockaddr_storage *sa = lua_newuserdata(s,sizeof *sa);
+ luaL_setmetatable(s,SOCKADDR_TYPENAME);
+ memset(sa,0,sizeof *sa);
+
+ ((struct sockaddr_un*)sa)->sun_family = AF_UNIX;
+ memcpy(((struct sockaddr_un*)sa)->sun_path,path,len);
+
+ return 1;
+}
+
+static int l_sockaddr_inet(lua_State *s)
+{
+ const char *addr = luaL_checkstring(s,1);
+ int port = luaL_checkinteger(s,2);
+
+ struct in_addr in_addr;
+ if (inet_pton(AF_INET,addr,&in_addr) != 1)
+ {
+ luaL_error(s,"invalid ipv4 address '%s'",addr);
+ }
+
+ struct sockaddr_storage *sa = lua_newuserdata(s,sizeof *sa);
+ luaL_setmetatable(s,SOCKADDR_TYPENAME);
+ memset(sa,0,sizeof *sa);
+
+ ((struct sockaddr_in*)sa)->sin_family = AF_INET;
+ ((struct sockaddr_in*)sa)->sin_port = htons(port);
+ memcpy(&((struct sockaddr_in*)sa)->sin_addr,&in_addr,sizeof in_addr);
+
+ return 1;
+}
+
+static void recv_handler(int fd, int flags, void *args)
+{
+ struct udp_socket *us = args;
+ ssize_t len = recvfrom(fd,us->pollbuf,sizeof us->pollbuf,0,NULL,NULL);
+
+ if (len < 0)
+ {
+ perror("recvfrom");
+ return;
+ }
+
+ if (!ml_ring_has_space(us->recvring,len + sizeof len))
+ {
+ fprintf(stderr,"dropped udp packet");
+ }
+
+ ml_ring_write_blocking(us->recvring,&len,sizeof len);
+ ml_ring_write_blocking(us->recvring,us->pollbuf,len);
+}
+
+static void send_handler(int fd, int flags, void *args)
+{
+ struct udp_socket *us = args;
+
+ size_t sa_len,len;
+ struct sockaddr_storage storage = {0};
+
+ ml_ring_read_blocking(us->sendring,&sa_len,sizeof sa_len);
+ ml_ring_read_blocking(us->sendring,&storage,sa_len);
+ ml_ring_read_blocking(us->sendring,&len,sizeof len);
+ ml_ring_read_blocking(us->sendring,us->pollbuf,len);
+
+ if (sendto(us->fd,us->pollbuf,len,0,(struct sockaddr*) &storage,sa_len) < 0)
+ {
+ perror("sendto");
+ }
+
+ uint64_t x;
+ if (read(us->event,&x,sizeof x) != sizeof x)
+ ;
+}
+
+static int l_socket(lua_State *s)
+{
+ int family = lua_tointeger(s,lua_upvalueindex(1));
+ int fd = socket(family,SOCK_DGRAM,0);
+
+ if (fd < 0)
+ {
+ const char *err = strerror(errno);
+ luaL_error(s,"failed to create socket: %s",err);
+ }
+
+ struct udp_socket *us = calloc(1,sizeof(*us));
+ us->fd = fd;
+ us->family = family;
+ us->sendring = ml_ring_init(UDP_PACKET_SIZE);
+ us->event = eventfd(0,EFD_SEMAPHORE);
+
+ ml_add_io_handler(us->event,POLLIN,send_handler,us);
+
+ *(struct udp_socket**) lua_newuserdata(s,sizeof(struct udp_socket*)) = us;
+ luaL_setmetatable(s,UDP_SOCKET_TYPENAME);
+
+ return 1;
+}
+
+static const char *family_name(int family)
+{
+ switch (family)
+ {
+ case AF_UNIX: return "unix";
+ case AF_INET: return "inet";
+ default: assert(0);
+ }
+}
+
+static void proc(const ml_proc_args *args)
+{
+ struct udp_socket *us = args->userp;
+ ssize_t len;
+
+ while (!ml_ring_empty(us->recvring))
+ {
+ ml_ring_read_blocking(us->recvring,&len,sizeof len);
+ ml_ring_read_blocking(us->recvring,us->procbuf,len);
+
+ lua_pushlstring(us->state,(const char*)us->procbuf,len);
+ int arg = luaL_ref(us->state,LUA_REGISTRYINDEX);
+
+ ml_schedule_event(0,us->callb,arg);
+ }
+}
+
+static size_t get_sockaddr_len(struct sockaddr *sa)
+{
+ if (sa->sa_family == AF_UNIX)
+ {
+ const char *path = ((struct sockaddr_un*)sa)->sun_path;
+
+ if (path[0] == 0 && path[1] != 0)
+ {
+ return offsetof(struct sockaddr_un,sun_path) + strlen(path+1) + 1;
+ }
+
+ return offsetof(struct sockaddr_un,sun_path) + strlen(path);
+ }
+ else
+ {
+ return sizeof(struct sockaddr_in);
+ }
+}
+
+static void check_family(lua_State *s, struct udp_socket *us, struct sockaddr *sa)
+{
+ if (us->family != sa->sa_family)
+ {
+ luaL_error(s,"sockaddr address family (%s) does not match socket address family (%s)",
+ family_name(sa->sa_family), family_name(us->family));
+ }
+}
+
+static int l_bind(lua_State *s)
+{
+ struct udp_socket *us = *(struct udp_socket**) luaL_checkudata(s,1,UDP_SOCKET_TYPENAME);
+ struct sockaddr *sa = luaL_checkudata(s,2,SOCKADDR_TYPENAME);
+
+ luaL_checktype(s,3,LUA_TFUNCTION);
+
+ if (us->bound)
+ {
+ luaL_error(s,"socket is already bound");
+ }
+
+ check_family(s,us,sa);
+
+ size_t len = get_sockaddr_len(sa);
+
+ if (bind(us->fd,sa,len) < 0)
+ {
+ const char *err = strerror(errno);
+ luaL_error(s,"failed to bind socket: %s",err);
+ }
+
+ us->bound = 1;
+ us->recvring = ml_ring_init(UDP_PACKET_SIZE);
+ us->state = s;
+
+ ml_add_io_handler(us->fd,POLLIN,recv_handler,us);
+
+ lua_pushvalue(s,3);
+ us->callb = luaL_ref(s,LUA_REGISTRYINDEX);
+
+ ml_node *node = ml_create_node(proc,us);
+ ml_channel *src = ml_add_output(node);
+ ml_channel *dst = ml_add_input(ml_get_root_node());
+ ml_connect(src,dst);
+
+ return 0;
+}
+
+static int l_send(lua_State *s)
+{
+ struct udp_socket *us = *(struct udp_socket**) luaL_checkudata(s,1,UDP_SOCKET_TYPENAME);
+ struct sockaddr *sa = luaL_checkudata(s,2,SOCKADDR_TYPENAME);
+ size_t len;
+ const char *data = luaL_checklstring(s,3,&len);
+
+ check_family(s,us,sa);
+
+ size_t sa_len = get_sockaddr_len(sa);
+ size_t full_len = sizeof len + len + sizeof sa_len + sa_len;
+
+ if (!ml_ring_has_space(us->sendring,full_len))
+ {
+ lua_pushboolean(s,0);
+ return 1;
+ }
+
+ ml_ring_write_blocking(us->sendring,&sa_len,sizeof sa_len);
+ ml_ring_write_blocking(us->sendring,sa,sa_len);
+ ml_ring_write_blocking(us->sendring,&len,sizeof len);
+ ml_ring_write_blocking(us->sendring,data,len);
+
+ uint64_t x = 1;
+ if (write(us->event,&x,sizeof x) < sizeof x)
+ ;
+
+ lua_pushboolean(s,1);
+ return 1;
+}
+
+int luaopen_melisma_udp(lua_State *s)
+{
+ lua_newtable(s);
+ int tbl = lua_gettop(s);
+
+/*** UDP support.
+
+ local mudp = require 'melisma.udp'
+
+ local addr = mudp.sockaddr.inet('127.0.0.1',8000)
+ local sock = mudp.socket.inet()
+
+ sock:send(addr,'hello')
+ sock:bind(addr,print)
+
+@module udp
+*/
+
+/*** Socket address description.
+@type sockaddr
+*/
+ lua_newtable(s);
+ int sockaddr_tbl = lua_gettop(s);
+
+/*** Create a unix domain address description.
+@static
+@function unix
+@tparam string path
+@treturn sockaddr
+*/
+ lua_pushcfunction(s,l_sockaddr_unix);
+ lua_setfield(s,sockaddr_tbl,"unix");
+
+
+/*** Create a internet domain address description.
+@static
+@function inet
+@tparam string host
+@tparam integer port
+@treturn sockaddr
+*/
+ lua_pushcfunction(s,l_sockaddr_inet);
+ lua_setfield(s,sockaddr_tbl,"inet");
+
+ lua_pushvalue(s,sockaddr_tbl);
+ lua_setfield(s,tbl,"sockaddr");
+
+ luaL_newmetatable(s,SOCKADDR_TYPENAME);
+ int sockaddr_mt = lua_gettop(s);
+
+ lua_pushvalue(s,sockaddr_tbl);
+ lua_setfield(s,sockaddr_mt,"__index");
+
+/*** Handle to a socket.
+@type socket
+*/
+
+ lua_newtable(s);
+ int socket_tbl = lua_gettop(s);
+
+/*** Create a unix domain socket.
+@static
+@function unix
+@treturn socket
+*/
+ lua_pushinteger(s,AF_UNIX);
+ lua_pushcclosure(s,l_socket,1);
+ lua_setfield(s,socket_tbl,"unix");
+
+
+/*** Create an internet domain socket.
+@static
+@function inet
+@treturn socket
+*/
+ lua_pushinteger(s,AF_INET);
+ lua_pushcclosure(s,l_socket,1);
+ lua_setfield(s,socket_tbl,"inet");
+
+/*** Bind a socket to an address.
+@function bind
+@tparam sockaddr addr Address description.
+@tparam function fn Packet callback function.
+*/
+ lua_pushcfunction(s,l_bind);
+ lua_setfield(s,socket_tbl,"bind");
+
+/*** Send a packet on a socket.
+@function send
+@tparam sockaddr addr Address description.
+@tparam string pkt Packet to send.
+*/
+ lua_pushcfunction(s,l_send);
+ lua_setfield(s,socket_tbl,"send");
+
+ lua_pushvalue(s,socket_tbl);
+ lua_setfield(s,tbl,"socket");
+
+ luaL_newmetatable(s,UDP_SOCKET_TYPENAME);
+ int socket_mt = lua_gettop(s);
+
+ lua_pushvalue(s,socket_tbl);
+ lua_setfield(s,socket_mt,"__index");
+
+ lua_settop(s,tbl);
+ return 1;
+}