initial
This commit is contained in:
commit
937d542281
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/sound.lua
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -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.
|
10
README.md
Normal file
10
README.md
Normal file
|
@ -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
|
5
conf.lua
Normal file
5
conf.lua
Normal file
|
@ -0,0 +1,5 @@
|
|||
function love.conf(t)
|
||||
t.identity="net.kimapr.sgstream"
|
||||
t.window.title = "sgstream"
|
||||
t.window.resizable = true
|
||||
end
|
100
main.lua
Normal file
100
main.lua
Normal file
|
@ -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
|
38
sound.lua.example
Normal file
38
sound.lua.example
Normal file
|
@ -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
|
259
soundgen.lua
Normal file
259
soundgen.lua
Normal file
|
@ -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]<bb[1] then
|
||||
bb={bb[2],bb[1]}
|
||||
end
|
||||
end
|
||||
return self:new(function(obj,t)
|
||||
return self:sample(t*f)
|
||||
end,bb)
|
||||
end
|
||||
|
||||
function smt:clamp(mi,ma,extend)
|
||||
local bounds = self:bounds_and(self.bounds,{mi,ma})
|
||||
return self:new(function(nself,t)
|
||||
if t<mi or t>ma then return 0 end
|
||||
return self.sample(self,t)
|
||||
end,extend and {mi,ma} or bounds or (mi<self.bounds[1]
|
||||
and {self.bounds[1],self.bounds[1]}
|
||||
or {self.bounds[2],self.bounds[2]}))
|
||||
end
|
||||
|
||||
function smt:loop()
|
||||
if self.bounds[2]-self.bounds[1] == math.huge then
|
||||
return self
|
||||
end
|
||||
local mi,ma = self.bounds[1],self.bounds[2]
|
||||
return smt:new(function(_,t)
|
||||
return self:sample((t-mi)%ma+mi)
|
||||
end)
|
||||
end
|
||||
|
||||
function smt.am(car,mod,mi,ma)
|
||||
return car:new(function(self,t)
|
||||
return car:sample(t)*((mod:sample(t)*0.5+0.5)*(ma-mi)+mi)
|
||||
end,car:bounds_and(car.bounds,mod.bounds))
|
||||
end
|
||||
|
||||
function smt.pm(car,mod,mi,ma)
|
||||
assert(car.bounds
|
||||
and car.bounds[1]==-math.huge
|
||||
and car.bounds[2]==math.huge,
|
||||
"infinite carrier expected")
|
||||
mod=mod:add(mod:silence())
|
||||
return car:new(function(self,t)
|
||||
return car:sample(t+((mod:sample(t)*0.5+0.5)*(ma-mi)+mi))
|
||||
end,car.bounds)
|
||||
end
|
||||
|
||||
function smt:add(a,b,...)
|
||||
if self.sample then
|
||||
return self.proto:add(self,a,b,...)
|
||||
end
|
||||
if not b then
|
||||
if not a then return self:silence() end
|
||||
return a
|
||||
end
|
||||
local ls={a,b,...}
|
||||
local bbs={}
|
||||
for k,v in ipairs(ls) do
|
||||
bbs[k]=v.bounds
|
||||
end
|
||||
return self:new(function(self,t)
|
||||
local r=0
|
||||
for k=1,#ls do
|
||||
local v = ls[k]
|
||||
r=r+v:sample(t)
|
||||
end
|
||||
return r
|
||||
end,a:bounds_or(unpack(bbs)))
|
||||
end
|
||||
|
||||
do
|
||||
local fwav=smt:sine():phase(0.75):freq(0.25)
|
||||
fwav=fwav:clamp(0,1)
|
||||
fwav=fwav:add(smt:silence(-1):clamp(-math.huge,0))
|
||||
function smt.fade(wave,ma,mi)
|
||||
local ff=fwav:freq(1/(mi-ma)):phase(-ma)
|
||||
return wave:am(ff,1,-1)
|
||||
end
|
||||
end
|
||||
|
||||
function smt:reverb(times,dist,mul)
|
||||
return self:new(function(_,t)
|
||||
local s = 0
|
||||
for n=0,times-1 do
|
||||
local tt = t-n*dist
|
||||
if tt>=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
|
65
thread.lua
Normal file
65
thread.lua
Normal file
|
@ -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
|
Loading…
Reference in a new issue