diff options
-rw-r--r-- | applause.lua | 462 |
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 |