aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--applause.lua462
1 files changed, 462 insertions, 0 deletions
diff --git a/applause.lua b/applause.lua
new file mode 100644
index 0000000..10da958
--- /dev/null
+++ b/applause.lua
@@ -0,0 +1,462 @@
+posix = require "posix"
+
+-- measure time required to execute fnc()
+function benchmark(fnc)
+ local t1_s, t1_ns = posix.clock_gettime("process_cputime_id")
+ fnc()
+ local t2_s, t2_ns = posix.clock_gettime("process_cputime_id")
+
+ local t1_ms = t1_s*1000 + t1_ns/1000000
+ local t2_ms = t2_s*1000 + t2_ns/1000000
+
+ print("Elapsed CPU time: "..(t2_ms - t1_ms).."ms")
+end
+
+function DeriveClass(base, ctor)
+ local class = {}
+
+ if base then
+ class = base:new()
+
+ -- we cannot derive metamethod tables, so we
+ -- copy all relevant metamethods
+ for _, m in pairs{"len", "call", "tostring",
+ "unm", "add", "sub", "mul", "div",
+ "concat", "lt", "le"} do
+ class["__"..m] = base["__"..m]
+ end
+ end
+
+ -- objects constructed from new class get the
+ -- class table as their metatable, so __index is set up
+ -- to look into the class table
+ function class:__index(key)
+ if type(key) == "string" then return getmetatable(self)[key] end
+
+ -- non-string keys create IndexTables
+ return IndexStream:new(self, key)
+ end
+
+ function class:new(...)
+ local obj = base and base:new() or {}
+
+ setmetatable(obj, self)
+
+ if ctor then ctor(obj, ...) end
+
+ return obj
+ end
+
+ return class
+end
+
+-- Stream base class
+Stream = DeriveClass(nil, function(self, value)
+ self.value = tonumber(value) or 0
+end)
+
+-- used by tostream():
+Stream.is_a_stream = true
+
+-- A stream, produces an infinite number of the same value by default
+-- (eternal quietness by default)
+function Stream:tick()
+ while true do coroutine.yield(self.value) end
+end
+
+function Stream:map(fnc)
+ return MapStream:new(self, fnc)
+end
+
+function Stream:ceil() return self:map(math.ceil) end
+function Stream:cos() return self:map(math.cos) end
+function Stream:cosh() return self:map(math.cosh) end
+function Stream:exp() return self:map(math.exp) end
+function Stream:floor() return self:map(math.floor) end
+function Stream:sin() return self:map(math.sin) end
+function Stream:sinh() return self:map(math.sinh) end
+function Stream:sqrt() return self:map(math.sqrt) end
+function Stream:tan() return self:map(math.tan) end
+function Stream:tanh() return self:map(math.tanh) end
+
+function Stream:sub(i, j)
+ return SubStream:new(self, i, j)
+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 since LuaJIT has problems
+-- with invoking the __len metamethod (# operator).
+function Stream:len()
+ return math.huge -- infinity
+end
+
+-- Stream metamethods
+
+function Stream:__len() return self:len() end
+
+function Stream:__call()
+ if self:len() == math.huge then error("Cannot serialize infinite stream") end
+
+ local co = coroutine.wrap(self.tick)
+ local vector = {}
+
+ while true do
+ local sample = co(self)
+ if not sample then break end
+ table.insert(vector, sample)
+ end
+
+ return vector
+end
+
+function Stream:__tostring()
+ if self:len() > 1024 then
+ return table.concat(self:sub(1, 1024)(), " ").."..."
+ else
+ return table.concat(self(), " ")
+ end
+end
+
+function Stream:__unm()
+ -- should be efficient enough,
+ -- no need to implemenent a NegateStream and
+ -- certainly better than MulStream:new(self, -1)
+ return self:map(function(x) return -x end)
+end
+
+function Stream.__add(op1, op2)
+ return AddStream:new(op1, op2)
+end
+
+function Stream.__sub(op1, op2)
+ -- FIXME: May be made more efficient if we use
+ -- a higher order stream composition
+ return AddStream:new(op1, -tostream(op2))
+end
+
+function Stream.__mul(op1, op2)
+ return MulStream:new(op1, op2)
+end
+
+function Stream.__div(op1, op2)
+ -- FIXME: May be made more efficient if we use
+ -- a higher order stream composition
+ return MulStream:new(op1, MapStream:new(op2, function(x)
+ return 1/x
+ end))
+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?
+
+function Stream.__lt(op1, op2)
+ return op1:len() < op2:len()
+end
+
+function Stream.__le(op1, op2)
+ return op1:len() <= op2:len()
+end
+
+VectorStream = DeriveClass(Stream, function(self, vector)
+ self.vector = vector
+end)
+
+function VectorStream:tick()
+ for _, v in ipairs(self.vector) do
+ coroutine.yield(tonumber(v))
+ end
+end
+
+function VectorStream:len()
+ return #self.vector
+end
+
+ConcatStream = DeriveClass(Stream, function(self, ...)
+ self.streams = {}
+ for k, v in pairs{...} do
+ self.streams[k] = tostream(v)
+ end
+
+ -- all but the last stream must be finite
+ -- (it makes no sense to append something to
+ -- an infinite stream)
+ for i = 1, #self.streams - 1 do
+ if self.streams[i]:len() == math.huge then
+ error("Stream "..i.." is infinite")
+ end
+ end
+end)
+
+function ConcatStream:tick()
+ for _, stream in ipairs(self.streams) do
+ stream:tick()
+ end
+end
+
+function ConcatStream:len()
+ local len = 0
+
+ -- if last stream is infinite, len will also be infinite
+ for _, stream in pairs(self.streams) do
+ len = len + stream:len()
+ end
+
+ return len
+end
+
+IotaStream = DeriveClass(Stream, function(self, v1, v2)
+ if not v2 then
+ self.from = 1
+ self.to = v1 or math.huge
+ else
+ self.from = v1
+ self.to = v2
+ end
+
+ if self.from < 1 or self.to < 1 or
+ self.from > self.to then
+ error("Invalid iota range ["..self.from..","..self.to.."]")
+ end
+end)
+
+function IotaStream:tick()
+ for i = self.from, self.to do coroutine.yield(i) end
+end
+
+function IotaStream:len()
+ return self.to == math.huge and math.huge or
+ self.to - self.from + 1
+end
+
+-- i and j have the same semantics as in string.sub()
+SubStream = DeriveClass(Stream, function(self, stream, i, j)
+ self.stream = tostream(stream)
+ self.i = i
+ self.j = j or -1
+
+ local stream_len = self.stream:len()
+
+ if self.i < 0 then self.i = self.i + stream_len + 1 end
+ if self.j < 0 then self.j = self.j + stream_len + 1 end
+
+ if self.i > stream_len or self.j > stream_len or
+ self.i > self.j then
+ error("Invalid sub-stream range ["..self.i..","..self.j.."]")
+ end
+end)
+
+function SubStream:tick()
+ local co = coroutine.wrap(self.stream.tick)
+
+ -- skip until self.i
+ for _ = 1, self.i - 1 do co(self.stream) end
+
+ -- produce samples i to j
+ for _ = self.i, self.j do coroutine.yield(co(self.stream)) end
+end
+
+function SubStream:len()
+ return self.j == math.huge and math.huge or
+ self.j - self.i + 1
+end
+
+IndexStream = DeriveClass(Stream, function(self, stream, index_stream)
+ self.stream = tostream(stream)
+ self.index_stream = tostream(index_stream)
+end)
+
+function IndexStream:tick()
+ local stream_len = self.stream:len()
+ local co = coroutine.wrap(self.stream.tick)
+ local index_co = coroutine.wrap(self.index_stream.tick)
+
+ -- cache of samples generated by stream
+ local cache = {}
+
+ while true do
+ local index_sample = index_co(self.index_stream)
+ if not index_sample then return end
+
+ if index_sample < 1 or index_sample > stream_len or
+ index_sample == math.huge then
+ error("Index "..index_sample.." out of range")
+ end
+
+ local index_floor, index_ceil = math.floor(index_sample),
+ math.ceil(index_sample)
+
+ while #cache < index_ceil do
+ table.insert(cache, co(self.stream))
+ end
+
+ -- applies linear interpolation if index_sample is
+ -- not an integer
+ coroutine.yield(cache[index_floor] +
+ (cache[index_ceil] - cache[index_floor])*
+ (index_sample - index_floor))
+ end
+end
+
+function IndexStream:len()
+ return self.index_stream:len()
+end
+
+-- FIXME: Perhaps AddStream and MulStream could be unified
+-- into one higher order stream that applies a function to
+-- a number of samples
+
+AddStream = DeriveClass(Stream, function(self, ...)
+ self.streams = {}
+ for k, v in pairs{...} do
+ self.streams[k] = tostream(v)
+ end
+end)
+
+function AddStream:tick()
+ local coroutines = {}
+
+ for k, stream in pairs(self.streams) do
+ coroutines[k] = coroutine.create(stream.tick)
+ end
+
+ while true do
+ local sum = 0
+ local running = false
+
+ for k = 1, #self.streams do
+ if coroutine.status(coroutines[k]) ~= "dead" then
+ local state, sample = coroutine.resume(coroutines[k], self.streams[k])
+ if not state then error(sample) end
+
+ running = running or sample
+ sum = sum + (sample or 0)
+ end
+ end
+ if not running then return end
+
+ coroutine.yield(sum)
+ end
+end
+
+function AddStream:len()
+ local max = 0
+
+ for _, stream in pairs(self.streams) do
+ max = math.max(max, stream:len())
+ end
+
+ return max
+end
+
+MulStream = DeriveClass(Stream, function(self, ...)
+ self.streams = {}
+ for k, v in pairs{...} do
+ self.streams[k] = tostream(v)
+ end
+end)
+
+function MulStream:tick()
+ local coroutines = {}
+
+ for k, stream in pairs(self.streams) do
+ coroutines[k] = coroutine.create(stream.tick)
+ end
+
+ while true do
+ local product = 1
+ local running = false
+
+ for k = 1, #self.streams do
+ if coroutine.status(coroutines[k]) ~= "dead" then
+ local state, sample = coroutine.resume(coroutines[k], self.streams[k])
+ if not state then error(sample) end
+
+ running = running or sample
+ product = product * (sample or 1)
+ end
+ end
+ if not running then return end
+
+ coroutine.yield(product)
+ end
+end
+
+function MulStream:len()
+ local max = 0
+
+ for _, stream in pairs(self.streams) do
+ max = math.max(max, stream:len())
+ end
+
+ return max
+end
+
+MapStream = DeriveClass(Stream, function(self, stream, fnc)
+ self.stream = tostream(stream)
+ self.fnc = fnc
+end)
+
+function MapStream:tick()
+ local co = coroutine.wrap(self.stream.tick)
+
+ while true do
+ local sample = co(self.stream)
+ if not sample then return end
+ coroutine.yield(self.fnc(sample))
+ end
+end
+
+function MapStream:len()
+ return self.stream:len()
+end
+
+-- primitives
+
+function tostream(v)
+ if type(v) == "table" then
+ if v.is_a_stream then return v end
+ -- assume to be vector
+ return VectorStream:new(v)
+ else
+ return Stream:new(v)
+ end
+end
+
+function iota(...) return IotaStream:new(...) end
+
+-- Sample rate
+-- FIXME: Get this from sound backend
+samplerate = 44100
+
+-- Time units: Convert between time and sample numbers
+-- These are functions, so we can round the result
+function seconds(x) return math.floor(samplerate*x) end
+function mseconds(x) return seconds(x/1000) end
+
+-- Wave forms
+function SawOsc(freq)
+ local accu = 0
+
+ return MapStream:new(freq, function(x)
+ accu = (accu + x/samplerate) % 1
+ return accu
+ end)
+end
+
+function SinOsc(freq)
+ -- The following mapping is equivalent to but more efficient as:
+ -- return (SawOsc(freq)*(2*math.pi)):sin()
+
+ local accu = 0
+
+ return MapStream:new(freq, function(x)
+ accu = (accu + x/samplerate) % 1
+ return math.sin(accu*2*math.pi)
+ end)
+end