From 937d5422812d6bcae610c7b181b2cf427ede9ed5 Mon Sep 17 00:00:00 2001 From: Kimapr Date: Mon, 27 May 2024 15:06:41 +0500 Subject: [PATCH] initial --- .gitignore | 1 + LICENSE | 21 ++++ README.md | 10 ++ conf.lua | 5 + main.lua | 100 ++++++++++++++++++ sound.lua.example | 38 +++++++ soundgen.lua | 259 ++++++++++++++++++++++++++++++++++++++++++++++ thread.lua | 65 ++++++++++++ 8 files changed, 499 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 conf.lua create mode 100644 main.lua create mode 100644 sound.lua.example create mode 100644 soundgen.lua create mode 100644 thread.lua 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