Skip to content

Instantly share code, notes, and snippets.

@superqix
Created June 25, 2020 01:54
Show Gist options
  • Save superqix/0a23e31fa1fb30cd8455b229c08b5b68 to your computer and use it in GitHub Desktop.
Save superqix/0a23e31fa1fb30cd8455b229c08b5b68 to your computer and use it in GitHub Desktop.
A port of LÖVE's SFXR library to Solar2D for generated sound effects
-- sfxr.lua
-- original by Tomas Pettersson, ported to Lua by nucular
-- ported to Solar2D by ponywolf
--[[
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.
]]--
-- Getting this sfxr port workinf with Solar2D
-- @module sfxr
local sfxr = {}
local bit = require( "plugin.bit" )
-- Constants
--- The module version (SemVer format)
-- @within Constants
sfxr.VERSION = "0.0.2"
--- [Waveform](https://en.wikipedia.org/wiki/Waveform) constants
-- @within Constants
-- @field SQUARE [square wave](https://en.wikipedia.org/wiki/Square_wave) (`= 0`)
-- @field SAWTOOTH [sawtooth wave](https://en.wikipedia.org/wiki/Sawtooth_wave) (`= 1`)
-- @field SINE [sine wave](https://en.wikipedia.org/wiki/Sine_wave) (`= 2`)
-- @field NOISE [white noise](https://en.wikipedia.org/wiki/White_noise) (`= 3`)
sfxr.WAVEFORM = {
SQUARE = 0,
[0] = 0,
SAWTOOTH = 1,
[1] = 1,
SINE = 2,
[2] = 2,
NOISE = 3,
[3] = 3
}
--- [Sampling rate](https://en.wikipedia.org/wiki/Sampling_(signal_processing)#Sampling_rate) constants
-- (use the number values directly, these are just for lookup)
-- @within Constants
-- @field 22050 22.05 kHz (`= 22050`)
-- @field 44100 44.1 kHz (`= 44100`)
sfxr.SAMPLERATE = {
[22050] = 22050, --- 22.05 kHz
[44100] = 44100 --- 44.1 kHz
}
--- [Bit depth](https://en.wikipedia.org/wiki/Audio_bit_depth) constants
-- (use the number values directly, these are just for lookup)
-- @within Constants
-- @field 0 floating point bit depth, -1 to 1 (`= 0`)
-- @field 8 unsigned 8 bit, 0x00 to 0xFF (`= 8`)
-- @field 16 unsigned 16 bit, 0x0000 to 0xFFFF (`= 16`)
sfxr.BITDEPTH = {
[0] = 0,
[16] = 16,
[8] = 8
}
--- [Endianness](https://en.wikipedia.org/wiki/Endianness) constants
-- @within Constants
-- @field LITTLE little endian (`= 0`)
-- @field BIG big endian (`= 1`)
sfxr.ENDIANNESS = {
LITTLE = 0,
[0] = 0,
BIG = 1,
[1] = 1
}
-- Utilities
--- Truncate a number to an unsigned integer.
-- @tparam number n a (signed) number
-- @treturn int the number, truncated and unsigned
local function trunc(n)
if n >= 0 then
return math.floor(n)
else
return -math.floor(-n)
end
end
--- Set the random seed and initializes the generator.
-- @tparam number seed the random seed
local function setseed(seed)
math.randomseed(seed)
for i=0, 5 do
math.random()
end
end
--- Return a random number between low and high.
-- @tparam number low the lower bound
-- @tparam number high the upper bound
-- @treturn number a random number where `low < n < high`
local function random(low, high)
return low + math.random() * (high - low)
end
--- Return a random boolean weighted towards false by n.
-- w = 1: uniform distribution
-- w = n: false is n times as likely as true
-- Note: n < 0 do not work, use `not maybe(w)` instead
-- @tparam[opt=1] number w the weight towards false
-- @treturn bool a random boolean
local function maybe(w)
return trunc(random(0, w or 1)) == 0
end
--- Clamp n between min and max.
-- @tparam number n the number
-- @tparam number min the lower bound
-- @tparam number max the upper bound
-- @treturn number the number where `min <= n <= max`
local function clamp(n, min, max)
return math.max(min or -math.huge, math.min(max or math.huge, n))
end
--- Copy a table (shallow) or a primitive.
-- @param t a table or primitive
-- @return a copy of t
local function shallowcopy(t)
if type(t) == "table" then
local t2 = {}
for k,v in pairs(t) do
t2[k] = v
end
return t2
else
return t
end
end
--- Recursively merge table t2 into t1.
-- @tparam tab t1 a table
-- @tparam tab t2 a table to merge into t1
-- @treturn tab t1
local function mergetables(t1, t2)
for k, v in pairs(t2) do
if type(v) == "table" then
if type(t1[k] or false) == "table" then
mergetables(t1[k] or {}, t2[k] or {})
else
t1[k] = v
end
else
t1[k] = v
end
end
return t1
end
--- Pack a number into a IEEE754 32-bit big-endian floating point binary string.
-- [source](https://stackoverflow.com/questions/14416734/)
-- @tparam number number a number
-- @treturn string a binary string
local function packIEEE754(number)
if number == 0 then
return string.char(0x00, 0x00, 0x00, 0x00)
elseif number ~= number then
return string.char(0xFF, 0xFF, 0xFF, 0xFF)
else
local sign = 0x00
if number < 0 then
sign = 0x80
number = -number
end
local mantissa, exponent = math.frexp(number)
exponent = exponent + 0x7F
if exponent <= 0 then
mantissa = math.ldexp(mantissa, exponent - 1)
exponent = 0
elseif exponent > 0 then
if exponent >= 0xFF then
return string.char(sign + 0x7F, 0x80, 0x00, 0x00)
elseif exponent == 1 then
exponent = 0
else
mantissa = mantissa * 2 - 1
exponent = exponent - 1
end
end
mantissa = math.floor(math.ldexp(mantissa, 23) + 0.5)
return string.char(
sign + math.floor(exponent / 2),
(exponent % 2) * 0x80 + math.floor(mantissa / 0x10000),
math.floor(mantissa / 0x100) % 0x100,
mantissa % 0x100)
end
end
--- Unpack a IEEE754 32-bit big-endian floating point string to a number.
-- [source](https://stackoverflow.com/questions/14416734/)
-- @tparam string packed a binary string
-- @treturn number a number
local function unpackIEEE754(packed)
local b1, b2, b3, b4 = string.byte(packed, 1, 4)
local exponent = (b1 % 0x80) * 0x02 + math.floor(b2 / 0x80)
local mantissa = math.ldexp(((b2 % 0x80) * 0x100 + b3) * 0x100 + b4, -23)
if exponent == 0xFF then
if mantissa > 0 then
return 0 / 0
else
mantissa = math.huge
exponent = 0x7F
end
elseif exponent > 0 then
mantissa = mantissa + 1
else
exponent = exponent + 1
end
if b1 >= 0x80 then
mantissa = -mantissa
end
return math.ldexp(mantissa, exponent - 0x7F)
end
--- Construct and return a new @{Sound} instance.
-- @treturn Sound a Sound instance
function sfxr.newSound(...)
local instance = setmetatable({}, sfxr.Sound)
instance:__init(...)
return instance
end
--- The main Sound class.
-- @type Sound
sfxr.Sound = {}
sfxr.Sound.__index = sfxr.Sound
--- Initialize the Sound instance.
-- Called by @{sfxr.newSound|the constructor}.
function sfxr.Sound:__init()
--- Number of supersampling passes to perform (*default* 8)
-- @within Parameters
self.supersampling = 8
--- Repeat speed:
-- Times to repeat the frequency slide over the course of the envelope
-- (*default* 0.0, *min* 0.0, *max* 1.0)
-- @within Parameters
self.repeatspeed = 0.0
--- The base @{WAVEFORM|waveform} (*default* @{WAVEFORM|SQUARE})
-- @within Parameters
self.waveform = sfxr.WAVEFORM.SQUARE
-- Build tables to store the parameters in
--- **The sound volume and gain all samples are multiplied with**
-- @within Volume
self.volume = {}
--- **The [ASD envelope](https://en.wikipedia.org/wiki/Synthesizer#Attack_
--Decay_Sustain_Release_.28ADSR.29_envelope) that controls the sound
-- amplitude (volume) over time**
-- @within Envelope
self.envelope = {}
--- **The base and minimum frequencies of the tone generator and their
-- slides**
-- @within Frequency
self.frequency = {}
--- **A [vibrato](https://en.wikipedia.org/wiki/Vibrato)-like amplitude
-- modulation effect**
-- SerializationVibrato
self.vibrato = {}
--- **Changes the frequency mid-sound to create a characteristic
-- "coin"-effect**
-- @within Change
self.change = {}
--- **The [duty](https://en.wikipedia.org/wiki/Duty_cycle) of the square
-- waveform**
-- @within Duty
self.duty = {}
--- **A simple [phaser](https://en.wikipedia.org/wiki/Phaser_(effect))
-- effect**
-- @within Phaser
self.phaser = {}
--- **A [lowpass filter](https://en.wikipedia.org/wiki/Low-pass_filter)
-- effect**
-- @within Lowpass
self.lowpass = {}
--- **A [highpass filter](https://en.wikipedia.org/wiki/High-pass_filter)
-- effect**
-- @within Highpass
self.highpass = {}
-- These are not affected by resetParameters()
--- Master volume (*default* 0.5)
-- @within Volume
self.volume.master = 0.5
--- Additional gain (*default* 0.5)
-- @within Volume
self.volume.sound = 0.5
self:resetParameters()
end
--- Set all parameters to their default values. Does not affect
-- @{self.supersampling|supersampling} and @{self.volume|volume}.
-- Called by @{sfxr.Sound:__init|the initializer}.
function sfxr.Sound:resetParameters()
self.repeatspeed = 0.0
self.waveform = sfxr.WAVEFORM.SQUARE
--- Attack time:
-- Time the sound takes to reach its peak amplitude
-- (*default* 0.0, *min* 0.0, *max* 1.0)
-- @within Envelope
self.envelope.attack = 0.0
--- Sustain time:
-- Time the sound stays on its peak amplitude
-- (*default* 0.0, *min* 0.0, *max* 1.0)
-- @within Envelope
self.envelope.sustain = 0.3
--- Sustain punch:
-- Amount by which the sound peak amplitude is increased at the start of the
-- sustain time
-- (*default* 0.0, *min* 0.0, *max* 1.0)
-- @within Envelope
self.envelope.punch = 0.0
--- Decay time:
-- Time the sound takes to decay after its sustain time
-- (*default* 0.0, *min* 0.0, *max* 1.0)
-- @within Envelope
self.envelope.decay = 0.4
--- Start frequency:
-- Base tone of the sound, before sliding
-- (*default* 0.0, *min* 0.0, *max* 1.0)
-- @within Frequency
self.frequency.start = 0.3
--- Min frequency:
-- Tone below which the sound will get cut off
-- (*default* 0.0, *min* 0.0, *max* 1.0)
-- @within Frequency
self.frequency.min = 0.0
--- Slide:
-- Amount by which the frequency is increased or decreased over time
-- (*default* 0.0, *min* -1.0, *max* 1.0)
-- @within Frequency
self.frequency.slide = 0.0
--- Delta slide:
-- Amount by which the slide is increased or decreased over time
-- (*default* 0.0, *min* -1.0, *max* 1.0)
-- @within Frequency
self.frequency.dslide = 0.0
--- Vibrato depth:
-- Amount of amplitude modulation
-- (*default* 0.0, *min* 0.0, *max* 1.0)
-- @within Vibrato
self.vibrato.depth = 0.0
--- Vibrato speed:
-- Oscillation speed of the vibrato
-- (*default* 0.0, *min* 0.0, *max* 1.0)
-- @within Vibrato
self.vibrato.speed = 0.0
--- Vibrato delay:
-- Unused and unimplemented
-- (*default* 0.0, *min* 0.0, *max* 1.0)
-- @within Vibrato
self.vibrato.delay = 0.0
--- Change amount:
-- Amount by which the frequency is changed mid-sound
-- (*default* 0.0, *min* -1.0, *max* 1.0)
-- @within Change
self.change.amount = 0.0
--- Change speed:
-- Time before the frequency change happens
-- (*default* 0.0, *min* 0.0, *max* 1.0)
-- @within Change
self.change.speed = 0.0
--- Square duty:
-- Width of the square wave pulse cycle (doesn't affect other waveforms)
-- (*default* 0.0, *min* 0.0, *max* 1.0)
-- @within Duty
self.duty.ratio = 0.0
--- Duty sweep:
-- Amount by which the square duty is increased or decreased over time
-- (*default* 0.0, *min* -1.0, *max* 1.0)
-- @within Duty
self.duty.sweep = 0.0
--- Phaser offset:
-- Amount by which the phaser signal is offset from the sound
-- (*default* 0.0, *min* -1.0, *max* 1.0)
-- @within Phaser
self.phaser.offset = 0.0
--- Phaser sweep:
-- Amount by which the phaser offset is increased or decreased over time
-- (*default* 0.0, *min* -1.0, *max* 1.0)
-- @within Phaser
self.phaser.sweep = 0.0
--- Lowpass filter cutoff:
-- Lower bound for frequencies allowed to pass through this filter
-- (*default* 0.0, *min* 0.0, *max* 1.0)
-- @within Lowpass
self.lowpass.cutoff = 1.0
--- Lowpass filter cutoff sweep:
-- Amount by which the LP filter cutoff is increased or decreased
-- over time
-- (*default* 0.0, *min* -1.0, *max* 1.0)
-- @within Lowpass
self.lowpass.sweep = 0.0
--- Lowpass filter resonance:
-- Amount by which certain resonant frequencies near the cutoff are
-- increased
-- (*default* 0.0, *min* 0.0, *max* 1.0)
-- @within Lowpass
self.lowpass.resonance = 0.0
--- Highpass filter cutoff:
-- Upper bound for frequencies allowed to pass through this filter
-- (*default* 0.0, *min* 0.0, *max* 1.0)
-- @within Highpass
self.highpass.cutoff = 0.0
--- Highpass filter cutoff sweep:
-- Amount by which the HP filter cutoff is increased or decreased
-- over time
-- (*default* 0.0, *min* -1.0, *max* 1.0)
-- @within Highpass
self.highpass.sweep = 0.0
end
--- Clamp all parameters within their sane ranges.
function sfxr.Sound:sanitizeParameters()
self.repeatspeed = clamp(self.repeatspeed, 0, 1)
self.waveform = clamp(self.waveform, 0, #sfxr.WAVEFORM)
self.envelope.attack = clamp(self.envelope.attack, 0, 1)
self.envelope.sustain = clamp(self.envelope.sustain, 0, 1)
self.envelope.punch = clamp(self.envelope.punch, 0, 1)
self.envelope.decay = clamp(self.envelope.decay, 0, 1)
self.frequency.start = clamp(self.frequency.start, 0, 1)
self.frequency.min = clamp(self.frequency.min, 0, 1)
self.frequency.slide = clamp(self.frequency.slide, -1, 1)
self.frequency.dslide = clamp(self.frequency.dslide, -1, 1)
self.vibrato.depth = clamp(self.vibrato.depth, 0, 1)
self.vibrato.speed = clamp(self.vibrato.speed, 0, 1)
self.vibrato.delay = clamp(self.vibrato.delay, 0, 1)
self.change.amount = clamp(self.change.amount, -1, 1)
self.change.speed = clamp(self.change.speed, 0, 1)
self.duty.ratio = clamp(self.duty.ratio, 0, 1)
self.duty.sweep = clamp(self.duty.sweep, -1, 1)
self.phaser.offset = clamp(self.phaser.offset, -1, 1)
self.phaser.sweep = clamp(self.phaser.sweep, -1, 1)
self.lowpass.cutoff = clamp(self.lowpass.cutoff, 0, 1)
self.lowpass.sweep = clamp(self.lowpass.sweep, -1, 1)
self.lowpass.resonance = clamp(self.lowpass.resonance, 0, 1)
self.highpass.cutoff = clamp(self.highpass.cutoff, 0, 1)
self.highpass.sweep = clamp(self.highpass.sweep, -1, 1)
end
--- Generate the sound and yield the sample data.
-- @tparam[opt=44100] SAMPLERATE rate the sampling rate
-- @tparam[opt=0] BITDEPTH depth the bit depth
-- @treturn function() a generator that yields the next sample when called
-- @usage for s in sound:generate(44100, 0) do
-- -- do something with s
-- end
-- @raise "invalid sampling rate: x", "invalid bit depth: x"
function sfxr.Sound:generate(rate, depth)
rate = rate or 44100
depth = depth or 0
assert(sfxr.SAMPLERATE[rate], "invalid sampling rate: " .. tostring(rate))
assert(sfxr.BITDEPTH[depth], "invalid bit depth: " .. tostring(depth))
-- Initialize all locals
local fperiod, maxperiod
local slide, dslide
local square_duty, square_slide
local chg_mod, chg_time, chg_limit
local phaserbuffer = {}
local noisebuffer = {}
-- Initialize the sample buffers
for i=1, 1024 do
phaserbuffer[i] = 0
end
for i=1, 32 do
noisebuffer[i] = random(-1, 1)
end
--- Reset the sound period
local function reset()
fperiod = 100 / (self.frequency.start^2 + 0.001)
maxperiod = 100 / (self.frequency.min^2 + 0.001)
local period = trunc(fperiod)
slide = 1.0 - self.frequency.slide^3 * 0.01
dslide = -self.frequency.dslide^3 * 0.000001
square_duty = 0.5 - self.duty.ratio * 0.5
square_slide = -self.duty.sweep * 0.00005
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
chg_time = 0
if self.change.speed == 1 then
chg_limit = 0
else
chg_limit = trunc((1 - self.change.speed)^2 * 20000 + 32)
end
end
local phase = 0
reset()
local second_sample = false
local env_vol = 0
local env_stage = 1
local env_time = 0
local env_length = {self.envelope.attack^2 * 100000,
self.envelope.sustain^2 * 100000,
self.envelope.decay^2 * 100000}
local fphase = self.phaser.offset^2 * 1020
if self.phaser.offset < 0 then fphase = -fphase end
local dphase = self.phaser.sweep^2
if self.phaser.sweep < 0 then dphase = -dphase end
local ipp = 0
local iphase = math.abs(trunc(fphase))
local fltp = 0
local fltdp = 0
local fltw = self.lowpass.cutoff^3 * 0.1
local fltw_d = 1 + self.lowpass.sweep * 0.0001
local fltdmp = 5 / (1 + self.lowpass.resonance^2 * 20) * (0.01 + fltw)
fltdmp = clamp(fltdmp, nil, 0.8)
local fltphp = 0
local flthp = self.highpass.cutoff^2 * 0.1
local flthp_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
local rep_time = 0
local rep_limit = trunc((1 - self.repeatspeed)^2 * 20000 + 32)
if self.repeatspeed == 0 then
rep_limit = 0
end
-- The main closure (returned as a generator)
local function next()
-- Repeat when needed
rep_time = rep_time + 1
if rep_limit ~= 0 and rep_time >= rep_limit then
rep_time = 0
reset()
end
-- 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
fperiod = maxperiod
-- Fail if the minimum frequency is too small
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
-- Apply to the frequency period
rfperiod = fperiod * (1.0 + math.sin(vib_phase) * vib_amp)
end
-- Update the period
local 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
-- Attack, Sustain, Decay/Release
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
fphase = fphase + dphase
iphase = clamp(math.abs(trunc(fphase)), nil, 1023)
-- Filter stuff
if flthp_d ~= 0 then
flthp = clamp(flthp * flthp_d, 0.00001, 0.1)
end
-- And finally the actual tone generation and supersampling
local ssample = 0
for si = 0, self.supersampling-1 do
local sample = 0
phase = phase + 1
-- fill the noise buffer every period
if phase >= period then
--phase = 0
phase = phase % period
if self.waveform == sfxr.WAVEFORM.NOISE then
for i = 1, 32 do
noisebuffer[i] = random(-1, 1)
end
end
end
-- Tone generators ahead
local fp = phase / period
-- Square, including square duty
if self.waveform == sfxr.WAVEFORM.SQUARE then
if fp < square_duty then
sample = 0.5
else
sample = -0.5
end
-- Sawtooth
elseif self.waveform == sfxr.WAVEFORM.SAWTOOTH then
sample = 1 - fp * 2
-- Sine
elseif self.waveform == sfxr.WAVEFORM.SINE then
sample = math.sin(fp * 2 * math.pi)
-- Pitched white noise
elseif self.waveform == sfxr.WAVEFORM.NOISE then
sample = noisebuffer[trunc(phase * 32 / period) % 32 + 1]
end
-- Apply the lowpass filter to the sample
local pp = fltp
fltw = clamp(fltw * fltw_d, 0, 0.1)
if self.lowpass.cutoff ~= 1 then
fltdp = fltdp + (sample - fltp) * fltw
fltdp = fltdp - fltdp * fltdmp
else
fltp = sample
fltdp = 0
end
fltp = fltp + fltdp
-- Apply the highpass filter to the sample
fltphp = fltphp + (fltp - pp)
fltphp = fltphp - (fltphp * flthp)
sample = fltphp
-- Apply the phaser to the sample
phaserbuffer[bit.band(ipp, 1023) + 1] = sample
sample = sample + 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.supersampling) * self.volume.master
ssample = ssample * (2 * self.volume.sound)
-- Hard limit
ssample = clamp(ssample, -1, 1)
-- Frequency conversion
second_sample = not second_sample
if rate == 22050 and second_sample then
-- hah!
local nsample = next()
if nsample then
return (ssample + nsample) / 2
else
return nil
end
end
-- bit conversions
if depth == 0 then
return ssample
elseif depth == 16 then
return trunc(ssample * 32000)
else
return trunc(ssample * 127 + 128)
end
end
return next
end
--- Get the maximum sample limit allowed by the current envelope.
-- Does not take any other limits into account, so the returned count might be
-- higher than samples actually generated. Still useful though.
-- @tparam[opt=44100] SAMPLERATE rate the sampling rate
-- @raise "invalid sampling rate: x", "invalid bit depth: x"
function sfxr.Sound:getEnvelopeLimit(rate)
rate = rate or 44100
assert(sfxr.SAMPLERATE[rate], "invalid sampling rate: " .. tostring(rate))
local env_length = {
self.envelope.attack^2 * 100000, --- attack
self.envelope.sustain^2 * 100000, --- sustain
self.envelope.decay^2 * 100000 --- decay
}
local limit = trunc(env_length[1] + env_length[2] + env_length[3] + 2)
return math.ceil(limit / (rate / 44100))
end
--- Generate the sound into a table.
-- @tparam[opt=44100] SAMPLERATE rate the sampling rate
-- @tparam[opt=0] BITDEPTH depth the bit depth
-- @tparam[opt] {} tab the table to synthesize into
-- @treturn {number,...} the table filled with sample data
-- @treturn int the number of written samples (== #tab)
-- @raise "invalid sampling rate: x", "invalid bit depth: x"
function sfxr.Sound:generateTable(rate, depth, tab)
rate = rate or 44100
depth = depth or 0
assert(sfxr.SAMPLERATE[rate], "invalid sampling rate: " .. tostring(rate))
assert(sfxr.BITDEPTH[depth], "invalid bit depth: " .. tostring(depth))
-- this could really use table pre-allocation, but Lua doesn't provide that
local t = tab or {}
local i = 1
for v in self:generate(rate, depth) do
t[i] = v
i = i + 1
end
return t, i
end
--- Generate the sound to a binary string.
-- @tparam[opt=44100] SAMPLERATE rate the sampling rate
-- @tparam[opt=16] BITDEPTH depth the bit depth (may not be @{BITDEPTH|0})
-- @tparam[opt=0] ENDIANNESS endianness the endianness (ignored when depth == 8)
-- @treturn string a binary string of sample data
-- @treturn int the number of written samples
-- @raise "invalid sampling rate: x", "invalid bit depth: x", "invalid endianness: x"
function sfxr.Sound:generateString(rate, depth, endianness)
rate = rate or 44100
depth = depth or 16
endianness = endianness or 0
assert(sfxr.SAMPLERATE[rate], "invalid sampling rate: " .. tostring(rate))
assert(sfxr.BITDEPTH[depth] and depth ~= 0, "invalid bit depth: " .. tostring(depth))
assert(sfxr.ENDIANNESS[endianness], "invalid endianness: " .. tostring(endianness))
local s = ""
--- buffer for arguments to string.char
local buf = {}
buf[100] = 0
local bi = 1
local i = 0
for v in self:generate(rate, depth) do
if depth == 8 then
buf[i] = v
bi = bi + 1
else
if endianness == sfxr.ENDIANNESS.BIG then
buf[bi] = bit.rshift(v, 8)
buf[bi + 1] = bit.band(v, 0xFF)
bi = bi + 2
else
buf[bi] = bit.band(v, 0xFF)
buf[bi + 1] = bit.rshift(v, 8)
bi = bi + 2
end
end
if bi >= 100 then
s = s .. string.char(unpack(buf))
bi = 0
end
i = i + 1
end
-- pass in up to 100 characters
s = s .. string.char(unpack(buf, i, 100))
return s, i
end
--- Synthesize the sound to a LÖVE SoundData instance. (Won't work on Solar2D)
-- @tparam[opt=44100] SAMPLERATE rate the sampling rate
-- @tparam[opt=0] BITDEPTH depth the bit depth
-- @tparam[opt] love.sound.SoundData sounddata a SoundData instance (will be
-- created if not passed)
-- @treturn love.sound.SoundData a SoundData instance
-- @treturn int the number of written samples
-- @raise "invalid sampling rate: x", "invalid bit depth: x"
function sfxr.Sound:generateSoundData(rate, depth, sounddata)
rate = rate or 44100
depth = depth or 0
assert(sfxr.SAMPLERATE[rate], "invalid sampling rate: " .. tostring(rate))
assert(sfxr.BITDEPTH[depth] and depth, "invalid bit depth: " .. tostring(depth))
local tab, count = self:generateTable(rate, depth)
if count == 0 then
return nil
end
-- TODO: Here's where we wpould load the RAW WAV data into a Solar2D audio
-- local data = audio.loadSound(tab, count, rate, depth)
-- for i = 0, #tab - 1 do
-- data:setSample(i, tab[i + 1])
-- end
return false -- data, count
end
--- Randomize all sound parameters
-- @within Randomization
-- @tparam[opt] number seed a random seed
function sfxr.Sound:randomize(seed)
if seed then setseed(seed) end
local waveform = self.waveform
self:resetParameters()
self.waveform = waveform
if maybe() then
self.repeatspeed = random(0, 1)
end
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.dslide = 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)
self:sanitizeParameters()
end
--- Mutate all sound parameters
-- @within Randomization
-- @tparam[opt=1] number amount by how much to mutate the parameters
-- @tparam[opt] number seed a random seed
-- @tparam[changefreq=true] bool changefreq whether to change the frequency parameters
function sfxr.Sound:mutate(amount, seed, changefreq)
if seed then setseed(seed) end
amount = (amount or 1)
local a = amount / 20
local b = (1 - a) * 10
changefreq = (changefreq == nil) and true or changefreq
if changefreq == true then
if maybe(b) then self.frequency.start = self.frequency.start + random(-a, a) end
if maybe(b) then self.frequency.slide = self.frequency.slide + random(-a, a) end
if maybe(b) then self.frequency.dslide = self.frequency.dslide + random(-a, a) end
end
if maybe(b) then self.duty.ratio = self.duty.ratio + random(-a, a) end
if maybe(b) then self.duty.sweep = self.duty.sweep + random(-a, a) end
if maybe(b) then self.vibrato.depth = self.vibrato.depth + random(-a, a) end
if maybe(b) then self.vibrato.speed = self.vibrato.speed + random(-a, a) end
if maybe(b) then self.vibrato.delay = self.vibrato.delay + random(-a, a) end
if maybe(b) then self.envelope.attack = self.envelope.attack + random(-a, a) end
if maybe(b) then self.envelope.sustain = self.envelope.sustain + random(-a, a) end
if maybe(b) then self.envelope.punch = self.envelope.punch + random(-a, a) end
if maybe(b) then self.envelope.decay = self.envelope.decay + random(-a, a) end
if maybe(b) then self.lowpass.resonance = self.lowpass.resonance + random(-a, a) end
if maybe(b) then self.lowpass.cutoff = self.lowpass.cutoff + random(-a, a) end
if maybe(b) then self.lowpass.sweep = self.lowpass.sweep + random(-a, a) end
if maybe(b) then self.highpass.cutoff = self.highpass.cutoff + random(-a, a) end
if maybe(b) then self.highpass.sweep = self.highpass.sweep + random(-a, a) end
if maybe(b) then self.phaser.offset = self.phaser.offset + random(-a, a) end
if maybe(b) then self.phaser.sweep = self.phaser.sweep + random(-a, a) end
if maybe(b) then self.change.speed = self.change.speed + random(-a, a) end
if maybe(b) then self.change.amount = self.change.amount + random(-a, a) end
if maybe(b) then self.repeatspeed = self.repeatspeed + random(-a, a) end
self:sanitizeParameters()
end
--- Randomize all sound parameters to generate a "pick up" sound
-- @within Randomization
-- @tparam[opt] number seed a random seed
function sfxr.Sound:randomPickup(seed)
if seed then setseed(seed) end
self:resetParameters()
self.frequency.start = random(0.4, 0.9)
self.envelope.attack = 0
self.envelope.sustain = random(0, 0.1)
self.envelope.punch = random(0.3, 0.6)
self.envelope.decay = random(0.1, 0.5)
if maybe() then
self.change.speed = random(0.5, 0.7)
self.change.amount = random(0.2, 0.6)
end
end
--- Randomize all sound parameters to generate a laser sound
-- @within Randomization
-- @tparam[opt] number seed a random seed
function sfxr.Sound:randomLaser(seed)
if seed then setseed(seed) end
self:resetParameters()
self.waveform = trunc(random(0, 3))
if self.waveform == sfxr.WAVEFORM.SINE and maybe() then
self.waveform = trunc(random(0, 1))
end
if maybe(2) then
self.frequency.start = random(0.3, 0.9)
self.frequency.min = random(0, 0.1)
self.frequency.slide = random(-0.65, -0.35)
else
self.frequency.start = random(0.5, 1)
self.frequency.min = clamp(self.frequency.start - random(0.2, 0.4), 0.2)
self.frequency.slide = random(-0.35, -0.15)
end
if maybe() then
self.duty.ratio = random(0, 0.5)
self.duty.sweep = random(0, 0.2)
else
self.duty.ratio = random(0.4, 0.9)
self.duty.sweep = random(-0.7, 0)
end
self.envelope.attack = 0
self.envelope.sustain = random(0.1, 0.3)
self.envelope.decay = random(0, 0.4)
if maybe() then
self.envelope.punch = random(0, 0.3)
end
if maybe(2) then
self.phaser.offset = random(0, 0.2)
self.phaser.sweep = random(-0.2, 0)
end
if maybe() then
self.highpass.cutoff = random(0, 0.3)
end
end
--- Randomize all sound parameters to generate an explosion sound
-- @within Randomization
-- @tparam[opt] number seed a random seed
function sfxr.Sound:randomExplosion(seed)
if seed then setseed(seed) end
self:resetParameters()
self.waveform = sfxr.WAVEFORM.NOISE
if maybe() then
self.frequency.start = random(0.1, 0.5)
self.frequency.slide = random(-0.1, 0.3)
else
self.frequency.start = random(0.2, 0.9)
self.frequency.slide = random(-0.2, -0.4)
end
self.frequency.start = self.frequency.start^2
if maybe(4) then
self.frequency.slide = 0
end
if maybe(2) then
self.repeatspeed = random(0.3, 0.8)
end
self.envelope.attack = 0
self.envelope.sustain = random(0.1, 0.4)
self.envelope.punch = random(0.2, 0.8)
self.envelope.decay = random(0, 0.5)
if maybe() then
self.phaser.offset = random(-0.3, 0.6)
self.phaser.sweep = random(-0.3, 0)
end
if maybe() then
self.vibrato.depth = random(0, 0.7)
self.vibrato.speed = random(0, 0.6)
end
if maybe(2) then
self.change.speed = random(0.6, 0.9)
self.change.amount = random(-0.8, 0.8)
end
end
--- Randomize all sound parameters to generate a "power up" sound
-- @within Randomization
-- @tparam[opt] number seed a random seed
function sfxr.Sound:randomPowerup(seed)
if seed then setseed(seed) end
self:resetParameters()
if maybe() then
self.waveform = sfxr.WAVEFORM.SAWTOOTH
else
self.duty.ratio = random(0, 0.6)
end
if maybe() then
self.frequency.start = random(0.2, 0.5)
self.frequency.slide = random(0.1, 0.5)
self.repeatspeed = random(0.4, 0.8)
else
self.frequency.start = random(0.2, 0.5)
self.frequency.slide = random(0.05, 0.25)
if maybe() then
self.vibrato.depth = random(0, 0.7)
self.vibrato.speed = random(0, 0.6)
end
end
self.envelope.attack = 0
self.envelope.sustain = random(0, 0.4)
self.envelope.decay = random(0.1, 0.5)
end
--- Randomize all sound parameters to generate a hit sound
-- @within Randomization
-- @tparam[opt] number seed a random seed
function sfxr.Sound:randomHit(seed)
if seed then setseed(seed) end
self:resetParameters()
self.waveform = trunc(random(0, 3))
if self.waveform == sfxr.WAVEFORM.SINE then
self.waveform = sfxr.WAVEFORM.NOISE
elseif self.waveform == sfxr.WAVEFORM.SQUARE then
self.duty.ratio = random(0, 0.6)
end
self.frequency.start = random(0.2, 0.8)
self.frequency.slide = random(-0.7, -0.3)
self.envelope.attack = 0
self.envelope.sustain = random(0, 0.1)
self.envelope.decay = random(0.1, 0.3)
if maybe() then
self.highpass.cutoff = random(0, 0.3)
end
end
--- Randomize all sound parameters to generate a jump sound
-- @within Randomization
-- @tparam[opt] number seed a random seed
function sfxr.Sound:randomJump(seed)
if seed then setseed(seed) end
self:resetParameters()
self.waveform = sfxr.WAVEFORM.SQUARE
self.duty.value = random(0, 0.6)
self.frequency.start = random(0.3, 0.6)
self.frequency.slide = random(0.1, 0.3)
self.envelope.attack = 0
self.envelope.sustain = random(0.1, 0.4)
self.envelope.decay = random(0.1, 0.3)
if maybe() then
self.highpass.cutoff = random(0, 0.3)
end
if maybe() then
self.lowpass.cutoff = random(0.4, 1)
end
end
--- Randomize all sound parameters to generate a "blip" sound
-- @within Randomization
-- @tparam[opt] number seed a random seed
function sfxr.Sound:randomBlip(seed)
if seed then setseed(seed) end
self:resetParameters()
self.waveform = trunc(random(0, 2))
if self.waveform == sfxr.WAVEFORM.SQUARE then
self.duty.ratio = random(0, 0.6)
end
self.frequency.start = random(0.2, 0.6)
self.envelope.attack = 0
self.envelope.sustain = random(0.1, 0.2)
self.envelope.decay = random(0, 0.2)
self.highpass.cutoff = 0.1
end
--- Generate and export the audio data to a PCM WAVE file.
-- @within Serialization
-- @tparam ?string|file|love.filesystem.File f a path or file in `wb`-mode
-- (passed files will not be closed)
-- @tparam[opt=44100] SAMPLERATE rate the sampling rate
-- @tparam[opt=0] BITDEPTH depth the bit depth
-- @raise "invalid sampling rate: x", "invalid bit depth: x"
function sfxr.Sound:exportWAV(f, rate, depth)
rate = rate or 44100
depth = depth or 16
assert(sfxr.SAMPLERATE[rate], "invalid sampling rate: " .. tostring(rate))
assert(sfxr.BITDEPTH[depth] and depth ~= 0, "invalid bit depth: " .. tostring(depth))
local close = false
if type(f) == "string" then
local path = system.pathForFile( f, system.DocumentsDirectory )
f = io.open(path, "wb")
close = true
end
-- Some utility functions
local function seek(pos)
if io.type(f) == "file" then
f:seek("set", pos)
else
f:seek(pos)
end
end
local function tell()
if io.type(f) == "file" then
return f:seek()
else
return f:tell()
end
end
local function bytes(num, len)
local str = ""
for i = 1, len do
str = str .. string.char(num % 256)
num = math.floor(num / 256)
end
return str
end
local function w16(num)
f:write(bytes(num, 2))
end
local function w32(num)
f:write(bytes(num, 4))
end
local function ws(str)
f:write(str)
end
-- These will hold important file positions
local pos_fsize
local pos_csize
-- Start the file by writing the RIFF header
ws("RIFF")
pos_fsize = tell()
w32(0) -- remaining file size, will be replaced later
ws("WAVE") -- type
-- Write the format chunk
ws("fmt ")
w32(16) -- chunk size
w16(1) -- compression code (1 = PCM)
w16(1) -- channel number
w32(rate) -- sampling rate
w32(rate * depth / 8) -- bytes per second
w16(depth / 8) -- block alignment
w16(depth) -- bits per sample
-- Write the header of the data chunk
ws("data")
pos_csize = tell()
w32(0) -- chunk size, will be replaced later
-- Aand write the actual sample data
local samples = 0
for v in self:generate(rate, depth) do
samples = samples + 1
if depth == 16 then
-- wrap around a bit
if v >= 256^2 then v = 0 end
if v < 0 then v = 256^2 + v end
w16(v)
else
f:write(string.char(v))
end
end
-- Seek back to the stored positions
seek(pos_fsize)
w32(pos_csize - 4 + samples * depth / 8) -- remaining file size
seek(pos_csize)
w32(samples * depth / 8) -- chunk size
if close then
f:close()
end
end
--- Save the sound parameters to a file as a Lua table
-- @within Serialization
-- @tparam ?string|file|love.filesystem.File f a path or file in `w`-mode
-- (passed files will not be closed)
-- @tparam[opt=true] bool minify whether to minify the output or not
function sfxr.Sound:save(f, minify)
local close = false
if type(f) == "string" then
local path = system.pathForFile( f, system.DocumentsDirectory )
f = io.open(path, "w")
close = true
end
local code = "local "
-- we'll compare the current parameters with the defaults
local defaults = sfxr.newSound()
-- this part is pretty awful but it works for now
local function store(keys, obj)
local name = keys[#keys]
if type(obj) == "number" then
-- fetch the default value
local def = defaults
for i=2, #keys do
def = def[keys[i]]
end
if obj ~= def then
local k = table.concat(keys, ".")
if not minify then
code = code .. "\n" .. string.rep(" ", #keys - 1)
end
code = code .. string.format("%s=%s;", name, obj)
end
elseif type(obj) == "table" then
local spacing = minify and "" or "\n" .. string.rep(" ", #keys - 1)
code = code .. spacing .. string.format("%s={", name)
for k, v in pairs(obj) do
local newkeys = shallowcopy(keys)
newkeys[#newkeys + 1] = k
store(newkeys, v)
end
code = code .. spacing .. "};"
end
end
store({"s"}, self)
code = code .. "\nreturn s, \"" .. sfxr.VERSION .. "\""
f:write(code)
if close then
f:close()
end
end
--- Load the sound parameters from a file containing a Lua table
-- @within Serialization
-- @tparam ?string|file|love.filesystem.File f a path or file in `r`-mode
-- (passed files will not be closed)
-- @raise "incompatible version: x.x.x"
function sfxr.Sound:load(f, dir)
local close = false
if type(f) == "string" then
local path = system.pathForFile( f, dir or system.ResourceDirectory )
f = io.open(path, "r")
close = true
end
local code
if io.type(f) == "file" then
code = f:read("*a")
else
code = f:read()
end
local params, version = assert(loadstring(code))()
-- check version compatibility
assert(version > sfxr.VERSION, "incompatible version: " .. tostring(version))
self:resetParameters()
-- merge the loaded table into the own
mergetables(self, params)
if close then
f:close()
end
end
--- Save the sound parameters to a file in the sfxr binary format (version 102)
-- @within Serialization
-- @tparam ?string|file|love.filesystem.File f a path or file in `wb`-mode
-- (passed files will not be closed)
function sfxr.Sound:saveBinary(f)
local close = false
if type(f) == "string" then
local path = system.pathForFile( f, system.DocumentsDirectory )
f = io.open(path, "w")
close = true
end
local function writeFloat(x)
local packed = packIEEE754(x):reverse()
assert(packed:len() == 4)
f:write(packed)
end
f:write('\x66\x00\x00\x00') -- version 102
assert(self.waveform < 256)
f:write(string.char(self.waveform) .. '\x00\x00\x00')
writeFloat(self.volume.sound)
writeFloat(self.frequency.start)
writeFloat(self.frequency.min)
writeFloat(self.frequency.slide)
writeFloat(self.frequency.dslide)
writeFloat(self.duty.ratio)
writeFloat(self.duty.sweep)
writeFloat(self.vibrato.depth)
writeFloat(self.vibrato.speed)
writeFloat(self.vibrato.delay)
writeFloat(self.envelope.attack)
writeFloat(self.envelope.sustain)
writeFloat(self.envelope.decay)
writeFloat(self.envelope.punch)
f:write('\x00') -- unused filter_on boolean
writeFloat(self.lowpass.resonance)
writeFloat(self.lowpass.cutoff)
writeFloat(self.lowpass.sweep)
writeFloat(self.highpass.cutoff)
writeFloat(self.highpass.sweep)
writeFloat(self.phaser.offset)
writeFloat(self.phaser.sweep)
writeFloat(self.repeatspeed)
writeFloat(self.change.speed)
writeFloat(self.change.amount)
if close then
f:close()
end
end
--- Load the sound parameters from a file in the sfxr binary format
-- (version 100-102)
-- @within Serialization
-- @tparam ?string|file|love.filesystem.File f a path or file in `rb`-mode
-- (passed files will not be closed)
-- @raise "incompatible version: x", "unexpected file length"
function sfxr.Sound:loadBinary(f)
local close = false
if type(f) == "string" then
f = io.open(f, "r")
close = true
end
local s
if io.type(f) == "file" then
s = f:read("*a")
else
s = f:read()
end
if close then
f:close()
end
self:resetParameters()
local off = 1
local function readFloat()
local f = unpackIEEE754(s:sub(off, off+3):reverse())
off = off + 4
return f
end
-- Start reading the string
local version = s:byte(off)
off = off + 4
if version < 100 or version > 102 then
error("incompatible version: " .. tostring(version))
end
self.waveform = s:byte(off)
off = off + 4
self.volume.sound = version==102 and readFloat() or 0.5
self.frequency.start = readFloat()
self.frequency.min = readFloat()
self.frequency.slide = readFloat()
self.frequency.dslide = version>=101 and readFloat() or 0
self.duty.ratio = readFloat()
self.duty.sweep = readFloat()
self.vibrato.depth = readFloat()
self.vibrato.speed = readFloat()
self.vibrato.delay = readFloat()
self.envelope.attack = readFloat()
self.envelope.sustain = readFloat()
self.envelope.decay = readFloat()
self.envelope.punch = readFloat()
off = off + 1 -- filter_on - seems to be ignored in the C++ version
self.lowpass.resonance = readFloat()
self.lowpass.cutoff = readFloat()
self.lowpass.sweep = readFloat()
self.highpass.cutoff = readFloat()
self.highpass.sweep = readFloat()
self.phaser.offset = readFloat()
self.phaser.sweep = readFloat()
self.repeatspeed = readFloat()
if version >= 101 then
self.change.speed = readFloat()
self.change.amount = readFloat()
end
assert(off-1 == s:len(), "unexpected file length")
end
return sfxr
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment