sfxr.lua/sfxr.lua

463 lines
12 KiB
Lua
Raw Normal View History

2014-04-11 18:16:20 +00:00
-- sfxr.lua
-- original by Tomas Pettersson, ported to Lua by nucular
--[[
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 sfxr = {}
2014-05-06 19:49:27 +00:00
-- Constants
sfxr.SQUARE = 0
sfxr.SAWTOOTH = 1
sfxr.SINE = 2
sfxr.NOISE = 3
-- Utilities
local function trunc(n)
return math.floor(n - 0.5)
end
local function random(low, high)
return low + math.random() * (high - low)
end
local function maybe(n)
return trunc(random(0, n or 1)) == 0
end
2014-05-07 14:54:13 +00:00
local function clamp(n, min, max)
2014-05-07 16:03:57 +00:00
return math.max(min or -math.huge, math.min(max or math.huge, n))
2014-05-07 14:54:13 +00:00
end
local function cpypol(a, b)
if b < 0 then
return -a
else
return a
end
end
2014-05-07 14:58:37 +00:00
-- The main Sound class
2014-05-06 19:49:27 +00:00
2014-05-07 14:58:37 +00:00
sfxr.Sound = {}
sfxr.Sound.__index = sfxr.Sound
function sfxr.Sound:__init()
2014-05-06 19:49:27 +00:00
-- Build tables to store the parameters in
self.volume = {}
self.envelope = {}
self.frequency = {}
self.vibrato = {}
self.change = {}
self.duty = {}
self.phaser = {}
self.lowpass = {}
self.highpass = {}
-- Phaser and noise buffers
self.phaserBuffer = {}
self.noiseBuffer = {}
self:resetParameters()
self:resetBuffers()
end
2014-05-07 16:03:57 +00:00
function sfxr.Sound:resetParameters()
2014-05-06 19:49:27 +00:00
-- Set all parameters to the default values
self.repeatSpeed = 0.0
self.waveType = sfxr.SQUARE
self.superSamples = 8
self.volume.master = 0.5
self.volume.sound = 0.5
self.envelope.attack = 0.0
self.envelope.sustain = 0.3
self.envelope.punch = 0.0
self.envelope.decay = 0.4
self.frequency.start = 0.3
self.frequency.min = 0.0
self.frequency.slide = 0.0
self.frequency.deltaSlide = 0.0
self.vibrato.depth = 0.0
self.vibrato.speed = 0.0
self.vibrato.delay = 0.0
self.change.amount = 0.0
self.change.speed = 0.0
self.duty.ratio = 0.5
self.duty.sweep = 0.0
self.phaser.offset = 0.0
self.phaser.sweep = 0.0
self.lowpass.cutoff = 1.0
self.lowpass.sweep = 0.0
self.lowpass.resonance = 0.0
self.highpass.cutoff = 0.0
self.highpass.sweep = 0.0
end
2014-05-07 16:03:57 +00:00
function sfxr.Sound:resetBuffers()
2014-05-07 14:54:13 +00:00
-- Reset the sample buffers
2014-05-06 19:49:27 +00:00
for i=1, 1025 do
self.phaserBuffer[i] = 0
end
for i=1, 33 do
2014-05-07 16:03:57 +00:00
self.noiseBuffer[i] = random(-1, 1)
2014-05-07 14:54:13 +00:00
end
end
2014-05-07 16:03:57 +00:00
function sfxr.Sound:generate()
2014-05-07 14:54:13 +00:00
-- Basically the main synthesizing function, yields the sample data
-- Initialize ALL the locals!
2014-05-07 16:48:23 +00:00
local fperiod, maxperiod
local slide, dslide
local square_duty, square_slide
local chg_mod, chg_time, chg_limit
local function reset()
fperiod = 100 / ((self.frequency.start - 0.025)^2 + 0.001)
maxperiod = 100 / (self.frequency.min^2 + 0.001)
period = trunc(fperiod)
slide = 1.0 - self.frequency.slide^3 * 0.01
dslide = -self.frequency.deltaSlide^3 * 0.000001
square_duty = 0.5 - self.duty.ratio * 0.5
square_slide = -self.duty.sweep * 0.00005
chg_mod = 0
if self.change.amount >= 0 then
chg_mod = 1.0 - self.change.amount^2 * 0.9
else
chg_mod = 1.0 - self.change.amount^2 * 10
end
2014-05-07 14:54:13 +00:00
2014-05-07 16:48:23 +00:00
chg_time = 0
chg_limit = 0
if self.change.speed == 1 then
chg_limit = 0
else
chg_limit = (1 - self.change.speed)^2 * 20000 + 32
end
end
reset()
2014-05-07 14:54:13 +00:00
2014-05-07 16:48:23 +00:00
local phase = 0
2014-05-07 14:54:13 +00:00
local env_vol = 0
2014-05-07 16:03:57 +00:00
local env_stage = 1
2014-05-07 14:54:13 +00:00
local env_time = 0
local env_length = {self.envelope.attack^2 * 100000,
self.envelope.sustain^2 * 100000,
self.envelope.decay^2 * 100000}
local phase = self.phaser.offset^2 * 1020
phase = cpypol(phase, self.phaser.offset)
local dphase = self.phaser.sweep^2
dphase = cpypol(dphase, self.phaser.sweep)
2014-05-07 16:03:57 +00:00
local ipp = 0
2014-05-07 14:54:13 +00:00
2014-05-07 16:03:57 +00:00
local iphase = math.abs(trunc(phase))
2014-05-07 14:54:13 +00:00
local ltp = 0
local ltdp = 0
local ltw = self.lowpass.cutoff^3 * 0.1
2014-05-07 16:03:57 +00:00
local ltw_d = 1 + self.lowpass.sweep * 0.0001
local ltdmp = 5 / (1 + self.lowpass.resonance^2 * 20) * (0.01 + ltw)
2014-05-07 14:54:13 +00:00
ltdmp = clamp(ltdmp, nil, 0.8)
local ltphp = 0
local lthp = self.highpass.cutoff^2 * 0.1
local lthp_d = 1 + self.highpass.sweep * 0.0003
local vib_phase = 0
local vib_speed = self.vibrato.speed^2 * 0.01
local vib_amp = self.vibrato.depth * 0.5
2014-05-07 16:48:23 +00:00
local rep_time = 0
local rep_limit = trunc((1 - self.repeatSpeed)^2 * 20000 + 32)
if self.repeatSpeed == 0 then
rep_limit = 0
2014-05-07 16:03:57 +00:00
end
-- Yay, the main closure
return function()
2014-05-07 16:48:23 +00:00
-- Repeat maybe
rep_time = rep_time + 1
if rep_limit ~= 0 and rep_time >= rep_limit then
reset()
rep_time = 0
end
2014-05-07 16:03:57 +00:00
-- Update the change time and apply it if needed
chg_time = chg_time + 1
if chg_limit ~= 0 and chg_time >= chg_limit then
chg_limit = 0
fperiod = fperiod * chg_mod
end
-- Apply the frequency slide and stuff
slide = slide + dslide
fperiod = fperiod * slide
if fperiod > maxperiod then
2014-05-07 16:48:23 +00:00
fperiod = maxperiod
2014-05-07 16:03:57 +00:00
-- If the frequency is too low, stop generating
if (self.frequency.min > 0) then
return nil
end
end
-- Vibrato
local rfperiod = fperiod
if vib_amp > 0 then
vib_phase = vib_phase + vib_speed
rfperiod = fperiod * (1.0 + math.sin(vib_phase) * vib_amp)
end
-- Update the period
period = trunc(rfperiod)
if (period < 8) then period = 8 end
-- Update the square duty
square_duty = clamp(square_duty + square_slide, 0, 0.5)
-- Volume envelopes
env_time = env_time + 1
if env_time > env_length[env_stage] then
env_time = 0
env_stage = env_stage + 1
-- After the decay stop generating
if env_stage == 4 then
return nil
end
end
if env_stage == 1 then
env_vol = env_time / env_length[1]
elseif env_stage == 2 then
env_vol = 1 + (1 - env_time / env_length[2])^1 * 2 * self.envelope.punch
elseif env_stage == 3 then
env_vol = 1 - env_time / env_length[3]
end
-- Phaser
phase = phase + dphase
iphase = math.abs(trunc(phase))
if iphase > 1024 then iphase = 1024 end
-- Lowpass stuff
if lthp_d ~= 0 then
lthp = clamp(lthp * lthp_d, 0.00001, 0.1)
end
-- And finally the actual tone generation and supersampling
local ssample = 0
for si = 0, self.superSamples do
local sample = 0
phase = phase + 1
-- fill the noise buffer every period
if phase >= period then
--phase = 0
phase = phase % period
if self.waveType == sfxr.NOISE then
for i = 1, 32 do
self.noiseBuffer[i] = random(-1, 1)
end
end
end
-- Tone oscillators ahead!!!
local fp = phase / period
if self.waveType == sfxr.SQUARE then
if fp < square_duty then
sample = 0.5
else
sample = -0.5
end
elseif self.waveType == sfxr.SAWTOOTH then
sample = 1 - fp * 2
elseif self.waveType == sfxr.SINE then
sample = math.sin(fp * 2 * math.pi)
elseif self.waveType == sfxr.NOISE then
sample = self.noiseBuffer[math.floor(phase * 32 / period) + 1]
end
-- Apply the lowpass filter to the sample
local pp = ltp
ltw = clamp(ltw * ltw_d, 0, 0.1)
if self.lowpass.cutoff ~= 1 then
ltdp = ltdp + (sample - ltp) * ltw
ltdp = ltdp - ltdp * ltdmp
else
ltp = sample
ltdp = 0
end
ltp = ltp + ltdp
-- Apply the highpass filter to the sample
ltphp = ltphp + ltp - pp
ltphp = ltphp - ltphp * lthp
sample = ltphp
-- Apply the phaser to the sample
self.phaserBuffer[bit.band(ipp, 1023) + 1] = sample
sample = sample + self.phaserBuffer[bit.band(ipp - iphase + 1024, 1023) + 1]
ipp = bit.band(ipp + 1, 1023)
-- Accumulation and envelope application
ssample = ssample + sample * env_vol
end
-- Apply the volumes
ssample = ssample / self.superSamples * self.volume.master
ssample = ssample * 2 * self.volume.sound
-- Hard limit
ssample = clamp(ssample, -1, 1)
-- Aaaand finally
return ssample
2014-05-06 19:49:27 +00:00
end
end
2014-05-07 16:05:04 +00:00
function sfxr.Sound:getEnvelopeLimit()
local env_length = {self.envelope.attack^2 * 100000,
self.envelope.sustain^2 * 100000,
self.envelope.decay^2 * 100000}
2014-05-07 16:48:23 +00:00
return trunc(env_length[1] + env_length[2] + env_length[3] + 2)
2014-05-07 16:05:04 +00:00
end
function sfxr.Sound:getLimit()
return self:getEnvelopeLimit()
end
function sfxr.Sound:generateTable()
local t = {}
t[self:getLimit()] = 0
local i = 1
for v in self:generate() do
if not v then
break
end
t[i] = v
i = i + 1
end
return t
end
function sfxr.Sound:generateSoundData(freq, bits)
local tab = self:generateTable()
local data = love.sound.newSoundData(#tab, freq, bits, 1)
for i = 0, #tab - 1 do
data:setSample(i, tab[i + 1])
end
return data
end
2014-05-07 16:48:23 +00:00
function sfxr.Sound:randomize(seed)
math.randomseed(seed)
self.repeatSpeed = random(1, 2)
if maybe() then
self.frequency.start = random(-1, 1)^3 + 0.5
else
self.frequency.start = random(-1, 1)^2
end
self.frequency.limit = 0
self.frequency.slide = random(-1, 1)^5
if self.frequency.start > 0.7 and self.frequency.slide > 0.2 then
self.frequency.slide = -self.frequency.slide
elseif self.frequency.start < 0.2 and self.frequency.slide <-0.05 then
self.frequency.slide = -self.frequency.slide
end
self.frequency.deltaSlide = random(-1, 1)^3
self.duty.ratio = random(-1, 1)
self.duty.sweep = random(-1, 1)^3
self.vibrato.depth = random(-1, 1)^3
self.vibrato.speed = random(-1, 1)
self.vibrato.delay = random(-1, 1)
self.envelope.attack = random(-1, 1)^3
self.envelope.sustain = random(-1, 1)^2
self.envelope.punch = random(-1, 1)^2
self.envelope.decay = random(-1, 1)
if self.envelope.attack + self.envelope.sustain + self.envelope.decay < 0.2 then
self.envelope.sustain = self.envelope.sustain + 0.2 + random(0, 0.3)
self.envelope.decay = self.envelope.decay + 0.2 + random(0, 0.3)
end
self.lowpass.resonance = random(-1, 1)
self.lowpass.cutoff = 1 - random(0, 1)^3
self.lowpass.sweep = random(-1, 1)^3
if self.lowpass.cutoff < 0.1 and self.lowpass.sweep < -0.05 then
self.lowpass.sweep = -self.lowpass.sweep
end
self.highpass.cutoff = random(0, 1)^3
self.highpass.sweep = random(-1, 1)^5
self.phaser.offset = random(-1, 1)^3
self.phaser.sweep = random(-1, 1)^3
self.change.speed = random(-1, 1)
self.change.amount = random(-1, 1)
end
2014-05-07 14:58:37 +00:00
-- Constructor
2014-05-06 19:49:27 +00:00
2014-05-07 14:58:37 +00:00
function sfxr.newSound(...)
local instance = setmetatable({}, sfxr.Sound)
instance:__init(...)
return instance
end
2014-05-06 19:49:27 +00:00
2014-05-07 14:58:37 +00:00
return sfxr