Created
June 25, 2020 01:54
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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