diff options
author | Robin Haberkorn <robin.haberkorn@googlemail.com> | 2023-09-13 17:26:53 +0300 |
---|---|---|
committer | Robin Haberkorn <robin.haberkorn@googlemail.com> | 2023-09-13 17:26:53 +0300 |
commit | 7fc0f17fb37326c86a83627b856a5b75f522c090 (patch) | |
tree | bc5de8b30c705afafc9b30e6fd3f5bc2b12f19d6 | |
parent | 1cfaa431c1f3c4f417811dcff342c9f28600f13f (diff) | |
download | applause2-7fc0f17fb37326c86a83627b856a5b75f522c090.tar.gz |
added LDoc documentation
* gives a useful overview of everything supported right now
* especially the type documentation is useful, as these things are not self-evident in Lua (because of dynamic typing).
* The LDoc page can later be published as the Github pages of the project.
This can even be done automatically by a Github action.
However, we should first make sure that it's okay to publish the project before defending the thesis since
Github pages will always be public even for private repositories.
* Documentation of command-line parameters is lacking (TODO).
* It may be possible to use types like "Stream(number)" to describe streams of numbers.
The LDoc documentation mentions boxed types.
Perhaps there can even be Streamable(number)?
* We are also lacking good example programs and/or introductory material.
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Makefile | 5 | ||||
-rw-r--r-- | README.md | 11 | ||||
-rw-r--r-- | TODO | 43 | ||||
-rw-r--r-- | applause.lua | 1018 | ||||
-rw-r--r-- | config.ld | 33 | ||||
-rw-r--r-- | dssi.lua | 86 | ||||
-rw-r--r-- | evdev.lua | 82 | ||||
-rw-r--r-- | filters.lua | 186 | ||||
-rw-r--r-- | midi.lua | 168 | ||||
-rw-r--r-- | sndfile-stream.lua | 46 | ||||
-rw-r--r-- | sndfile.lua | 4 |
12 files changed, 1369 insertions, 314 deletions
@@ -2,3 +2,4 @@ *.o /.applause_history +/doc
\ No newline at end of file @@ -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 @@ -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. @@ -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 @@ -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 @@ -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 +]==] @@ -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 = {} |