aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Makefile5
-rw-r--r--README.md11
-rw-r--r--TODO43
-rw-r--r--applause.lua1018
-rw-r--r--config.ld33
-rw-r--r--dssi.lua86
-rw-r--r--evdev.lua82
-rw-r--r--filters.lua186
-rw-r--r--midi.lua168
-rw-r--r--sndfile-stream.lua46
-rw-r--r--sndfile.lua4
12 files changed, 1369 insertions, 314 deletions
diff --git a/.gitignore b/.gitignore
index bd77a2f..ffaea07 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
*.o
/.applause_history
+/doc \ No newline at end of file
diff --git a/Makefile b/Makefile
index d91b265..a2fbffa 100644
--- a/Makefile
+++ b/Makefile
@@ -19,10 +19,15 @@ LDFLAGS += $(LUA_LDFLAGS) $(READLINE_LDFLAGS) $(JACK_LDFLAGS) \
# with the LuaJIT FFI interface:
LDFLAGS += -rdynamic
+.PHONY: all doc
+
all : applause
applause : applause.o evdev.o
$(CC) -o $@ $^ $(LDFLAGS)
+doc:
+ ldoc .
+
clean:
$(RM) *.o applause
diff --git a/README.md b/README.md
index bd81e57..685efd3 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,17 @@ Furthermore, install the following dependencies:
sudo apt-get install build-essential libreadline-dev libjack-jackd2-dev \
libsndfile1 libasound2 feedgnuplot
+To compile the project, type:
+
+ make
+
+Up-to-date documentation is available at the [website](http://rhaberkorn.github.io/applause2).
+In case you want to build it manually, install the `lua-ldoc` and `lua-discount` packages and type:
+
+ make doc
+
+The generated documentation will be generated in the `doc/` subdirectory.
+
## Usage
Start qjackctl.
diff --git a/TODO b/TODO
index faf5d3e..619459e 100644
--- a/TODO
+++ b/TODO
@@ -1,34 +1,54 @@
# Bugs
* Stream:foreach() cannot be interrupted
- Perhaps C core should export an interrupted variable that we can check from Lua.i
+ Perhaps C core should export an interrupted variable that we can check from Lua.
For Stream:play() this is solved differently.
+* Stream:gtick() is not real-time safe but called from tick functions currently.
+ This could be resolved by letting gtick() return an initializer function and only
+ this initializer function will return the tick function.
+ Streams could implement a get_tick_init() method and Stream:gtick() would continue
+ to exist as a shortcut.
* EvdevStream("TrackPoint"):evrel('REL_X'):scale(440,880):SinOsc():play()
Afterwards even Stream.SinOsc(880):play() will stutter.
- However, this seems to happen only with the builtin speakers.
+ However, this seems to happen only with the builtin speakers and with other programs as well.
# Features
* Line Continuations on the CLI (like Lua's CLI)
-* CLI auto completions via libreadline
+* CLI auto completions via libreadline.
+ This could directly introspect the Stream object for instance.
+ All of this should be ported to Lua code naturally.
* OSC support
* Audio input (see inputstream branch)
* File writing during live playback to avoid having to use jack_rec.
+ Stream:save() could be adapted and an immediate save could be achieved using a Stream:drain() method
+ or with a Stream:savenow() shortcut.
* Multi-core support via GStreamer-like queue:
https://gstreamer.freedesktop.org/data/doc/gstreamer/head/gstreamer-plugins/html/gstreamer-plugins-queue.html
* Stream optimizer
* Automatic caching (finding identical subtrees)
* Automatic paralellization
+ * Automatic eager evaluation of small Streams independant of real-time input.
+ * Resolving Stream:sub() by alternative means, e.g. specifying an osciallator phase or
+ passing in a seek to a SndfileStream.
* ...
-* Documentation.
- Either we use LDoc or manually write a Markdown file.
+* Github pages (LDoc documentation).
+ They can be automatically pushed by a Github action.
+ Since they will always be public, copyright questions should be resolved first.
# Improvements
-* add optional Stream:gtickCtx() containing potentially real-time unsafe code.
+* add optional Stream:gtickCtx() (or Stream:get_tick_init()) containing potentially real-time unsafe code.
Higher-order streams call Stream:gtickCtx() for all dependant streams only
once in their own gtickCtx() function.
This will allow gtick() to be called in tick-functions (that must be real-time safe).
+* It could be useful to pass a timestamp (in samples) into the tick function.
+ This will simplify some calculations and allows resetting a stream elegantly (for instance in RepeatStream).
+ It will also speed up seeks, as in SubStream.
+ IndexStream could be without memory limits.
+ Unfortunately, it will not resolve all non-realtime-safe gtick-invocations.
+ It's also questionable what happens when the timestamp wraps. Whether a wrap is safe or not, will depend
+ on the generator.
* Inconsistent signal normalization.
Some signals like Stream:CC() are not normalized to [-1,1], so you need
special scaling methods like Stream:ccscale().
@@ -37,8 +57,9 @@
On the other hand, with normalized outputs, you could also write Stram:mul(Stream:CC(...):scale(1)).
Or there could even be a Stream:vol() method that takes signals between [-1,1].
The question is whether the JIT compiler is smart enough to optimize this code.
-* Does the compiler currently produce SIMD instructions?
- Perhaps we need blockwise processing to faciliate those optimizations.
- Unfortunately, this would complicate all of the code.
- Unless we could define a block with metamethods to allow basic arithmetics to be
- performed just like on scalars.
+* The JIT compiler currently does not emit SIMD instructions (see simd-test.lua).
+ See also https://github.com/LuaJIT/LuaJIT/issues/40
+* Perhaps always enable Fused multiply-add (-O+fma).
+ The reduced accuracy shouldn't hurt us much.
+* Allow building LuaJIT with -DLUAJIT_ENABLE_LUA52COMPAT.
+ This will enable #Stream and we may implement an __ipairs metamethod.
diff --git a/applause.lua b/applause.lua
index 5b8e87e..08d0415 100644
--- a/applause.lua
+++ b/applause.lua
@@ -1,3 +1,9 @@
+---
+--- This "module" lists all symbols available on a REPL prompt.
+--- It does not have to and cannot be externally included using `require`.
+--- @module applause
+--- @author Robin Haberkorn
+---
local bit = require "bit"
local ffi = require "ffi"
-- According to LuaJIT docs, it makes sense to cache
@@ -54,8 +60,9 @@ int clock_gettime(clockid_t clk_id, struct timespec *tp);
void free(void *ptr);
]]
--- Measure time required to execute fnc()
--- See also Stream:benchmark()
+--- Measure time required to execute fnc()
+-- @func fnc Function to benchmark
+-- @see Stream:benchmark
function benchmark(fnc)
local t1 = ffi.new("struct timespec[1]")
local t2 = ffi.new("struct timespec[1]")
@@ -78,29 +85,48 @@ function benchmark(fnc)
print("Elapsed CPU time: "..tonumber(t2_ms - t1_ms).."ms")
end
--- Sample rate
--- This is overwritten by the C core
+--- Sample rate in Hz.
+-- This variable is overwritten by the C core.
samplerate = 44100
--- Time units: Convert between time and sample numbers
+--- Convert seconds to sample numbers
-- These are functions, so we can round the result
--- automatically
+-- automatically.
+-- @number[opt=1] x Number of seconds
+-- @treturn int Number of samples
function sec(x) return math.floor(samplerate*(x or 1)) end
+--- Convert milliseconds to sample numbers.
+-- @number[opt=1] x Number of milliseconds
+-- @treturn int Number of samples
function msec(x) return sec((x or 1)/1000) end
--- The sample cache used to implement CachedStream.
+--- The sample cache.
+-- This maps @{Stream} objects to arbitrary values (usually numbers) and
+-- can be used by Stream implementations to avoid recalculations during a single tick.
+-- It is cleared after every tick.
+-- You usually **don't** have to access this table manually, but *should* use
+-- @{Stream:cache} instead.
+-- This is only seldom useful when implementing new Stream classes.
+--
-- We don't know how large it must be, but once it is
-- allocated we only table.clear() it.
sampleCache = {}
--- Reload the main module: Useful for hacking it without
--- restarting applause
+--- Reload the main module.
+-- Useful for hacking it without having to restart the application.
+-- @local
function reload()
dofile "applause.lua"
collectgarbage()
end
--- FIXME: Inconsistent naming. Use all-lower case for functions
+--- Derive class
+-- @tparam[opt] Class base
+-- Base class.
+-- This should usually be @{Stream} or @{MuxableStream} when deriving custom Stream classes.
+-- @return Derived class table
+--
+-- @fixme Inconsistent naming. Use all-lower case for functions
-- and methods?
function DeriveClass(base)
local class = {base = base}
@@ -170,13 +196,39 @@ function DeriveClass(base)
return class
end
--- Stream base class
+--- Stream base class.
+-- This can be instantiated to generate plain values (although you should usually
+-- use @{tostream} instead).
+-- This class is also important to derive from in order to implement custom Streams.
+-- Most importantly, it defines all common Stream operations.
+-- @type Stream
Stream = DeriveClass()
+--- Create a stream for generating values.
+-- The stream will produce the same value in every tick.
+-- @within Class Stream
+-- @fixme The @within tag is necessary to fix later additions to the Stream class
+-- after the end of the section.
+-- @function Stream:new
+-- @param[opt=0] value Value to generate
+-- @treturn Stream
+-- @see tostream
+-- @usage Stream:new(23)
+-- @usage Stream(23)
function Stream:ctor(value)
+ -- @fixme Why does this convert everything to a number?
self.value = tonumber(value) or 0
end
+--- Stream constructor.
+-- This is an **abstract** method that must be implemented when deriving custom classes.
+-- It is never invoked directly - use the corresponding @{Stream:new} method instead.
+-- @function ctor
+-- @param ... Arbitrary parameters, passed on from the @{Stream:new} method.
+-- @treturn ?Stream
+-- You can optionally return a @{Stream} instance to competely replace the object table.
+-- If given, this value will be returned by the @{Stream:new} method instead.
+
-- There is Stream:instanceof(), but testing Stream.is_a_stream
-- is sometimes faster (for generator functions) and can be done
-- without knowing that the table at hand is an object
@@ -187,6 +239,11 @@ Stream.channels = 1
-- A stream, produces an infinite number of the same value by default
-- (eternal quietness by default)
+-- @fixme This should probably be renamed and return a function in a function,
+-- so you can do non-realtime safe stuff in Stream:gtick and real-time
+-- safe initialization in the returned function.
+-- This would allow "resetting" dependant streams from real-time safe tick
+-- functions.
function Stream:gtick()
local value = self.value
@@ -195,28 +252,102 @@ function Stream:gtick()
end
end
--- Cache this stream value to avoid recalculation within
--- the same tick (ie. point in time). This may happen when
--- a stream is used multiple times in the same "patch".
--- FIXME: This should be done automatically by an optimizer stage.
--- FIXME: This is counter-productive for simple number streams
--- (anything simpler than a table lookup)
-function Stream:cache()
- return CachedStream:new(self)
-end
-
-function Stream:rep(repeats)
- return RepeatStream:new(self, repeats)
-end
-
-function Stream:map(fnc)
- return MapStream:new(self, fnc)
-end
-Stream["\u{00A8}"] = Stream.map -- APL Double-Dot
-
-- Register all unary functions from the math package
-- as stream operations/methods (creates a stream that calls
-- the function on every sample)
+-- FIXME: There is actually no need to do this using a loop anymore
+-- since we document every method individually anyway.
+
+--- Get absolute value of all samples
+-- @function abs
+-- @treturn Stream
+-- @see math.abs
+
+--- Get arc cosine of all samples (in radians)
+-- @function acos
+-- @treturn Stream
+-- @see math.acos
+
+--- Get the arc sine of all samples (in radians)
+-- @function asin
+-- @treturn Stream
+-- @see math.asin
+
+--- Get the arc tangent of all samples (in radians)
+-- @function atan
+-- @treturn Stream
+-- @see math.atan
+
+--- Get the smallest integer larger than or equal to all samples
+-- @function ceil
+-- @treturn Stream
+-- @see math.ceil
+
+--- Get the cosine of all samples (assumed to be in radians)
+-- @function cos
+-- @treturn Stream
+-- @see math.cos
+
+--- Get the hyperbolic cosine of all samples
+-- @function cosh
+-- @treturn Stream
+-- @see math.cosh
+
+--- Get the angle of all samples (given in radians) in degrees
+-- @function deg
+-- @treturn Stream
+-- @see math.deg
+
+--- Get the value e^^x for all samples
+-- @function exp
+-- @treturn Stream
+-- @see math.exp
+
+--- Get the largest integer smaller than or equal to all samples
+-- @function floor
+-- @treturn Stream
+-- @see math.floor
+
+--- Get the natural logarithm of all samples
+-- @function log
+-- @treturn Stream
+-- @see math.log
+
+--- Get the base-10 logarithm of all samples
+-- @function log10
+-- @treturn Stream
+-- @see math.log10
+
+--- Get the angle of all samples (given in degrees) in radians
+-- @function rad
+-- @treturn Stream
+-- @see math.rad
+
+--- Get the sine of all samples (assumed to be in radians).
+-- @function sin
+-- @treturn Stream
+-- @see math.sin
+
+--- Get the hyperbolic sine of all samples.
+-- @function sinh
+-- @treturn Stream
+-- @see math.sinh
+
+--- Get the square root of all samples.
+-- (You can also use the expression x^^0.5 to compute this value.)
+-- @function sqrt
+-- @treturn Stream
+-- @see math.sqrt
+
+--- Get the tangent of all samples (assumed to be in radians)
+-- @function tan
+-- @treturn Stream
+-- @see math.tan
+
+--- Get the hyperbolic tangent of all samples
+-- @function tanh
+-- @treturn Stream
+-- @see math.tanh
for _, fnc in pairs{"abs", "acos", "asin", "atan",
"ceil", "cos", "cosh", "deg",
"exp", "floor", "log", "log10",
@@ -227,7 +358,21 @@ for _, fnc in pairs{"abs", "acos", "asin", "atan",
end
end
+--
-- Some binary functions from the math package
+--
+
+--- Returns the minimum value between two streams
+-- @function min
+-- @StreamableNumber v Serves as the right argument to @{math.min}.
+-- @treturn Stream
+-- @see ZipStream
+
+--- Returns the maximum value between two streams
+-- @function max
+-- @StreamableNumber v Serves as the right argument to @{math.max}.
+-- @treturn Stream
+-- @see ZipStream
for _, name in pairs{"min", "max"} do
local fnc = math[name]
@@ -238,11 +383,64 @@ for _, name in pairs{"min", "max"} do
end
end
+--
+-- Register all binary operators of the "bit" module
+--
+
+--- Get the bitwise **not** of all samples
+-- @treturn Stream
+-- @see bit.bnot
function Stream:bnot()
return self:map(bit.bnot)
end
--- Register all binary operators of the "bit" module
+--- Perform bitwise **or** between two streams
+-- @function bor
+-- @StreamableNumber v Serves as the right argument to @{bit.bor}.
+-- @treturn Stream
+-- @see ZipStream
+
+--- Perform bitwise **and** between two streams
+-- @function band
+-- @StreamableNumber v Serves as the right argument to @{bit.band}.
+-- @treturn Stream
+-- @see ZipStream
+
+--- Perform bitwise **xor** between two streams
+-- @function xor
+-- @StreamableNumber v Serves as the right argument to @{bit.xor}.
+-- @treturn Stream
+-- @see ZipStream
+
+--- Perform bitwise **logical left-shift** between two streams
+-- @function lshift
+-- @StreamableNumber v Serves as the right argument to @{bit.lshift}.
+-- @treturn Stream
+-- @see ZipStream
+
+--- Perform bitwise **logical right-shift** between two streams
+-- @function rshift
+-- @StreamableNumber v Serves as the right argument to @{bit.rshift}.
+-- @treturn Stream
+-- @see ZipStream
+
+--- Perform bitwise **arithmetic right-shift** between two streams
+-- @function arshift
+-- @StreamableNumber v Serves as the right argument to @{bit.arshift}.
+-- @treturn Stream
+-- @see ZipStream
+
+--- Perform bitwise **left rotation** between two streams
+-- @function rol
+-- @StreamableNumber v Serves as the right argument to @{bit.rol}.
+-- @treturn Stream
+-- @see ZipStream
+
+--- Perform bitwise **right rotation** between two streams
+-- @function ror
+-- @StreamableNumber v Serves as the right argument to @{bit.ror}.
+-- @treturn Stream
+-- @see ZipStream
for _, name in pairs{"bor", "band", "bxor",
"lshift", "rshift", "arshift",
"rol", "ror"} do
@@ -255,6 +453,10 @@ for _, name in pairs{"bor", "band", "bxor",
end
end
+--- Clip all samples between two values (between [-1,+1] by default).
+-- @StreamableNumber[opt=-1] min Serves as the lower bound
+-- @StreamableNumber[opt=+1] max Serves as the upper bound
+-- @treturn Stream
function Stream:clip(min, max)
min = min or -1
max = max or 1
@@ -262,8 +464,11 @@ function Stream:clip(min, max)
return self:max(min):min(max)
end
--- Scale [-1,+1] signal to [lower,upper]
--- lower is optional and defaults to 0
+--- Scale stream with values between [-1,+1] to [lower,upper]
+-- @StreamableNumber[opt=0] v1 Delivers the lower value.
+-- @StreamableNumber v2 Delivers the upper value
+-- @treturn Stream
+-- @fixme The API is not documentable easily.
function Stream:scale(v1, v2)
local lower = v2 and v1 or 0
local upper = v2 or v1
@@ -277,31 +482,21 @@ function Stream:scale(v1, v2)
end
end
-function Stream:scan(fnc)
- return ScanStream:new(self, fnc)
-end
-
-function Stream:fold(fnc)
- return FoldStream:new(self, fnc)
-end
-
-function Stream:zip(fnc, ...)
- return ZipStream:new(fnc, self, ...)
-end
-
-function Stream:sub(i, j)
- return SubStream:new(self, i, j)
-end
-
-function Stream:ravel()
- return RavelStream:new(self)
-end
-
+--- Mix two streams
+-- @Stream other Other stream to mix in
+-- @StreamableNumber[opt=0.5] wetness
+-- Wetness factor between [0,1].
+-- This determines the loudness of the other stream.
+-- @treturn Stream
function Stream:mix(other, wetness)
wetness = wetness or 0.5
return self*(1 - wetness) + other*wetness
end
+--- Distribute mono-stream between two stereo channels
+-- @StreamableNumber[opt=0] location
+-- Provides the location (between [0,1]) of the source stream in the resulting stereo stream.
+-- @treturn MuxStream
function Stream:pan(location)
location = location or 0
local cached = self:cache()
@@ -317,24 +512,15 @@ function Stream:pan(location)
end
end
-function Stream:delay(length)
- return DelayStream:new(self, length)
-end
-function Stream:delayx(length, max_length)
- return DelayXStream:new(self, length, max_length)
-end
-
-function Stream:echo(length, wetness)
- local cached = self:cache()
- return cached:mix(cached:delay(length), wetness)
-end
-function Stream:echox(length, wetness, max_length)
- local cached = self:cache()
- return cached:mix(cached:delayx(length, max_length), wetness)
-end
-
--- This is a linear resampler thanks to the
--- semantics of IndexStream
+--- Resample stream.
+-- This is a linear resampler thanks to the semantics of IndexStream.
+-- @number factor
+-- The resampling factor.
+-- If lower than 1, the stream will be slowed.
+-- If higher than 1, it will be sped up.
+-- @treturn Stream
+-- @todo It would be useful if factor would be StreamableNumber.
+-- However the time in line() would consequently also have to be StreamableNumber.
function Stream:resample(factor)
-- FIXME FIXME FIXME
-- self:len()-1 is a workaround for a mysterious bug in LuaJIT
@@ -352,7 +538,12 @@ end
-- be useful for constant frequencies.
--
--- Ramp from 0 to 1
+--- Create a ramp between [0,1] with the frequency taken from the source stream.
+-- It can also be invoked as a regular function to pass constant frequencies.
+-- @function Phasor
+-- @number[opt=0] phase The phase between [0,1].
+-- @treturn Stream
+-- @usage Stream.Phasor(440)
function Stream.Phasor(freq, phase)
phase = phase or 0
@@ -361,7 +552,12 @@ function Stream.Phasor(freq, phase)
end)
end
--- Saw tooth wave from -1 to 1
+--- Saw tooth wave between [-1,+1] with the frequency taken from the source stream.
+-- It can also be invoked as a regular function to pass constant frequencies.
+-- @function SawOsc
+-- @number[opt=0] phase The phase between [0,1].
+-- @treturn Stream
+-- @usage Stream.SawOsc(440)
function Stream.SawOsc(freq, phase)
phase = (phase or 0)*2+1
@@ -370,24 +566,47 @@ function Stream.SawOsc(freq, phase)
end) - 1
end
+--- Sinusiod wave between [-1,+1] with the frequency taken from the source stream.
+-- It can also be invoked as a regular function to pass constant frequencies.
+-- @function SinOsc
+-- @number[opt=0] phase The phase between [0,1].
+-- @treturn Stream
+-- @usage Stream.SinOsc(440)
function Stream.SinOsc(freq, phase)
return Stream.Phasor(freq, phase):mul(2*math.pi):sin()
end
Stream["\u{25CB}"] = Stream.SinOsc -- APL Circle
--- Pulse between 0 and 1 in half a period (width = 0.5)
+--- Pulse between [0,1] with the frequency taken from the source stream.
+-- It can also be invoked as a regular function to pass constant frequencies.
+-- @function PulseOsc
+-- @number[opt=0] phase The phase between [0,1].
+-- @treturn Stream
+-- @usage Stream.PulseOsc(440)
function Stream.PulseOsc(freq, phase)
return Stream.Phasor(freq, phase):map(function(x)
return x < 0.5 and 1 or 0
end)
end
+--- Square wave between [-1,+1] with the frequency taken from the source stream.
+-- It can also be invoked as a regular function to pass constant frequencies.
+-- @function SqrOsc
+-- @number[opt=0] phase The phase between [0,1].
+-- @treturn Stream
+-- @usage Stream.SqrOsc(440)
function Stream.SqrOsc(freq, phase)
return Stream.Phasor(freq, phase):map(function(x)
return x < 0.5 and 1 or -1
end)
end
+--- Triangle wave between [-1,+1] with the frequency taken from the source stream.
+-- It can also be invoked as a regular function to pass constant frequencies.
+-- @function TriOsc
+-- @number[opt=0] phase The phase between [0,1].
+-- @treturn Stream
+-- @usage Stream.TriOsc(440)
function Stream.TriOsc(freq, phase)
local abs = math.abs
@@ -396,7 +615,10 @@ function Stream.TriOsc(freq, phase)
end)
end
--- Bit crusher effect
+--- Bit crusher effect.
+-- This reduces the bit depth of the source stream.
+-- @number[opt=8] bits The resulting streams bit depth.
+-- @treturn Stream
function Stream:crush(bits)
bits = bits or 8
local floor = math.floor
@@ -406,19 +628,24 @@ function Stream:crush(bits)
end)
end
--- The len() method is the main way to get a stream's
--- length (at least in this code) and classes should overwrite
--- this method.
--- The __len metamethod is also defined but it currently cannot
+--- Get a stream's length in samples.
+-- @treturn int Stream length in samples (@{math.huge} for infinite streams).
+-- @fixme __len metamethod is also defined but it currently cannot
-- work since Lua 5.1 does not consider a table's metamethod when
-- evaluating the length (#) operator.
+-- This however would work when building LuaJIT with -DLUAJIT_ENABLE_LUA52COMPAT
function Stream:len()
return math.huge -- infinity
end
+--- Send samples to the Jack output ports, ie. play the stream.
+-- This will block and can be interrupted by pressing Ctrl+C (SIGINT).
+-- @int[opt=1] first_port
+-- First Jack output port to use.
+-- The first stream in a multi-channel stream will go to this port,
+-- the next one to first_port+1 and so on.
function Stream:play(first_port)
- first_port = first_port or 1
- first_port = first_port - 1
+ first_port = (first_port or 1) - 1
-- Make sure JIT compilation is turned on for the generator function
-- and all subfunctions.
@@ -469,16 +696,19 @@ function Stream:play(first_port)
end
end
--- implemented in applause.c
-function Stream:fork()
- error("C function not registered!")
-end
-
--- NOTE: This implementation is for single-channel streams
--- only. See also MuxStream:foreach()
+--- Execute function for each frame generated by the stream.
+-- This will process samples as fast as possible and may therefore
+-- not be well suited to process real-time input.
+-- @func fnc
+-- Function to execute.
+-- It gets passed an array of samples, one for each channel.
+-- @fixme This is not currently interruptable and therefore not suitable
+-- to be executed dynamically at the command-line.
function Stream:foreach(fnc)
local clear = table.clear
+ -- NOTE: This implementation is for single-channel streams
+ -- only. See also MuxStream:foreach().
local frame = table.new(1, 0)
local tick = self:gtick()
@@ -489,6 +719,11 @@ function Stream:foreach(fnc)
end
end
+--- Benchmark stream (time to generate all samples).
+-- This does not work for infinite streams.
+-- Naturally, this calculates samples as fast as possible
+-- and it does not make sense to benchmark streams with real-time input.
+-- @see benchmark
function Stream:benchmark()
if self:len() == math.huge then
error("Cannot benchmark infinite stream")
@@ -499,15 +734,22 @@ function Stream:benchmark()
end)
end
--- Dump bytecode of tick function.
--- FIXME: Return string instead
+--- Dump bytecode for stream (its tick function).
+-- See also the undocumented `jit.bc` module in LuaJIT.
+-- This does not return a string, so it can contain color output.
+-- @tparam[opt=io.stdout] file out File stream to print to (can also be a table of functions).
+-- @bool[opt=true] all Whether to dump all subfunctions as well.
function Stream:jbc(out, all)
-- Load the utility library on-demand.
-- Its API is not stable according to luajit docs.
require("jit.bc").dump(self:gtick(), out, all)
end
--- FIXME: Return string instead
+--- Dump bytecode, traces and machine code of stream (its tick function).
+-- See also the undocumented `jit.dump` module in LuaJIT.
+-- This does not return a string, so it can contain color output.
+-- @string[opt="tbim"] opt Output flags
+-- @tparam[opt=io.stdout] file outfile File stream to print to (can also be a table of functions).
function Stream:jdump(opt, outfile)
local dump = require("jit.dump")
local tick = self:gtick()
@@ -532,9 +774,19 @@ function Stream:jdump(opt, outfile)
if err then error(err) end
end
+--- Convert all values to Lua numbers
+-- @treturn Stream
+-- @see tonumber
function Stream:tonumber() return self:map(tonumber) end
+--- Convert all values to Lua strings
+-- @treturn Stream
+-- @see tostring
function Stream:tostring() return self:map(tostring) end
+--- Calculate all values of a stream and return them as Lua arrays/tables.
+-- This naturally does not work for infinite streams and the eagerly evaluated
+-- stream must not depend on real-time input (MIDI controllers etc).
+-- @return For multi-channel streams, every channel will be returned in its own return value.
function Stream:totable()
if self:len() == math.huge then
error("Cannot serialize infinite stream")
@@ -558,14 +810,23 @@ function Stream:totable()
return unpack(channel_vectors)
end
--- Effectively eager-evaluates the stream returning
--- an array-backed stream.
+--- Evaluate stream eagerly.
+-- This precalculates all values for non-infinite streams, which may be useful
+-- to lower CPU load during real-time playback.
+-- @treturn Stream
+-- @see Stream:totable
function Stream:eval()
return MuxStream:new(self:totable())
end
--- NOTE: This will only plot the stream's first channel
+--- Plot stream with numbers between [-1,+1] to ASCII graphics.
+-- @int[opt=25] rows Rows of the ASCII diagram.
+-- @int[opt=80] cols Columns of the ASCII diagram.
+-- @treturn string ASCII diagram formatted as a string.
+-- @usage =Stream.SinOsc(440):sub(1, samplerate/440):toplot()
+-- @warning This will only plot the stream's first channel
function Stream:toplot(rows, cols)
+ -- @fixme Perhaps default to $ROWS and $COLUMNS?
rows = rows or 25
cols = cols or 80
@@ -605,6 +866,15 @@ function Stream:toplot(rows, cols)
return str
end
+--- Pipe stream values to external program.
+-- This sends one frame per tick on their own line.
+-- Every line can contain multiple numbers separated by tabs depending on the number of channels.
+-- @string prog The program to launch and receive the stream data.
+-- @string[opt="full"] vbufmode Buffering mode.
+-- @int[opt] vbufsize The buffer size.
+-- @see file:setvbuf
+-- @fixme This is currently allowed for infinite streams as well,
+-- but there is no way to interrupt Stream:foreach().
function Stream:pipe(prog, vbufmode, vbufsize)
local hnd = io.popen(prog, "w")
hnd:setvbuf(vbufmode or "full", vbufsize)
@@ -617,6 +887,10 @@ function Stream:pipe(prog, vbufmode, vbufsize)
hnd:close()
end
+--- Plot stream using [gnuplot](http://www.gnuplot.info/).
+-- This is not allowed for infinite streams.
+-- @warning This requires the feedgnuplot script.
+-- @fixme gnuplot is not the ideal tool for plotting audio data.
function Stream:gnuplot()
if self:len() == math.huge then
error("Cannot plot infinite stream")
@@ -640,29 +914,57 @@ function Stream:gnuplot()
hnd:close()
end
-function Stream:mux(...)
- return MuxStream:new(self, ...)
-end
-
-function Stream:dupmux(channels)
- return DupMux(self, channels)
-end
-
--- For single-channel streams only, see also MuxStream:demux()
-function Stream:demux(i, j)
- j = j or i
- assert(i == 1 and j == 1,
- "Invalid channel range specified (mono-channel stream)")
- return self
-end
+--- Check whether stream is an instance of a particular class.
+-- @function instanceof
+-- @tparam Class other_class The other object or class to check against.
+-- @treturn bool Whether the stream is an instance of other_class.
+-- @see DeriveClass
+--
-- Stream metamethods
+--
--- NOTE: Currently non-functional since Lua 5.1 does not
--- consider metamethods when evaluating the length operator.
+--- Create new stream using the `()` operator (metamethod).
+-- This is equivalent to calling the @{Stream:new} method.
+-- @metamethod __call
+-- @local
+-- @treturn Stream
+-- @usage Stream(23)
+-- @see Stream:new
+-- @see DeriveClass
+
+--- Extract and interpolate samples using the `[]` operator (metamethod).
+-- @metamethod __index
+-- @StreamableNumber index
+-- The stream that will generate indices into the base stream.
+-- If these numbers have fractions, the output sample will be linearilly interpolated.
+-- The maximum number generated by this stream determines the memory requirements
+-- of the index operation, so this *should* never produce very large numbers or
+-- unboundedly growing numbers.
+-- @treturn Stream
+-- The resulting stream will have a the same length as the index-stream (`index:len()`).
+-- @usage SndfileStream("test.wav")[Stream.SinOsc(0.5):scale(1, sec(5))]
+-- @usage iota(2, 10)[{2, 5, 8}]
+-- @fixme This will only work for number streams at the moment.
+-- @fixme The memory requirements could be lifted by having a way to arbitrarily
+-- seek the streams. This however would complicate the design significantly.
+
+--- Get length of stream via `#` operator (metamethod).
+-- @metamethod __len
+-- @local
+-- @treturn int
+-- @see Stream:len
+-- @usage #tostream{1, 2, 3}
+-- @fixme Currently non-functional since Lua 5.1 does not
+-- consider metamethods when evaluating the length operator
+-- unless you compile with -DLUAJIT_ENABLE_LUA52COMPAT.
function Stream:__len() return self:len() end
--- NOTE: Will only convert the first channel
+--- Format stream as string (metamethod).
+-- This will only format the first channel and not more than 1024 samples.
+-- @metamethod __tostring
+-- @treturn string
+-- @usage tostring(tostream{1, 2, 3})
function Stream:__tostring()
local stream = self:tostring()
@@ -677,7 +979,7 @@ end
-- NOTE: These operators work with scalars and streams.
-- The semantics of e.g. adding Stream(x) is compatible
-- with a map that adds x. Maps are preferred since
--- they are (slightly).
+-- they are (slightly) faster.
-- NOTE: Named addOp() and similar functions below
-- are necessary instead of lambdas so consecutive
-- operations can be collapsed by ZipStream (which
@@ -686,6 +988,13 @@ end
do
local function addOp(x1, x2) return x1+x2 end
+ --- Add samples of two streams
+ -- This can be called as a Stream method or as an operator.
+ -- @StreamableNumber v1 Provides left values of the add operation.
+ -- @StreamableNumber v2 Provides right values of the add operation.
+ -- @treturn Stream
+ -- @usage tostream(23):add(5)
+ -- @usage tostream(23) + 5
function Stream.add(v1, v2)
return type(v2) == "number" and
MapStream:new(v1, function(x) return x+v2 end) or
@@ -706,6 +1015,13 @@ do
--
--local function subOp(x1, x2) return x1-x2 end
+ --- Subtract samples of two streams
+ -- This can be called as a Stream method or as an operator.
+ -- @StreamableNumber v1 Provides left values of the subtraction operation.
+ -- @StreamableNumber v2 Provides right values of the subtraction operation.
+ -- @treturn Stream
+ -- @usage tostream(23):minus(5)
+ -- @usage tostream(23) - 5
function Stream.minus(v1, v2)
return type(v2) == "number" and
MapStream:new(v1, function(x) return x-v2 end) or
@@ -717,11 +1033,23 @@ end
do
local function mulOp(x1, x2) return x1*x2 end
+ --- Multiply samples of two streams
+ -- This can be called as a Stream method or as an operator.
+ -- @StreamableNumber v1 Provides left values of the multiplication operation.
+ -- @StreamableNumber v2 Provides right values of the multiplication operation.
+ -- @treturn Stream
+ -- @usage tostream(23):mul(5)
+ -- @usage tostream(23) * 5
function Stream.mul(v1, v2)
return type(v2) == "number" and
MapStream:new(v1, function(x) return x*v2 end) or
ZipStream:new(mulOp, v1, v2)
end
+ --- Change volume (gain) of stream.
+ -- This is an alias of @{Stream.mul}.
+ -- @function gain
+ -- @StreamableNumber volume The volume of the resulting stream.
+ -- @treturn Stream
Stream.gain = Stream.mul
Stream["\u{00D7}"] = Stream.mul -- APL Multiply/Signum
Stream.__mul = Stream.mul
@@ -731,6 +1059,13 @@ do
-- FIXME: See above minus()
--local function divOp(x1, x2) return x1/x2 end
+ --- Divide samples of two streams
+ -- This can be called as a Stream method or as an operator.
+ -- @StreamableNumber v1 Provides left values of the division operation.
+ -- @StreamableNumber v2 Provides right values of the division operation.
+ -- @treturn Stream
+ -- @usage tostream(23):div(5)
+ -- @usage tostream(23) / 5
function Stream.div(v1, v2)
return type(v2) == "number" and
MapStream:new(v1, function(x) return x/v2 end) or
@@ -744,6 +1079,13 @@ do
-- FIXME: See above minus()
--local function modOp(x1, x2) return x1%x2 end
+ --- Calculate modulus of two streams
+ -- This can be called as a Stream method or as an operator.
+ -- @StreamableNumber v1 Provides left values of the modulo operation.
+ -- @StreamableNumber v2 Provides right values of the modulo operation.
+ -- @treturn Stream
+ -- @usage tostream(23):mod(5)
+ -- @usage tostream(23) % 5
function Stream.mod(v1, v2)
return type(v2) == "number" and
MapStream:new(v1, function(x) return x%v2 end) or
@@ -756,6 +1098,13 @@ do
-- FIXME: See above minus()
--local function powOp(x1, x2) return x1^x2 end
+ --- Take one stream to the power of another stream.
+ -- This can be called as a Stream method or as an operator.
+ -- @StreamableNumber v1 Provides left values of the power operation.
+ -- @StreamableNumber v2 Provides right values of the power operation.
+ -- @treturn Stream
+ -- @usage tostream(23):pow(5)
+ -- @usage tostream(23)^5
function Stream.pow(v1, v2)
return type(v2) == "number" and
MapStream:new(v1, function(x) return x^v2 end) or
@@ -765,28 +1114,62 @@ do
Stream.__pow = Stream.pow
end
+--- Negate all samples of stream via `-` operator (metamethod)
+-- @metamethod __unm
+-- @treturn Stream
+-- @usage -tostream(23)
function Stream:__unm() return self * -1 end
-function Stream.__concat(op1, op2)
- return ConcatStream:new(op1, op2)
-end
-
-- FIXME: Length comparisions can already be written
-- elegantly - perhaps these operators should have
-- more APLish semantics instead?
-- However Lua practically demands these metamethods
-- (as well as __eq) to return booleans.
-function Stream.__lt(op1, op2)
- return op1:len() < op2:len()
-end
-
-function Stream.__le(op1, op2)
- return op1:len() <= op2:len()
-end
-
+--- Check whether one stream is short than second via `<` operator (metamethod)
+-- @metamethod __lt
+-- @Stream op1 Left stream
+-- @Stream op2 Right stream
+-- @treturn bool
+-- @usage tostream{1, 2, 3} < tostream(23)
+function Stream.__lt(op1, op2) return op1:len() < op2:len() end
+
+--- Check whether one stream is short than or equal to second via `<=` operator (metamethod)
+-- @metamethod __le
+-- @Stream op1 Left stream
+-- @Stream op2 Right stream
+-- @treturn bool
+-- @usage tostream{1, 2, 3} <= tostream(23)
+function Stream.__le(op1, op2) return op1:len() <= op2:len() end
+
+--- Concatenate two streams via `..` operator (metamethod)
+-- @metamethod __concat
+-- @StreamableNumber op1 First stream
+-- @StreamableNumber op2 Second stream
+-- @treturn Stream
+-- @see ConcatStream:new
+-- @usage tostream{1, 2, 3}..23
+function Stream.__concat(op1, op2) return ConcatStream:new(op1, op2) end
+
+--- Class for muxing multiple streams into a multi-channel stream.
+-- @type MuxStream
MuxStream = DeriveClass(Stream)
+--- Create new MuxStream, ie combine multiple streams given as parameters into a single multi-channel stream.
+-- The individual parameters can also be scalars and tables converted via @{tostream}.
+-- The individual streams can both be a plain single-channel @{Stream}s or MuxStreams with an arbitrary number of channels.
+-- The resulting stream's number of channels is the sum of all constituent streams' channels.
+-- @function new
+-- @param ... Streams to mux
+-- @treturn MuxStream
+-- @see Stream:mux
+-- @see tostream
+-- @see Stream:totable
+-- @usage MuxStream(Stream.SinOsc(440), Stream.SinOsc(880)):play()
+-- @fixme This could actually be hidden (@local) from end users.
+-- The only advantage over Stream:mux is that it is more elegant when creating from
+-- constants.
+-- We could however use Stream.mux(...) as well and it would work exactly like MuxStream:new().
function MuxStream:ctor(...)
self.streams = {}
for k, stream in ipairs{...} do
@@ -820,6 +1203,21 @@ function MuxStream:len()
return self.streams[1]:len()
end
+--- Extract one or more channels
+-- @within Class Stream
+-- @int i Id of first channel to extract.
+-- @int[opt=i] j
+-- Id of last channel to extract.
+-- If omitted, only one channel (i) is extracted.
+-- @treturn Stream|MuxStream
+function Stream:demux(i, j)
+ j = j or i
+ -- For single-channel streams only, see also MuxStream:demux()
+ assert(i == 1 and j == 1,
+ "Invalid channel range specified (mono-channel stream)")
+ return self
+end
+
-- Overrides Stream:demux()
function MuxStream:demux(i, j)
j = j or i
@@ -861,8 +1259,24 @@ function MuxStream:foreach(fnc)
end
end
--- FIXME: This should perhaps be a class
-function DupMux(stream, channels)
+--- Mux several streams with the source stream into a multi-channel stream.
+-- @within Class Stream
+-- @param ... Streams to mux
+-- @treturn MuxStream
+-- @see MuxStream:new
+-- @usage Stream.SinOsc(440):mux(Stream.SinOsc(880)):play()
+function Stream:mux(...)
+ return MuxStream:new(self, ...)
+end
+
+--- Duplicate channel in single-channel stream (or suitable scalar value).
+-- This can be called as a function or method.
+-- @within Class Stream
+-- @StreamableNumber stream Source of single channel data.
+-- @int[opt=2] channels Number of channels of resulting stream.
+-- @treturn MuxStream
+-- @see MuxStream:new
+function Stream.dupmux(stream, channels)
channels = channels or 2
local cached = tostream(stream):cache()
@@ -875,14 +1289,24 @@ function DupMux(stream, channels)
return MuxStream:new(unpack(streams))
end
--- Base class for all streams that operate on arbitrary numbers
--- of other streams. Handles muxing opaquely.
+--- Base class for all streams that operate on multi-channel streams.
+-- It handles muxing opaquely, by applying the tick() function on every
+-- channel individually.
+-- This allows writing multi-channel-capable Streams without complicating
+-- things with having to handle frames etc.
+-- This class is **abstract**, you are only supposed to derive from it,
+-- not to instantiate it directly.
+-- @type MuxableStream
+-- @see DeriveClass
MuxableStream = DeriveClass(Stream)
--- Describes the part of the muxableCtor's signature
--- containing muxable streams.
--- By default all arguments are muxable streams.
+--- The first muxable stream in @{MuxableStream:muxableCtor}'s signature.
+-- If negative, this refers to an argument at the end of the parameter list.
+-- This is 1 (first argument) by default.
MuxableStream.sig_first_stream = 1
+--- The last muxable stream in @{MuxableStream:muxableCtor}'s signature.
+-- If negative, this refers to an argument at the end of the parameter list.
+-- This is -1 (last argument) by default.
MuxableStream.sig_last_stream = -1
function MuxableStream:ctor(...)
@@ -936,6 +1360,24 @@ function MuxableStream:ctor(...)
return MuxStream:new(unpack(channel_streams))
end
+--- Constructor for classes derived form @{MuxableStream}.
+-- This is an **abstract** method, you **must** implement it in your subclass.
+-- @function muxableCtor
+-- @param ...
+-- The arguments passed on from the @{Stream:new} method.
+-- All arguments between @{MuxableStream.sig_first_stream} and
+-- @{MuxableStream.sig_last_stream} are automatically converted
+-- to Streams and demuxed (see @{Stream:demux}), so you will only
+-- get passed plain single-channel streams.
+-- @treturn ?Stream
+-- You can optionally return a @{Stream} instance to competely replace the object table.
+-- If given, this value will be returned by the @{Stream:new} method instead.
+-- @see Stream:ctor
+
+--- Class for caching streams.
+-- Cached streams are calculated only once per tick.
+-- @type CachedStream
+-- @local
CachedStream = DeriveClass(MuxableStream)
function CachedStream:muxableCtor(stream)
@@ -959,6 +1401,21 @@ function CachedStream:len()
return self.stream:len()
end
+--- Cache this stream value to avoid recalculation within
+-- the same tick (ie. point in time).
+-- This may happen when a stream is used multiple times in the same "patch".
+-- @within Class Stream
+-- @treturn Stream
+-- @todo This could be done automatically by an optimizer stage.
+-- @fixme This is counter-productive for simple number streams
+-- (anything simpler than a table lookup).
+function Stream:cache()
+ return CachedStream:new(self)
+end
+
+--- Class for streams based on vectors (arrays).
+-- @type VectorStream
+-- @local
VectorStream = DeriveClass(Stream)
-- NOTE: This is mono-streams only, the inverse of Stream:totable()
@@ -981,8 +1438,21 @@ function VectorStream:len()
return #self.vector
end
+--- Class for creating streams consisting of multiple concatenated base streams.
+-- @type ConcatStream
+-- @fixme The only advantage of using ConcatStream:new instead of the `..` operator
+-- is that you do not need braces to apply other Stream methods.
+-- This could be circumvented by introducing a Stream:append() method.
+-- In this case, ConcatStream could be @local.
ConcatStream = DeriveClass(MuxableStream)
+--- Create new ConcatStream, ie concatenation of serveral streams.
+-- @function new
+-- @param ...
+-- Individual @{Stream}s (or numbers or tables, that can be converted to streams).
+-- All but the last one must not be infinite.
+-- @treturn ConcatStream
+-- @see Stream.__concat
function ConcatStream:muxableCtor(...)
self.streams = {}
for _, v in ipairs{...} do
@@ -1066,9 +1536,16 @@ function RepeatStream:gtick()
-- next iteration
i = i + 1
- -- FIXME: The tick() method itself may be too
+ -- @fixme The Stream:gtick() method itself may be too
-- inefficient for realtime purposes.
-- Also, we may slowly leak memory.
+ -- It would be possible to precalculate the tick functions,
+ -- but that wouldn't work for infinite repeats.
+ -- This could be circumvented by supporting an optional reset function
+ -- returned by Stream:gtick.
+ -- This however would complicate all higher-order streams.
+ -- Another solution would be a two-stage Stream:gtick which allows real-time
+ -- safe gticking.
tick = stream:gtick()
end
end
@@ -1078,6 +1555,15 @@ function RepeatStream:len()
return self.stream:len() * self.repeats
end
+--- Repeat stream. After the stream runs out of samples, it will start all over again.
+-- @within Class Stream
+-- @int[opt] repeats Number of repeats. If missing, the stream will be repeated indefinitely.
+-- @treturn Stream
+-- @fixme See restrictions of RepeatStream:gtick and Stream:gtick.
+function Stream:rep(repeats)
+ return RepeatStream:new(self, repeats)
+end
+
-- Ravel operation inspired by APL.
-- This removes one level of nesting from nested streams
-- (e.g. streams of streams), and is semantically similar
@@ -1107,6 +1593,7 @@ function RavelStream:gtick()
-- NOTE: We don't use instanceof() here for performance
-- reasons
if type(value) == "table" and value.is_a_stream then
+ -- @fixme This is not real-time safe!
current_tick = value:gtick()
else
return value
@@ -1134,6 +1621,15 @@ function RavelStream:len()
return len
end
+--- Ravel stream.
+-- This takes a stream of streams and concatenates them.
+-- @within Class Stream
+-- @treturn Stream
+-- @usage tostream{Stream.SinOsc(440):sub(1, sec()), Stream.SinOsc(880):sub(1, sec())}:ravel():play()
+function Stream:ravel()
+ return RavelStream:new(self)
+end
+
IotaStream = DeriveClass(Stream)
function IotaStream:ctor(v1, v2)
@@ -1166,12 +1662,28 @@ function IotaStream:len()
self.to - self.from + 1
end
--- i and j have the same semantics as in string.sub()
+---
+--- @section end
+---
+
+--- Generate sequences of integers between [v1,v2].
+-- @function iota
+-- @int[opt=1] v1
+-- If this is the only argument, the lower bound is 1 and v1 specifies the upper bound.
+-- So without arguments, an infinite sequence of integers beginning with 1 will be generated.
+-- @int[opt=math.huge] v2 If a second argument is specified, v1 is the lower bound and v2 the upper bound.
+-- @treturn Stream
+-- @usage iota(23)
+-- @usage iota(1, 23)
+iota = IotaStream
+_G["\u{2373}"] = iota -- APL Iota
+
SubStream = DeriveClass(MuxableStream)
-- We have trailing non-stream arguments
SubStream.sig_last_stream = 1
+-- i and j have the same semantics as in string.sub()
function SubStream:muxableCtor(stream, i, j)
self.stream = stream
self.i = i
@@ -1191,8 +1703,13 @@ end
function SubStream:gtick()
local tick = self.stream:gtick()
- -- OPTIMIZE: Perhaps ask stream to skip the first
- -- self.i-1 samples
+ -- @fixme There is room for optimization.
+ -- Perhaps ask stream to skip the first self.i-1 samples
+ -- Either gtick() could take an argument or introduce an
+ -- overwritable gtick_seek(). Problem as always is that
+ -- this would have to chain to substreams.
+ -- Perhaps, it would be wiser to let this be handled by
+ -- an optimizer stage that resolves Stream:sub-calls.
for _ = 1, self.i-1 do tick() end
local i = self.i
@@ -1209,7 +1726,26 @@ function SubStream:len()
self.j - self.i + 1
end
--- FIXME: Will not work for non-samlpe streams
+--- Get substream (restrict stream in length).
+-- This both allows discarding samples at the beginning and restricting the length
+-- (even of infinite streams).
+-- The semantics of the parameters are similar to @{string.sub}.
+-- @within Class Stream
+-- @int i
+-- Start sample (1 is the first sample).
+-- It may be negative to specify positions from the end, whereas -1 refers to the last sample.
+-- @int[opt=-1] j
+-- The last sample to include in the resulting stream.
+-- It can also be negative to refer to samples at the end of the stream.
+-- @treturn Stream
+-- @see sec
+-- @see msec
+-- @usage Stream.SinOsc(440):sub(1, sec()):play()
+function Stream:sub(i, j)
+ return SubStream:new(self, i, j)
+end
+
+-- @fixme Will not work for non-sample streams
-- This should be split into a generic (index) and
-- sample-only (interpolate) operation
IndexStream = DeriveClass(MuxableStream)
@@ -1286,6 +1822,16 @@ function MapStream:len()
return self.stream:len()
end
+--- Map function to every sample of stream
+-- @within Class Stream
+-- @func fnc Function to apply to every sample.
+-- @treturn Stream
+-- @usage Stream.Phasor(440):mul(2*math.pi):map(math.sin)
+function Stream:map(fnc)
+ return MapStream:new(self, fnc)
+end
+Stream["\u{00A8}"] = Stream.map -- APL Double-Dot
+
ScanStream = DeriveClass(MuxableStream)
-- We have trailing non-stream arguments
@@ -1314,12 +1860,28 @@ function ScanStream:len()
return self.stream:len()
end
+--- Scan stream with function.
+-- Every function call will receive the last and current sample,
+-- so it is possible to "accumulate" values.
+-- @within Class Stream
+-- @func fnc
+-- The function that gets called with the last and current sample to determine the output sample.
+-- @treturn Stream
+-- @usage =iota(10):scan(function(last, sample) return last+sample end)
+function Stream:scan(fnc)
+ return ScanStream:new(self, fnc)
+end
+
FoldStream = DeriveClass(MuxableStream)
-- We have trailing non-stream arguments
FoldStream.sig_last_stream = 1
function FoldStream:muxableCtor(stream, fnc)
+ if stream:len() == math.huge then
+ error("An infinite stream cannot be folded!")
+ end
+
self.stream = stream
self.fnc = fnc
end
@@ -1346,11 +1908,26 @@ function FoldStream:len()
return self.stream:len() > 0 and 1 or 0
end
+--- Fold stream by calling function between all samples.
+-- This will pass the current sample as the *right* argument and the last returned
+-- value as the *left* argument.
+-- In other words, it is similar to `fnc(fnc(fnc(sample[1], sample[2]), sample[3]), ...)`.
+-- This naturally cannot work on infinite streams.
+-- @within Class Stream
+-- @func fnc Function that gets called with the *left* and *right* arguments.
+-- @treturn Stream The returned stream has either length 0 or 1.
+-- @usage =iota(10):fold(function(last, sample) return last+sample end)
+function Stream:fold(fnc)
+ return FoldStream:new(self, fnc)
+end
+
+---
-- ZipStream combines any number of streams into a single
--- stream using a function. This is the basis of the "+"
--- and "*" operations.
+-- stream using a function.
+-- This is the basis of the `+` and `*` stream operations.
+-- @type ZipStream
--
--- NOTE (FIXME?): This "inlines" ZipStream arguments with
+-- @fixme This "inlines" ZipStream arguments with
-- the same function as an optimization. This ONLY WORKS
-- for associative operations and more than 3 operands are
-- probably slower than two ZipStreams except for very large
@@ -1361,6 +1938,15 @@ ZipStream = DeriveClass(MuxableStream)
-- We have a leading non-stream argument
ZipStream.sig_first_stream = 2
+--- Create a ZipStream.
+-- This is a stream that combines two or more substituent streams by
+-- applying a function between each of their samples.
+-- @function new
+-- @func fnc Function to apply between samples.
+-- @param ... Streams to combine.
+-- @treturn ZipStream
+-- @see Stream:zip
+-- @usage ZipStream(function(l, r) return l+r end), Stream.SinOsc(440):gain(0.5), Stream.SinOsc(880):gain(0.5))
function ZipStream:muxableCtor(fnc, ...)
self.fnc = fnc
@@ -1405,7 +1991,7 @@ function ZipStream:gtick()
else
-- NOTE: Unfortunately, functions in the
-- ticks array cannot be inlined
- local ticks = {}
+ local ticks = table.new(#self.streams, 0)
for i = 1, #self.streams do
ticks[i] = self.streams[i]:gtick()
end
@@ -1431,8 +2017,26 @@ function ZipStream:len()
return self.streams[1]:len()
end
+--- Zip stream with one or more other streams.
+-- @within Class Stream
+-- @func fnc Function to apply between samples.
+-- @param ... Streams to zip with the calling stream.
+-- @see ZipStream:new
+function Stream:zip(fnc, ...)
+ return ZipStream:new(fnc, self, ...)
+end
+
+--- A stream generating random values (noise) between [-1,1].
+-- Since it does not have parameters, you don't necessarily have to instantiate it.
+-- The class table itself is a valid stream object.
+-- @type NoiseStream
+-- @usage NoiseStream:play()
NoiseStream = DeriveClass(Stream)
+--- Create new NoiseStream
+-- @function new
+-- @treturn NoiseStream
+
function NoiseStream:gtick()
local random = math.random
@@ -1441,17 +2045,35 @@ function NoiseStream:gtick()
end
end
--- NOTE: Adapted from the algorithm used here:
--- http://vellocet.com/dsp/noise/VRand.html
+---
+--- @section end
+---
+
+--- Create a brown noise stream.
+-- @treturn Stream
+-- @see NoiseStream
+-- @fixme This is inconsistent. Perhaps we should define a BrownNoiseStream itself.
function BrownNoise()
+ -- NOTE: Adapted from the algorithm used here:
+ -- http://vellocet.com/dsp/noise/VRand.html
return NoiseStream():scan(function(brown, white)
brown = (brown or 0) + white
return (brown < -8 or brown > 8) and brown - white or brown
end) * 0.0625
end
+--- Stream generating pink noise.
+-- Since it does not have parameters, you don't necessarily have to instantiate it.
+-- The class table itself is a valid stream object.
+-- @type PinkNoiseStream
+-- @see NoiseStream
+-- @usage PinkNoiseStream:play()
PinkNoiseStream = DeriveClass(Stream)
+--- Create new NoiseStream
+-- @function new
+-- @treturn PinkNoiseStream
+
-- NOTE: Adapted from the algorithm used here:
-- http://vellocet.com/dsp/noise/VRand.html
function PinkNoiseStream:gtick()
@@ -1501,9 +2123,6 @@ end
--
-- Delay Lines
--- NOTE: Echoing could be implemented here as well since
--- delay lines are only an application of echoing with a wetness of 1.0.
--- However this complicates matters because we have to handle nil.
--
DelayStream = DeriveClass(MuxableStream)
@@ -1536,6 +2155,29 @@ function DelayStream:len()
return self.length + self.stream:len()
end
+--- Delay stream by buffering (delay line).
+-- This is different to prepending a `Stream(0):sub(1, length)` in that it can be used to
+-- produce feedback lines and delay real-time input.
+-- @within Class Stream
+-- @int length Length of delay line in samples.
+-- @treturn Stream
+function Stream:delay(length)
+ return DelayStream:new(self, length)
+end
+
+--- Add echo to stream.
+-- @within Class Stream
+-- @int length How much to delay the echo.
+-- @StreamableNumber[opt=0.5] wetness
+-- Wetness/loadness of the echo (between [0,1]).
+-- @treturn Stream
+-- @see Stream:delay
+-- @usage SndfileStream("voice.wav"):echo(msec(200)):play()
+function Stream:echo(length, wetness)
+ local cached = self:cache()
+ return cached:mix(cached:delay(length), wetness)
+end
+
--
-- Delay line with a variable (stream) length and
-- maximum length. Non-interpolating.
@@ -1578,10 +2220,47 @@ function DelayXStream:len()
return self.max_length + self.stream:len()
end
+--- Delay line with variable length.
+-- @within Class Stream
+-- @StreamableNumber length Stream generating the echo length.
+-- @int[opt=sec()] max_length Maximum length of the delay line.
+-- @see Stream:delay
+-- @fixme This is probably broken.
+-- @fixme It may be possible to merge this into Stream:delay.
+function Stream:delayx(length, max_length)
+ return DelayXStream:new(self, length, max_length)
+end
+
+--- Echo with variable delay.
+-- @within Class Stream
+-- @StreamableNumber length Stream generating the echo delay.
+-- @StreamableNumber[opt=0.5] wetness
+-- Wetness/loadness of the echo (between [0,1]).
+-- @int[opt=sec()] max_length Maximum length of the delay line.
+-- @see Stream:delay
+-- @fixme This is probably broken.
+-- @fixme It may be possible to merge this into Stream:delay.
+function Stream:echox(length, wetness, max_length)
+ local cached = self:cache()
+ return cached:mix(cached:delayx(length, max_length), wetness)
+end
+
+---
+--- @section end
+---
+
--
-- Primitives
--
+--- Convert value to @{Stream}.
+-- Streams are returned unchanged.
+-- A table/array will be converted to a stream, that produces all of its elements consecutively.
+-- All other Lua values are generated infinitely.
+-- @param v Value to convert.
+-- @treturn Stream
+-- @usage tostream(440):SinOsc():play()
+-- @usage tostream{"A4", "B4", "C4"}:mtof()
function tostream(v)
if type(v) == "table" then
if v.is_a_stream then return v end
@@ -1592,17 +2271,34 @@ function tostream(v)
end
end
-function iota(...) return IotaStream:new(...) end
-_G["\u{2373}"] = iota -- APL Iota
-
+--- Generate a linear line segment.
+-- It will start with v1 and linearilly slide to v2 in the given time.
+-- @StreamableNumber v1 Start value.
+-- @int t Duration of the value change in samples.
+-- @StreamableNumber v2 End value.
+-- @treturn Stream The resulting stream will have length t.
+-- @usage Stream.SinOsc(440):gain(line(0, sec(5), 1)):play()
function line(v1, t, v2)
return iota(t) * ((v2-v1)/t) + v1
end
--- Derived from RTcmix' "curve" table
--- Generates a single linear or logarithmic line segment
--- See http://www.music.columbia.edu/cmc/Rtcmix/docs/scorefile/maketable.html#curve
+--- Generates a single linear or logarithmic line segment.
+-- It will start at v1 and logarithmically slide to v2 in the given time.
+-- @number v1 Start value.
+-- @number alpha
+-- Line curvature.
+-- A value of nil or 0, creates a straight line.
+-- If smaller than 0, makes a logarithmic (convex) curve; larger negative values make "sharper" curves.
+-- If larger than 0, makes a logarithmic (concave) curve; larger values make "sharper" curves.
+-- @int t Duration of the value change in samples.
+-- @number[opt=0] v2 End value.
+-- @treturn Stream The resulting stream will have length t.
+-- @see line
+-- @usage Stream.SinOsc(440):gain(curve(0, -1, sec(5), 1)):play()
+-- @fixme Could v1 and v2 be StreamableNumbers?
function curve(v1, alpha, t, v2)
+ -- Derived from RTcmix' "curve" table
+ -- See http://www.music.columbia.edu/cmc/Rtcmix/docs/scorefile/maketable.html#curve
v2 = v2 or 0
if not alpha or alpha == 0 then return line(v1, t, v2) end
@@ -1615,8 +2311,15 @@ function curve(v1, alpha, t, v2)
end)
end
--- Generates a variable number of concatenated line segments
--- E.g. curves(0, 0, sec(1), 1, 0, sec(1), 0)
+--- Generates a variable number of concatenated line segments.
+-- @number v1 Start value.
+-- @number alpha Line curvature.
+-- @int t Duration of the value change in samples.
+-- @number v2 End value.
+-- @param ... There can be more line segments starting with v2.
+-- @treturn Stream
+-- @see curve
+-- @usage Stream.SinOsc(440):gain(curves(0, 0, sec(1), 1, 0, sec(1), 0)):play()
function curves(...)
local args = {...}
local ret
@@ -1631,6 +2334,8 @@ end
-- Jack client abstractions. This passes low level signals
-- and works only with clients created via Stream.fork()
--
+-- @fixme This is currently broken.
+--
cdef_safe[[
int kill(int pid, int sig);
@@ -1656,6 +2361,11 @@ end
Client.__gc = Client.kill
+-- implemented in applause.c
+function Stream:fork()
+ error("C function not registered!")
+end
+
--
-- Additional modules are loaded with dofile(),
-- so they react to reload()
diff --git a/config.ld b/config.ld
new file mode 100644
index 0000000..b7b4ad5
--- /dev/null
+++ b/config.ld
@@ -0,0 +1,33 @@
+-- LDoc configuration file
+
+project = "Applause"
+description = "LuaJIT-based real-time synthesizer, based on a stream algebra"
+
+format = "discount"
+readme = "README.md"
+examples = "examples/"
+
+file = {
+ "applause.lua", "sndfile-stream.lua", -- "sndfile.lua",
+ "filters.lua", "dssi.lua", "midi.lua", "evdev.lua"
+}
+
+no_space_before_args = true
+manual_url "https://www.lua.org/manual/5.1/manual.html"
+
+-- Support external references to the bit module.
+custom_see_handler("^bit%.(.+)$", function(fnc)
+ return "bit."..fnc, "https://bitop.luajit.org/api.html#"..fnc
+end)
+
+-- Use @Stream only if a parameter must already be a Stream object.
+tparam_alias("Stream", "Stream")
+-- Use @StreamableNumber for parameters that can be converted to number streams using tostream().
+tparam_alias("StreamableNumber", "Stream|{number,...}|number")
+
+-- For metamethods
+-- FIXME: Perhaps we can put them into separate sections.
+new_type("metamethod", "Metamethods")
+
+-- The @submodule tag does not work, so we use `@module applause` multiple times instead.
+merge = true
diff --git a/dssi.lua b/dssi.lua
index ba7f47c..ce6ca47 100644
--- a/dssi.lua
+++ b/dssi.lua
@@ -1,3 +1,6 @@
+---
+--- @module applause
+---
local bit = require "bit"
local ffi = require "ffi"
local C = ffi.C
@@ -442,7 +445,7 @@ const DSSI_Descriptor *dssi_descriptor(unsigned long Index);
local asound = ffi.load("asound")
--- Check for symbol existence in C library table.
+--- Check for symbol existence in C library table.
-- There does not seem to be a more elegant way to do this.
local function checkClibSymbol(lib, symbol)
return pcall(getmetatable(lib).__index, lib, symbol) == true
@@ -507,36 +510,42 @@ local function mangleInputPorts(input_ports, ...)
return input_ports
end
+--- Stream wrapper for external DSSI and LADSPA plugins.
+-- @type DSSIStream
DSSIStream = DeriveClass(Stream)
--- `file` is either the full path to a plugin library or a basename
--- looked up in $DSSI_PATH and $LADSPA_PATH.
--- It may be followed by an optional ":Label" to select a plugin by type
--- from this file (otherwise, the first one is used).
+--- Create a DSSI or LADSPA stream.
+-- @function new
+-- @string file
+-- `file` is either the full path to a plugin library or a basename
+-- looked up in $DSSI_PATH and $LADSPA_PATH.
+-- It may be followed by an optional ":Label" to select a plugin by type
+-- from this file (otherwise, the first one is used).
+-- @Stream[opt] midi_event_stream
+-- If the plugin is DSSI, this may be a @{MIDIStream}, but may also be nil.
+-- For LADSPA plugins, this argument already specifies the first input port.
+-- @tab[opt] input_ports
+-- A table defining the mapping from
+-- LADSPA port names to Streams or constants for audio and control input ports.
+-- This host does not make a difference between audio and control ports.
+-- A mapping from port Id to Streams (ie. an array of Streams corresponding
+-- with the ports) is also allowed.
+-- Every plugin input port must either be mapped or have a default value.
+-- Constants are handled specially and are faster than streams.
+-- Multi-channel input streams do not result in muxing of the DSSIStream,
+-- so every input stream must be mono.
+-- However, to ease binding the individual channels of a multi-channel
+-- stream, they are automatically expanded to consecutive input streams.
+-- @param ...
+-- All additional arguments are added to this table as an array,
+-- so port mappings can be specified as a list of arguments as well.
+-- @treturn DSSIStream|MuxStream
+-- Multi-channel output plugins are always muxed. But you may use
+-- @{Stream:demux} to discard uninteresting output channels.
+-- @see Stream:DSSI
--
--- If the plugin is DSSI, the second argument may be a MIDI
--- event stream but may also be nil.
---
--- `input_ports` are tables defining the mapping from
--- LADSPA port names to Streams or constants for audio and control input ports.
--- This host does not make a difference between audio and control ports.
--- A mapping from port Id to Streams (ie. an array of Streams corresponding
--- with the ports) is also allowed.
--- All additional arguments are added to this table as an array,
--- so port mappings can be specified as a list of arguments as well.
--- Every plugin input port must either be mapped or have a default
--- value.
--- Constants are handled specially and are faster than streams.
--- Multi-channel input streams do not result in muxing of the DSSIStream,
--- so every input stream must be mono.
--- However, to ease binding the individual channels of a multi-channel
--- stream, they are automatically expanded to consecutive input streams.
---
--- Multi-channel output plugins are always muxed. But you may use
--- DSSIStream(...):demux(...) to discard uninteresting output channels.
---
--- FIXME: We could simplify things by just assuming a flat array of
--- input ports
+-- @fixme We could simplify things by just assuming a flat array of
+-- input ports.
function DSSIStream:ctor(file, midi_event_stream, ...)
local plugin_file, label = file:match("^([^:]+):(.+)")
plugin_file = plugin_file or file
@@ -704,6 +713,8 @@ function DSSIStream:ctor(file, midi_event_stream, ...)
end
end
+--- Get name of DSSI/LADSPA plugin.
+-- @treturn string
function DSSIStream:getName()
return ffi.string(self.ladspa_descriptor.Name)
end
@@ -838,11 +849,20 @@ function DSSIStream:gtick()
end
end
--- For the Stream method, we just assume that the
--- subject stream is the MIDI stream (DSSI plugin),
--- or first input stream.
--- FIXME: This doesn't work for symbolic
--- port mappings, though.
+--- Apply DSSI or LADSPA plugin to stream.
+-- For a DSSI plugin, this must usually be called on a @{MIDIStream}.
+-- Otherwise, the object stream will be the plugin's first input stream.
+-- You cannot use this method to map the object stream to a symbolic port, though.
+-- @within Class Stream
+-- @string file File name of plugin.
+-- @tab[opt] input_ports
+-- A table defining the mapping from
+-- LADSPA port names to Streams or constants for audio and control input ports.
+-- @param ...
+-- All additional arguments are added to this table as an array,
+-- so port mappings can be specified as a list of arguments as well.
+-- @treturn Stream
+-- @see DSSIStream:new
function Stream:DSSI(file, ...)
return DSSIStream:new(file, self, ...)
end
diff --git a/evdev.lua b/evdev.lua
index dcf5a57..4eba28c 100644
--- a/evdev.lua
+++ b/evdev.lua
@@ -1,3 +1,6 @@
+---
+--- @module applause
+---
local ffi = require "ffi"
local C = ffi.C
@@ -34,8 +37,35 @@ enum applause_evdev_abs {
};
]]
+--- Stream of Evdev (HID device) events.
+-- This allows using ordinary keyboards, trackpads, trackpoints, mice etc. as
+-- real-time controllers.
+-- See also [input-event-codes.h](https://raw.githubusercontent.com/torvalds/linux/master/include/uapi/linux/input-event-codes.h)
+-- for useful constants.
+-- @type EvdevStream
+-- @see MIDIStream
EvdevStream = DeriveClass(Stream)
+--- Create EvdevStream (stream HID device events).
+-- @function new
+-- @tparam int|string id
+-- Either an integer referring to the device node (`/dev/input/eventX`) or
+-- a Lua pattern matched against the device names.
+-- The first matching device is used.
+-- @bool[opt=true] grab
+-- Whether to "grab" the device.
+-- A "grabbed" device will get its events delivered exclusively to Applause,
+-- so it will not interfer with the operating system
+-- (pressed keyboard keys or mouse movement will not cause any trouble).
+-- Consequently there can only be one EvdevStream per grabbed device.
+-- The device will be "ungrabbed" once the object is garbage collected.
+-- @treturn EvdevStream
+-- A stream of C structures, describing the event in detail.
+-- Its fields will be 0 if no event ocurred.
+-- @todo Document the C structure as a Lua table.
+-- @see Stream:evrel
+-- @see Stream:evabs
+-- @see Stream:evkey
function EvdevStream:ctor(id, grab)
local grab = grab == nil or grab
@@ -81,10 +111,22 @@ function EvdevStream:gtick()
end
end
--- Relative devices like some mouses and the trackpoint.
--- The coordinate is returned in `resolution` steps between [-1, 1]
--- NOTE: `code` is optional (default: REL_X) and you can specify a number (0 or 1)
--- or string ('REL_X', 'REL_Y') as well.
+--- Filter Evdev event stream to get the last value of a device with relative positioning (EV_REL).
+-- This can be used to retrieve the position of mice for instance.
+-- The relative movements are automatically converted to absolute positions given a resolution and
+-- the stream "holds" the last value.
+-- It is usally applied on streams returned by @{EvdevStream:new}.
+-- @within Class Stream
+-- @tparam[opt='REL_X'] applause_evdev_rel|int|string code
+-- This is a C value, integer or string ('REL_X', 'REL_Y'...), specifying the axis to extract.
+-- The possible values correspond to the C header `input-event-codes.h`.
+-- @number[opt=1000] resolution
+-- The device resolution, ie. the number of steps between [-1,1].
+-- The larger this value, the longer it takes to move from the minimum to the maximum position.
+-- @treturn Stream Stream of numbers between [-1,1].
+-- @see EvdevStream:new
+-- @see Stream:scale
+-- @usage EvdevStream("TrackPoint"):evrel():scale(440,880):SinOsc():play()
function Stream:evrel(code, resolution)
code = ffi.cast("enum applause_evdev_rel", code)
resolution = resolution or 1000
@@ -97,7 +139,23 @@ function Stream:evrel(code, resolution)
end) / (resolution/2) - 1
end
--- FIXME: min and max can be read from the properties of the corresponding code.
+--- Filter Evdev event stream to get the last value of a device with absolute positioning (EV_ABS).
+-- This can be used to retrieve the position of touchpads for instance.
+-- @within Class Stream
+-- @tparam[opt='ABS_X'] applause_evdev_abs|int|string code
+-- This is a C value, integer or string ('ABS_X', 'ABS_Y'...), specifying the axis to extract.
+-- The possible values correspond to the C header `input-event-codes.h`.
+-- @int min
+-- Minimum value (absolute position).
+-- This will currently have to be looked up manually using `evtest`.
+-- @int max
+-- Maximum value (absolute position).
+-- This will currently have to be looked up manually using `evtest`.
+-- @treturn Stream Stream of numbers between [-1,1].
+-- @see EvdevStream:new
+-- @see Stream:scale
+-- @usage EvdevStream("TouchPad"):evabs('ABS_X', 1232, 5712):scale(440,880):SinOsc():play()
+-- @fixme min and max can be read from the properties of the corresponding code.
-- This would however require us to do all postprocessing already in EvdevStream:gtick()
-- or even in applause_evdev_new().
function Stream:evabs(code, min, max)
@@ -110,8 +168,18 @@ function Stream:evabs(code, min, max)
end)
end
--- FIXME: We haven't got constants for all KEY_X constants,#
--- so you will have to look up the key code in input-event-codes.h.
+--- Filter Evdev event stream to get the last key code (EV_KEY).
+-- This can be used to turn PC keyboards into controllers.
+-- The key code will be generated as long as the key is pressed,
+-- otherwise it will be 0.
+-- @within Class Stream
+-- @int key
+-- The key code to extract.
+-- This must currently always be an integer, corresponding to the
+-- `KEY_X` constants from `input-event-codes.h`.
+-- @treturn Stream Stream of key codes or 0 if the key is not currently pressed.
+-- @see EvdevStream:new
+-- @usage EvdevStream(10):evkey(16):instrument(Stream.SinOsc(440)):play()
function Stream:evkey(key)
return self:scan(function(last, sample)
last = last or 0
diff --git a/filters.lua b/filters.lua
index e9b2900..3deb790 100644
--- a/filters.lua
+++ b/filters.lua
@@ -1,86 +1,15 @@
---[==[
---
--- Non-working FIR filters (FIXME)
---
-
--- Normalized Sinc function
-local function Sinc(x)
- return x == 0 and 1 or
- math.sin(2*math.pi*x)/(2*math.pi*x)
-end
-
-local function Hamming(n, window)
- local alpha = 0.54
- return alpha - (1-alpha)*math.cos((2*math.pi*n)/(window-1))
-end
-local function Blackman(n, window)
- local alpha = 0.16
- return (1-alpha)/2 -
- 0.5*math.cos((2*math.pi*n)/(window-1)) +
- alpha*0.5*math.cos((4*math.pi*n)/(window-1))
-end
-
-FIRStream = DeriveClass(Stream)
-
-function FIRStream:ctor(stream, freq_stream)
- self.stream = tostream(stream)
- self.freq_stream = tostream(freq_stream)
-end
-
-function FIRStream:gtick()
- local window = {}
-
- -- window size (max. 1024 samples)
- -- this is the max. latency introduced by the filter
- -- since the window must be filled before we can generate
- -- (filtered) samples
- local window_size = math.min(1024, self.stream:len())
- local window_p = window_size-1
- local accu = 0
-
- local blackman = {}
- for i = 1, window_size do blackman[i] = Blackman(i-1, window_size) end
-
- local tick = self.stream:gtick()
- local freq_tick = self.freq_stream:gtick()
-
- return function()
- -- fill buffer (initial)
- while #window < window_size-1 do
- table.insert(window, tick())
- end
-
- window[window_p+1] = tick()
- window_p = (window_p + 1) % window_size
-
- local period = freq_tick()/samplerate
-
- local sample = 0
- local i = window_p
- repeat
- -- FIXME
- sample = sample + window[(i % window_size)+1] *
- Sinc((i-window_p - window_size/2)/period) *
- blackman[i-window_p+1]
- i = i + 1
- until (i % window_size) == window_p
-
- return sample
- end
-end
-
-function FIRStream:len()
- return self.stream:len()
-end
-]==]
+---
+--- @module applause
+---
--
-- General-purpose IIR filters:
-- These are direct translations of ChucK's LPF, HPF, BPF and BRF
-- ugens which are in turn adapted from SuperCollider 3.
+-- See also https://chuck.stanford.edu/doc/program/ugen_full.html#LPF
--
--- De-denormalize function adapted from ChucK.
+--- De-denormalize function adapted from ChucK.
-- Not quite sure why this is needed - properly to make the
-- IIR filters numerically more stable.
local function ddn(f)
@@ -88,6 +17,9 @@ local function ddn(f)
(f < -1e-15 and f > -1e15 and f or 0)
end
+--- Low Pass Filter streams.
+-- @type LPFStream
+-- @local
LPFStream = DeriveClass(MuxableStream)
function LPFStream:muxableCtor(stream, freq)
@@ -147,6 +79,11 @@ function LPFStream:len()
return self.stream:len()
end
+--- Apply Low Pass Filter to stream.
+-- This is a resonant filter (2nd order Butterworth).
+-- @within Class Stream
+-- @StreamableNumber freq Cutoff frequency.
+-- @treturn Stream
function Stream:LPF(freq)
return LPFStream:new(self, freq)
end
@@ -213,12 +150,15 @@ function HPFStream:len()
return self.stream:len()
end
+--- Apply High Pass Filter to stream.
+-- This is a resonant filter (2nd order Butterworth).
+-- @within Class Stream
+-- @StreamableNumber freq Cutoff frequency.
+-- @treturn Stream
function Stream:HPF(freq)
return HPFStream:new(self, freq)
end
--- NOTE: The quality factor, indirectly proportional
--- to the passband width
BPFStream = DeriveClass(MuxableStream)
function BPFStream:muxableCtor(stream, freq, quality)
@@ -284,12 +224,16 @@ function BPFStream:len()
return self.stream:len()
end
+--- Apply Band Pass Filter to stream (2nd order Butterworth).
+-- @within Class Stream
+-- @StreamableNumber freq Center frequency.
+-- @StreamableNumber quality
+-- The quality factor, indirectly proportional to the passband width.
+-- @treturn Stream
function Stream:BPF(freq, quality)
return BPFStream:new(self, freq, quality)
end
--- NOTE: The quality factor, indirectly proportional
--- to the passband width
BRFStream = DeriveClass(MuxableStream)
function BRFStream:muxableCtor(stream, freq, quality)
@@ -356,6 +300,88 @@ function BRFStream:len()
return self.stream:len()
end
+--- Apply Band Reject Filter to stream (2nd order Butterworth).
+-- @within Class Stream
+-- @StreamableNumber freq Center frequency.
+-- @StreamableNumber quality
+-- The quality factor, indirectly proportional to the rejectband width.
+-- @treturn Stream
function Stream:BRF(freq, quality)
return BRFStream:new(self, freq, quality)
end
+
+--[==[
+--
+-- Non-working FIR filters (FIXME)
+--
+
+-- Normalized Sinc function
+local function Sinc(x)
+ return x == 0 and 1 or
+ math.sin(2*math.pi*x)/(2*math.pi*x)
+end
+
+local function Hamming(n, window)
+ local alpha = 0.54
+ return alpha - (1-alpha)*math.cos((2*math.pi*n)/(window-1))
+end
+local function Blackman(n, window)
+ local alpha = 0.16
+ return (1-alpha)/2 -
+ 0.5*math.cos((2*math.pi*n)/(window-1)) +
+ alpha*0.5*math.cos((4*math.pi*n)/(window-1))
+end
+
+FIRStream = DeriveClass(Stream)
+
+function FIRStream:ctor(stream, freq_stream)
+ self.stream = tostream(stream)
+ self.freq_stream = tostream(freq_stream)
+end
+
+function FIRStream:gtick()
+ local window = {}
+
+ -- window size (max. 1024 samples)
+ -- this is the max. latency introduced by the filter
+ -- since the window must be filled before we can generate
+ -- (filtered) samples
+ local window_size = math.min(1024, self.stream:len())
+ local window_p = window_size-1
+ local accu = 0
+
+ local blackman = {}
+ for i = 1, window_size do blackman[i] = Blackman(i-1, window_size) end
+
+ local tick = self.stream:gtick()
+ local freq_tick = self.freq_stream:gtick()
+
+ return function()
+ -- fill buffer (initial)
+ while #window < window_size-1 do
+ table.insert(window, tick())
+ end
+
+ window[window_p+1] = tick()
+ window_p = (window_p + 1) % window_size
+
+ local period = freq_tick()/samplerate
+
+ local sample = 0
+ local i = window_p
+ repeat
+ -- FIXME
+ sample = sample + window[(i % window_size)+1] *
+ Sinc((i-window_p - window_size/2)/period) *
+ blackman[i-window_p+1]
+ i = i + 1
+ until (i % window_size) == window_p
+
+ return sample
+ end
+end
+
+function FIRStream:len()
+ return self.stream:len()
+end
+]==]
diff --git a/midi.lua b/midi.lua
index 4025fe5..bf24489 100644
--- a/midi.lua
+++ b/midi.lua
@@ -1,11 +1,38 @@
+---
+--- @module applause
+---
local bit = require "bit"
local ffi = require "ffi"
local C = ffi.C
cdef_include "midi.h"
+--- MIDI event stream.
+-- Since it does not have parameters, you don't necessarily have to instantiate it.
+-- The class table itself is a valid stream object.
+--
+-- MIDI events are 24-bit words with the
+-- [following structure](https://www.codecademy.com/resources/docs/markdown/tables):
+--
+-- | Bit 23-16 | 15-8 | 7-4 | 3-0 |
+-- | --------- | ---- | --- | --- |
+-- | Controller value / velocity | Controller number / key | Command code | Channel number |
+--
+-- MIDI streams can and are usually ticked at the sample rate but will generate 0 words
+-- in absence of events.
+--
+-- @type MIDIStream
+-- @usage Stream.SinOsc(440):gain(MIDIStream:CC(0):ccscale()):play()
+-- @fixme Theoretically, we could pass on a C structure as well.
+-- On the other hand, bit manipulations are necessary anyway to parse the message.
MIDIStream = DeriveClass(Stream)
+--- Create new MIDIStream
+-- @function new
+-- @treturn MIDIStream
+-- @see Stream:CC
+-- @see Stream:mvelocity
+
function MIDIStream:gtick()
return function()
-- This is always cached since there is only one MIDI event queue
@@ -19,7 +46,16 @@ function MIDIStream:gtick()
end
end
--- Last value of a specific control channel
+--- Filter out last value of a specific MIDI control channel.
+-- This remembers the last value and is therefore a stream, representing the controller state.
+-- It is usually applied on @{MIDIStream}.
+-- @within Class Stream
+-- @int control Controller number between [0,127].
+-- @int[opt=0] channel MIDI channel between [0,15].
+-- @treturn Stream Stream of numbers between [0,127].
+-- @see MIDIStream
+-- @see Stream:ccscale
+-- @usage Stream.SinOsc(440):gain(MIDIStream:CC(0):ccscale()):play()
function Stream:CC(control, channel)
channel = channel or 0
@@ -39,9 +75,16 @@ function Stream:CC(control, channel)
end)
end
--- same as Stream:scale() but for values between [0, 127]
--- (ie. MIDI CC values)
--- FIXME: If Stream:CC() would output between [-1, 1], there would be no need
+--- Scale MIDI controller value.
+-- This is very similar to @{Stream:scale} but works for input values between [0, 127]
+-- (ie. MIDI CC values).
+-- @within Class Stream
+-- @StreamableNumber[opt=0] v1 Delivers the lower value.
+-- @StreamableNumber v2 Delivers the upper value
+-- @treturn Stream
+-- @see Stream:CC
+-- @see Stream:scale
+-- @fixme If Stream:CC() would output between [-1,1], there would be no need
-- for Stream:ccscale().
function Stream:ccscale(v1, v2)
local lower = v2 and v1 or 0
@@ -56,9 +99,19 @@ function Stream:ccscale(v1, v2)
end
end
--- Velocity of NOTE ON for a specific note on a channel
+--- Filter out last value of a MIDI note velocity.
+-- This will be a stream of velocity values as long as the given note is on and otherwise 0.
+-- It is usually applied on @{MIDIStream}.
+-- @within Class Stream
+-- @tparam string|int note A MIDI note name (eg. "A4") or number between [0,127].
+-- @int[opt=0] channel MIDI channel between [0,15].
+-- @treturn Stream
+-- Stream of velocities between [0,127].
+-- The note is on as long as the value is not 0.
+-- @see MIDIStream
+-- @see ntom
+-- @usage Stream.SinOsc(ntof("C4")):gain(MIDIStream:mvelocity("C4") / 127):play()
function Stream:mvelocity(note, channel)
- -- `note` may be a note name like "A4"
note = type(note) == "string" and ntom(note) or note
channel = channel or 0
@@ -81,6 +134,10 @@ function Stream:mvelocity(note, channel)
end)
end
+---
+--- @section end
+---
+
--
-- MIDI primitives
--
@@ -94,14 +151,22 @@ do
"F#", "G", "G#", "A", "A#", "B"
}
- -- MIDI note number to name
- -- NOTE: mton() can handle the words as generated by MIDINoteStream
+ --- Convert MIDI note number to name.
+ -- See also the [conversion table](https://newt.phys.unsw.edu.au/jw/notes.html).
+ -- @int note MIDI note number.
+ -- @treturn string MIDI note name in all upper case.
+ -- @see Stream:mton
+ -- @see ntom
function mton(note)
note = band(note, 0xFF)
local octave = floor(note / 12)-1
return note_names[(note % 12)+1]..octave
end
+ --- Convert stream of MIDI note numbers to names.
+ -- @within Class Stream
+ -- @treturn Stream
+ -- @see mton
function Stream:mton() return self:map(mton) end
local ntom_offsets = {}
@@ -112,12 +177,21 @@ do
ntom_offsets[name:lower()] = i-1
end
- -- Note name to MIDI note number
+ --- Convert MIDI note name to number.
+ -- See also the [conversion table](https://newt.phys.unsw.edu.au/jw/notes.html).
+ -- @string name MIDI note name (case insensitive).
+ -- @treturn int MIDI note number between [0,127].
+ -- @see Stream:ntom
+ -- @see mton
function ntom(name)
local octave = name:byte(-1) - 48 + 1
return octave*12 + ntom_offsets[name:sub(1, -2)]
end
+ --- Convert stream of MIDI note names to numbers.
+ -- @within Class Stream
+ -- @treturn Stream
+ -- @see ntom
function Stream:ntom() return self:map(ntom) end
-- There are only 128 possible MIDI notes,
@@ -130,40 +204,68 @@ do
mtof_cache[note] = 440 * 2^((note - 69)/12)
end
- -- Convert from MIDI note to frequency
- -- NOTE: mtof() can handle the words as generated by MIDINoteStream
+ --- Convert from MIDI note to frequency.
+ -- See also the [conversion table](https://newt.phys.unsw.edu.au/jw/notes.html).
+ -- @int note MIDI note number between [0,127].
+ -- @treturn number Frequency
+ -- @see Stream:mtof
+ -- @see ftom
function mtof(note)
return mtof_cache[band(note, 0xFF)]
end
+ --- Convert stream of MIDI note numbers to frequencies.
+ -- @within Class Stream
+ -- @treturn Stream
+ -- @see mtof
function Stream:mtof() return self:map(mtof) end
- -- Convert from frequency to closest MIDI note
+ --- Convert from frequency to closest MIDI note.
+ -- @number freq Arbitrary frequency.
+ -- @treturn int Closest MIDI note number.
+ -- @see Stream:ftom
+ -- @see mtof
function ftom(freq)
-- NOTE: math.log/2 is a LuaJIT extension
return floor(12*log(freq/440, 2) + 0.5)+69
end
+ --- Convert stream of frequencies to closest MIDI note numbers.
+ -- @within Class Stream
+ -- @treturn Stream
+ -- @see ftom
function Stream:ftom() return self:map(ftom) end
end
--- Convert from MIDI name to frequency
+--- Convert from MIDI name to frequency.
+-- @string name MIDI name (case-insensitive).
+-- @treturn number Frequency
+-- @see Stream:ntof
+-- @see ntom
+-- @see mtof
function ntof(name) return mtof(ntom(name)) end
+
+--- Convert stream of MIDI names to frequencies.
+-- @within Class Stream
+-- @treturn Stream
+-- @see ntof
+-- @usage =tostream{"A4", "B4", "C4"}:ntof()
function Stream:ntof() return self:map(ntof) end
--- Convert from frequency to closest MIDI note name
+--- Convert from frequency to closest MIDI note name.
+-- @number freq Frequency
+-- @treturn string The all-upper-case MIDI note name.
+-- @see Stream:fton
+-- @see ftom
+-- @see mton
function fton(freq) return mton(ftom(freq)) end
+
+--- Convert stream of frequencies to closest MIDI note names.
+-- @within Class Stream
+-- @treturn Stream
+-- @see fton
function Stream:fton() return self:map(fton) end
--- Tick an instrument only when an inputstream (note_stream),
--- gets ~= 0. When it changes back to 0 again, an "off"-stream
--- is triggered. This allows the construction of instruments with
--- Attack-Sustain and Decay phases based on real-time control signals.
--- The note values can be passed into the constructor by using functions
--- for the "on" and "off" streams.
--- Usually, the note stream will be a MIDIStream:mvelocity(), so the two
--- instrument streams can be based on the MIDI velocity (but don't have
--- to be if the velocity is not important).
InstrumentStream = DeriveClass(MuxableStream)
InstrumentStream.sig_last_stream = 1
@@ -234,6 +336,26 @@ function InstrumentStream:len()
return self.note_stream:len()
end
+--- Tick instrument streams based on an input stream.
+-- Once the input stream is unequal to 0, the "on"-stream is activated.
+-- When it changes back to 0 again, the "off"-stream gets triggered.
+-- This allows the construction of instruments with
+-- Attack-Sustain and Decay phases based on real-time control signals.
+-- Usually, the instrument stream will be applied on @{Stream:mvelocity}, so the two
+-- instrument streams can be based on the MIDI velocity (but don't have
+-- to be if the velocity is not important).
+-- @within Class Stream
+-- @tparam StreamableNumber|func on_stream
+-- Stream to trigger when the input value becomes unequal to 0.
+-- If this is a function, it will be called with the input value (usually a MIDI velocity)
+-- to generate the "on"-stream.
+-- @tparam[opt] StreamableNumber|func off_stream
+-- Stream to trigger when the input value becomes 0.
+-- If this is a function, it will be called with the previous input value (usually a MIDI velocity)
+-- to generate the "off"-stream.
+-- @treturn Stream
+-- @see Stream:mvelocity
+-- @usage MIDIStream:mvelocity("C4"):instrument(Stream.SinOsc(ntof("C4"))):play()
function Stream:instrument(on_stream, off_stream)
return InstrumentStream:new(self, on_stream, off_stream)
end
diff --git a/sndfile-stream.lua b/sndfile-stream.lua
index 68ed5cc..248f82a 100644
--- a/sndfile-stream.lua
+++ b/sndfile-stream.lua
@@ -1,12 +1,31 @@
+---
+--- @module applause
+---
local sndfile = require "sndfile"
+--- Stream for reading a sound file.
+-- This can be used with all file types supported by [libsndfile](http://www.mega-nerd.com/libsndfile/#Features).
+-- @type SndfileStream
SndfileStream = DeriveClass(Stream)
+--- Create a SndfileStream.
+-- @function new
+-- @string filename Filename of sound file to read.
+-- @int[opt] samplerate
+-- Samplerate of file.
+-- This is ignored unless reading files in ffi.C.SF_FORMAT_RAW.
+-- @int[opt] channels
+-- Number of channels in file.
+-- This is ignored unless reading files in ffi.C.SF_FORMAT_RAW.
+-- @tparam[opt] SF_FORMAT format
+-- Format of file to read ([C type](http://www.mega-nerd.com/libsndfile/api.html)).
+-- If omitted, this guessed from the file extension.
+-- @treturn SndfileStream|MuxStream
+-- @see Stream:save
+-- @usage SndfileStream("test.wav"):play()
+-- @fixme This fails if the file is not at the correct sample rate.
+-- Need to resample...
function SndfileStream:ctor(filename, samplerate, channels, format)
- -- FIXME: This fails if the file is not at the
- -- correct sample rate. Need to resample...
- -- NOTE: samplerate and channels are ignored unless SF_FORMAT_RAW
- -- files are read.
local handle = sndfile:new(filename, "SFM_READ",
samplerate, channels, format)
self.filename = filename
@@ -64,9 +83,24 @@ function SndfileStream:gtick()
end
end
-function SndfileStream:len() return self.frames end
+function SndfileStream:len()
+ return self.frames
+end
--- TODO: Use a buffer to improve perfomance (e.g. 1024 samples)
+--- Save stream to sound file.
+-- Naturally, this cannot work with infinite streams.
+-- Since the stream is processed/written as fast as possible,
+-- it is not possible to capture real-time input.
+-- @within Class Stream
+-- @string filename Filename of sound file to write
+-- @tparam[opt] SF_FORMAT format
+-- Format of file to write (C type).
+-- If omitted, this guessed from the file extension.
+-- @see SndfileStream
+-- @fixme It would be useful to have a Stream:save method that works
+-- during playback with real-time input.
+-- In this case, you could do a Stream:save(...):drain().
+-- @todo Use a buffer to improve perfomance (e.g. 1024 samples)
function Stream:save(filename, format)
if self:len() == math.huge then
error("Cannot save infinite stream")
diff --git a/sndfile.lua b/sndfile.lua
index 8f2a5e7..1e6f6b7 100644
--- a/sndfile.lua
+++ b/sndfile.lua
@@ -1,3 +1,7 @@
+---
+--- @classmod sndfile
+---
+
-- module table
local sndfile = {}