-- 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.
]]--

--[[--
A port of the sfxr sound effect synthesizer to pure Lua, designed to be used
together with the *awesome* [LÖVE](https://love2d.org) game framework.
]]--
-- @module sfxr
local sfxr = {}
local bit = bit32 or require("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)
        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
        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.
-- @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

    local data = sounddata or love.sound.newSoundData(count, freq, bits, 1)

    for i = 0, #tab - 1 do
        data:setSample(i, tab[i + 1])
    end

    return 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
    local amount = (amount or 1)
    local a = amount / 20
    local b = (1 - a) * 10
    local 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
        f = io.open(f, "wb")
        close = true
    end

    -- Some utility functions
    function seek(pos)
        if io.type(f) == "file" then
            f:seek("set", pos)
        else
            f:seek(pos)
        end
    end

    function tell()
        if io.type(f) == "file" then
            return f:seek()
        else
            return f:tell()
        end
    end

    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

    function w16(num)
        f:write(bytes(num, 2))
    end

    function w32(num)
        f:write(bytes(num, 4))
    end

    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(freq) -- sampling rate
    w32(freq * bits / 8) -- bytes per second
    w16(bits / 8) -- block alignment
    w16(bits) -- 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 * bits / 8) -- remaining file size
    seek(pos_csize)
    w32(samples * bits / 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
        f = io.open(f, "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
    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)
    local close = false
    if type(f) == "string" then
        f = io.open(f, "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
        f = io.open(f, "w")
        close = true
    end

    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