commit 937d5422812d6bcae610c7b181b2cf427ede9ed5 Author: Kimapr Date: Mon May 27 15:06:41 2024 +0500 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..993426a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/sound.lua diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f697dfc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ + +Copyright (C)2024 Kimapr + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject +to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d88bc33 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# Sound synthesis playground + +Experiment with sound generation in Lua. + +# Usage + +- Copy `sound.lua.example` to `sound.lua` +- Open `sound.lua` in your text editor (autosave on change recommended) +- Run this with `love .` +- Edit `sound.lua`, and watch (listen) the sound update in real time diff --git a/conf.lua b/conf.lua new file mode 100644 index 0000000..94a9b66 --- /dev/null +++ b/conf.lua @@ -0,0 +1,5 @@ +function love.conf(t) + t.identity="net.kimapr.sgstream" + t.window.title = "sgstream" + t.window.resizable = true +end diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..c1281f0 --- /dev/null +++ b/main.lua @@ -0,0 +1,100 @@ +local rate,bits = 44100,16 +local chunk = 1/4 +local look = 1/4 +local chunkc = 2 +local sour,pos +local emsnd = require("soundgen") + .silence(0) + :clamp(0,chunk) + :compile(rate,bits) +local chunkd = {} +local function samplify(snd) + local chunk = {} + for i=0,snd:getSampleCount()-1 do + chunk[i] = snd:getSample(i,1) + end + chunkd[#chunkd+1] = chunk + return snd +end +local sndc = love.thread.getChannel("snd") +local ctrl = love.thread.getChannel("ctrl") +local function push(c,v) + ctrl:supply(c) + ctrl:push(v) +end +local offset = 0 +local function init() + sour = love.audio.newQueueableSource(rate,bits,1,chunkc) + pos = 0 + chunkd={} + for n=1,chunkc*2 do + samplify(emsnd) + end + push("chunk",chunk) + push("emsnd",emsnd) + push("pos",pos) + push("look",look) + push("rate",rate) + push("bits",bits) + while offset>0 do + local c = sndc:demand() + local v = sndc:demand() + if c == "snd" then + offset=offset-1 + end + end +end +function love.load() + love.thread.newThread("thread.lua"):start() + init() + sour:play() +end +function love.keypressed(key) + if key == "r" then + sour:pause() + sour:stop() + init() + end +end +function love.update(dt) + for n=1,sour:getFreeBufferCount()-offset do + push("snd",true) + offset=offset+1 + table.remove(chunkd,1) + end + while sndc:getCount()>0 do + local c = sndc:demand() + local v = sndc:demand() + if c == "snd" then + sour:queue(samplify(v)) + sour:play() + offset=offset-1 + elseif c == "pos" then + pos = v + elseif c == "look" then + look = v + else + error("unkc: "..tostring(c)) + end + end +end +function love.draw() + local w,h = love.graphics.getDimensions() + local line = {} + local sx = math.floor(((chunkc+sour:getFreeBufferCount()-offset)*chunk+sour:tell())*rate) + local rr = emsnd:getSampleCount() + for t=0,w do + local x = t/w + x=x*2-1 + x=x*rate*look + x=sx+x + line[#line+1]=t + x=math.floor(x+.5) + local si,ii = math.floor(x/rr)+1,x%rr + line[#line+1]=(((chunkd[si]or{})[ii]or 0)*0.5+0.5)*h + end + love.graphics.setColor(1,1,1,0.1) + love.graphics.line(w/2,0,w/2,h) + love.graphics.setColor(1,1,1) + love.graphics.line(line) +end diff --git a/sound.lua.example b/sound.lua.example new file mode 100644 index 0000000..0fdced9 --- /dev/null +++ b/sound.lua.example @@ -0,0 +1,38 @@ +local sg = require "soundgen" +sg=(function(sg) + local obj = setmetatable({},sg) + obj.__index = obj + obj.proto = obj + -- custom methods here + return obj +end)(sg) + +-- throw a bunch of random things together +local snd = sg:sine():freq(250) + :pm(sg:sine():freq(250/6):fade(0,0.3):amp(1/250),-1,1) +snd=snd:amp(0.8):freq(2):add( + sg:sine():freq(250) + :pm(sg:sine():freq(250),0,1/250):amp(0.3)) +snd=snd:freq(1) + :pm(snd:phase(0):fade(0.3,0.0),0,0.001) + :freq(0.5) + +-- fade in, fade out, reverb +snd=snd + :fade(0.01,0) + :fade(0,0.3) + :amp(0.5) + :reverb(4,1/250*30,0.4) + +-- compose +local ssnd = sg.add( + snd:freq(1):phase(0), + snd:freq(0.99):phase(-0.5), + snd:freq(1.01):phase(-1), + snd:freq(1):phase(-1.4), + snd:freq(1.1):phase(-1.8) +):clamp(0,2.1,true):loop():amp(1) + +return + ssnd, -- sound + 1/250*6 -- oscillogram lookaround diff --git a/soundgen.lua b/soundgen.lua new file mode 100644 index 0000000..963486a --- /dev/null +++ b/soundgen.lua @@ -0,0 +1,259 @@ +--[[-- + +Copyright (C)2023-2024 Kimapr + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject +to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--]]-- + +local smt={} +smt.__index=smt +smt.proto=smt + +local function round(x) + return math.floor(x+.5) +end + +function smt:bounds_and(a,b,c,...) + if not a or not b then return false end + local mi,ma + mi=math.max(a[1],b[1]) + ma=math.min(a[2],b[2]) + if ma-mi<=0 then return false end + if c then + return self:bounds_and({mi,ma},c,...) + end + return {mi,ma} +end + +function smt:bounds_or(a,b,c,...) + local mi,ma=math.min(a[1],b[1]),math.max(a[2],b[2]) + if not a then return b end + if not b then return a end + if c then + return self:bounds_or({mi,ma},c,...) + end + return {mi,ma} +end + +function smt:new(fn,bounds) + local obj={} + obj.bounds=bounds + if obj.bounds==nil then + obj.bounds={-math.huge,math.huge} + end + obj.sample=fn + return setmetatable(obj,self.proto) +end + +function smt:sine() + return self:new(function(self,t) + return math.sin(t*math.pi*2) + end) +end + +function smt:square() + return self:new(function(self,t) + return (t%1)<=0.5 and -1 or 1 + end) +end + +function smt:sawtooth() + return self:new(function(self,t) + return (t%1)*2-1 + end) +end + +function smt:triangle() + return self:new(function(self,t) + --return ((t%1)<=0.5 and (t*2)%1 or 1-(t*2)%1)*2-1 -- sounds cool and glitchy but wrong + local a=t%1 + if a>0.5 then + return (1-(a*2-1))*2-1 + end + return (a*2)*2-1 + end) +end + +function smt:noise(y,z,x) + return self:new(function(self,t) + return love.math.noise(t,y,z,x)*2-1 + end) +end + +function smt:silence(a) + a=a or 0 + return smt:new(function()return a end) +end + +local function rated_bounds(bounds,rate) + local mi,ma=bounds[1],bounds[2] + mi,ma=mi*rate,ma*rate + mi,ma=round(mi),round(ma)-1 + return mi,ma +end + +local function prefi(y) + return math.min(1,math.max(-1,y)) +end + +function smt:compile(rate,bits,alt) + rate=rate or 44100 + bits=bits or 16 + local bounds=self.bounds + local mi,ma=rated_bounds(bounds,rate) + if alt then + bounds=self:bounds_and(bounds,alt.bounds) + local mi,ma=rated_bounds(bounds,rate) + local sdata=love.sound.newSoundData(ma-mi+1,rate,bits,2) + for x=mi,ma do + local x=x+mi + sdata:setSample(x*2,prefi(self:sample(x/rate+0.5/rate))) + sdata:setSample(x*2+1,prefi(alt:sample(x/rate+0.5/rate))) + end + return sdata + end + local sdata=love.sound.newSoundData(ma-mi+1,rate,bits,1) + for x=mi,ma do + local xx=x-mi + sdata:setSample(xx,prefi(self:sample(x/rate+0.5/rate))) + end + return sdata +end + +function smt:phase(p) + local bb=self.bounds + if bb then + bb={bb[1]-p,bb[2]-p} + end + return self:new(function(obj,t) + return self:sample(t+p) + end,bb) +end + +function smt:amp(a) + return self:new(function(obj,t) + return self:sample(t)*a + end,self.bounds) +end + +function smt:freq(f) + local bb=self.bounds + if bb then + bb={bb[1]/f,bb[2]/f} + if bb[2]ma then return 0 end + return self.sample(self,t) + end,extend and {mi,ma} or bounds or (mi=self.bounds[1] and tt<=self.bounds[2] then + s = s + self:sample(t-n*dist)*(mul^n) + end + end + return s + end,{self.bounds[1],self.bounds[2]+times*dist}) +end + +function smt.fm(car,mod,rate,mi,ma) -- TODO + local cframe,cache=0,{} + return smt.new(function(self,t) + end,mod.bounds) +end + +return smt diff --git a/thread.lua b/thread.lua new file mode 100644 index 0000000..bce2420 --- /dev/null +++ b/thread.lua @@ -0,0 +1,65 @@ +require "love.sound" +require "love.thread" +require "love.math" +require "love.timer" +local ctrl = love.thread.getChannel("ctrl") +local sndc = love.thread.getChannel("snd") +local pos,look,chunk,rate,bits +local emsnd +local function send(c,v) + sndc:push(c) + sndc:push(v) +end +local function go() + local t = love.timer.getTime() + local ok,err,x = xpcall(function() + local snd,x = dofile("sound.lua") + snd = snd:phase(pos):clamp(0,chunk,true) + snd = assert(snd:compile(rate,bits),"no sound") + assert(snd:typeOf("SoundData"),"fake sound") + return snd,x + end,debug.traceback) + look = tonumber(x) or look + sndc:performAtomic(function() + send("look",look) + if not ok then + send("snd",emsnd) + print(err) + else + send("snd",err) + end + pos=pos+chunk + send("pos",pos) + end) + t=love.timer.getTime()-t + local chnkcc=16 + local pgb=math.floor(t/chunk*chnkcc+.5) + print(("time: %.2f..%.2f\tload: %.2f%%\t[%s%s%s]"):format( + pos-chunk,pos, + math.floor(t/chunk*100+.5), + ("#"):rep(math.min(pgb,chnkcc)), + ("."):rep(math.max(0,chnkcc-pgb)), + ("!"):rep(math.max(0,-(chnkcc-pgb))) + )) +end +while true do + local cmd = ctrl:demand() + local v = ctrl:demand() + if cmd == "pos" then + pos = v + elseif cmd == "look" then + look = v + elseif cmd == "chunk" then + chunk = v + elseif cmd == "emsnd" then + emsnd = v + elseif cmd == "rate" then + rate = v + elseif cmd == "bits" then + bits = v + elseif cmd == "snd" then + go() + else + error("unkc: "..tostring(cmd)) + end +end