https://github.com/krasjet/usporth
a minimal rewrite of sporth core, a small stack-based audio programming language
https://github.com/krasjet/usporth
audio c forth programming-language
Last synced: 6 months ago
JSON representation
a minimal rewrite of sporth core, a small stack-based audio programming language
- Host: GitHub
- URL: https://github.com/krasjet/usporth
- Owner: Krasjet
- Created: 2021-10-10T10:00:47.000Z (over 4 years ago)
- Default Branch: master
- Last Pushed: 2021-12-26T08:15:41.000Z (about 4 years ago)
- Last Synced: 2025-03-15T20:14:39.259Z (11 months ago)
- Topics: audio, c, forth, programming-language
- Language: C
- Homepage:
- Size: 169 KB
- Stars: 1
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README
Awesome Lists containing this project
README
μsporth
=======
A minimal rewrite of the core of Paul Batchelor's Sporth [1],
a small stack-based audio programming language.
The main goal of μsporth is to create a self-contained audio
programming language that is actually possible to extend and
comprehend completely within one or two hours, given some C
programming background.
The stack-based approach of Sporth is great for this goal,
but the original implementation is a bit convoluted and it
is mainly a wrapper of Soundpipe for personal use. It's not
straightforward to make it your own. Though luckily, the
concept of Sporth is so simple that many parts can be
greatly simplified if you rethink it from scratch.
μsporth implements most of the core features of Sporth in
around 500 lines of C (`usporth.c` + `usporth.h`). It is
probably even more extensible since the DSP part is now
completely independent of the IO part. This is why multiple
interfaces to μsporth could be implemented as independent
standalone programs:
usporth_text: text interface, mainly for debugging
usporth_jack: real-time audio interface via JACK
usporth_wav: offline rendering interface via libsndfile
μsporth doesn't include all the unit generators in Sporth.
Existing ugens are mostly to help developing and testing new
ugens and to demonstrate how you can extend and use the
language. You can treat it as a "bring your own ugens"
version of Sporth, a blank slate for you to implement your
own language, also a great playground to experiment with DSP
algorithms in C.
How to read
-----------
To understand the implementation of μsporth, I suggest you
start with "The Stack" section of the Sporth Cookbook
https://web.archive.org/web/20211015023825/https://pbat.ch/proj/cook/stack.html
Notice that the `sine` ugen of μsporth doesn't include the
amplitude parameter, so
Sporth μsporth
440 0.5 sine <=> 440 sine 0.5 *
Syntax-wise, μsporth is equivalent to Sporth, possibly
slightly more flexible since it allows numbers without
leading zeros. There are only three types of elements in
μsporth source code
# numbers (floats)
1.23 -1.23 .23
# strings
_word "is equivalent to" 'string without spaces'
# ugens
sine + - * /
Each element is compiled into a "pipe",
usp_pipe pipes = usporth_eval("440 sine 0.5 *");
440 sine 0.5 *
=>
+f----+ +u-----+ +f----+ +u--+
| 440 +=>| sine +=>| 0.5 +=>| * +=>
+-----+ +------+ +-----+ +---+
which is an instance of the element, with its own internal
states, that can pump out and comsume values on the stack in
a sequential, streaming fashion. We call a sequence of pipes
a "pipeline".
Given a pipeline, we can evaluate all the pipes in the
pipeline sequentially using
pipes_init(&ctx, pipes); /* init all pipes */
pipes_tick(&ctx, pipes); /* compute a sample */
pipes_free(pipes); /* clean up all pipes */
where `ctx` is the runtime context of μsporth, which holds
the data that need to be shared among all ugens, such as the
stack and the sampling rate. It can be initialized using
usp_ctx ctx;
usporth_init_ctx(&ctx, sr);
To get a full picture of the μsporth API, `main_text.c` is a
great place to start. Then follow the implementation of
these five functions to understand the rest of the
implementation.
Adding a new ugen
-----------------
To add a new unit generator, e.g. inv (this one is already
implemented now),
1. Create `inv.c` in `ugens` directory.
2. Define `init`, `tick`, and `free` functions for the new
ugen. The name and prototype of the functions must match
the following pattern:
ugen_status ugen_inv_init(usp_ctx *ctx, ugen_instance *pugen)
ugen_status ugen_inv_tick(usp_ctx *ctx, ugen_instance ugen)
void ugen_inv_free(ugen_instance ugen)
`ugen_instance` is how the ugen keeps an internal state.
It is similar to LADSPA's `LADSPA_Handle` [2] or LV2's
`LV2_Handle` [3].
If the ugen needs an internal state, you need to store it
in `pugen` during the init phase. Then the state will be
available to you via the `ugen` argument during the tick
and free phase. See existing implementations in `ugens`
directory for some examples. I recommend starting with
`sine.c`.
I also recommend checking out `ftgen.c`. It shows you how
to use "extension data", which is how custom data can be
shared between different ugens. It also demonstrate two
important quirks:
1. String arguments are only pushed onto the stack in
init phase, not in tick phase.
2. In the `free` function, you must make sure the
program doesn't crash when `ugen` is NULL.
3. Add documentation to `ugens.lua`, e.g.
['inv'] = {
input = {
{name = 'v', type = 'f', cond = 'x != 0'},
},
output = {
{name = 'inv', type = 'f'},
},
description = 'compute 1 / v'
},
to the `ugens` table.
4. Run
$ lua ./ugens.lua
to regenerate `ugens.h`. You need to have lua installed
for this.
5. Update the makefile to include the new object file
`ugens/inv.o` to `OBJ` and add header dependency
ugens/inv.o: usporth.h
This step might be automated in the future.
Usage
-----
JACK interface:
$ usporth_jack in.usp
Text interface:
$ usporth_text [-s] [-n nsamples] [-r sr] in.usp
$ usporth_text [-s] [-n nsamples] [-r sr] in.usp < in.txt
Examples:
$ usporth_text -s -n 20 -r 48000 examples/sine.usp
$ echo '1 2 3' | usporth_text -n 3 examples/in.usp
The output can be directly piped into gnuplot using
$ gnuplot -p -e "plot '< cat -' w lines"
WAV interface:
$ usporth_wav [-t nsecs] [-r sr] [-o out.wav] in.usp
$ usporth_wav [-t nsecs] -i in.wav [-o out.wav] in.usp
Examples:
$ usporth_wav -t 3 -r 8000 -o sine.wav examples/sine.usp
$ usporth_wav -i sine.wav -o sine_out.wav examples/in.usp
See the man pages for details.
Build
-----
To build the JACK interface, you need JACK [4] installed on
your system, and to build the WAV interface, you need
libsndfile [5] installed. If you want to add new ugens, you
also need to have lua installed.
The text interface only requires a C99-compatible C compiler
to build.
For Arch-based distros, you can install all the optional
dependencies by
$ pacman -S jack2 libsndfile lua
For Debian-based distros, install by
$ apt-get install libjack-jackd2-dev libsndfile1-dev lua5.1
For FreeBSD, install by
$ pkg install pkgconf audio/jack libsndfile lua51
For macOS, you can use Homebrew [6]:
$ brew install pkgconf jack libsndfile lua
For Windows, you can use MSYS2 with MinGW [7]:
$ pacman -S pkg-config mingw-w64-x86_64-libsndfile mingw-w64-x86_64-lua
If you want to compile the JACK interface, you need to
build JACK from source:
$ pacman -S git base-devel python mingw-w64-x86_64-toolchain
$ git clone git://github.com/jackaudio/jack2.git
$ cd jack2
$ ./waf configure \
--prefix=${MINGW_PREFIX} \
--check-c-compiler=gcc \
--check-cxx-compiler=g++
$ ./waf build
$ ./waf install
After you have sorted out the dependencies, use
$ ./configure
to generate a `config.mk` file containing compiler flags,
and run
$ make
to build the programs. If you want to install μsporth to
your system, use
$ make install
or you can specify a destination with
$ make PREFIX=~/.local install
Run
$ make uninstall
to uninstall μsporth.
[1]: https://web.archive.org/web/20211117190245/https://pbat.ch/proj/sporth.html
[2]: https://www.ladspa.org/
[3]: https://lv2plug.in/
[4]: https://jackaudio.org/
[5]: https://libsndfile.github.io/libsndfile/
[6]: https://brew.sh/
[7]: https://www.msys2.org/