Documented everything using LDoc

This commit is contained in:
nucular 2016-03-02 00:37:30 +01:00
parent 0593330ebe
commit 0816498446
4 changed files with 387 additions and 119 deletions

44
.gitignore vendored Normal file
View File

@ -0,0 +1,44 @@
# Compiled Lua sources
luac.out
# luarocks build files
*.src.rock
*.zip
*.tar.gz
# Object files
*.o
*.os
*.ko
*.obj
*.elf
# Precompiled Headers
*.gch
*.pch
# Libraries
*.lib
*.a
*.la
*.lo
*.def
*.exp
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
# LDoc
doc/*
!doc/.gitkeep

11
config.ld Normal file
View File

@ -0,0 +1,11 @@
project = "sfxr.lua"
description = "A port of the sfxr sound effect synthesizer to Lua"
title = "sfxr.lua Documentation"
format = "markdown"
style = "!pale"
kind_names = {topic = "Topics", module = "API"}
dir = "./doc"
file = "sfxr.lua"
topics = {
}

0
doc/.gitkeep Normal file
View File

433
sfxr.lua
View File

@ -21,27 +21,67 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
]]-- ]]--
--- A port of the sfxr sound effect synthesizer to Lua
-- @module sfxr
local sfxr = {} local sfxr = {}
local bit = bit32 or require("bit") local bit = bit32 or require("bit")
-- Constants -- Constants
sfxr.VERSION = "0.0.1" --- The module version (SemVer)
sfxr.VERSION = "0.0.2"
sfxr.SQUARE = 0 --- Wave form constants
sfxr.SAWTOOTH = 1 -- @field SQUARE square wave (`= 0`)
sfxr.SINE = 2 -- @field SAW saw wave (`= 1`)
sfxr.NOISE = 3 -- @field SINE sine wave (`= 2`)
-- @field NOISE white noise (`= 3`)
sfxr.WAVEFORM = {
SQUARE = 0,
[0] = 0,
SAW = 1,
[1] = 1,
SINE = 2,
[2] = 2,
NOISE = 3,
[3] = 3
}
sfxr.FREQ_44100 = 44100 --- Sample rate constants
sfxr.FREQ_22050 = 22050 -- (use the number values directly, these are just for lookup)
sfxr.BITS_FLOAT = 0 -- @field 22050 22.05 kHz (`= 22050`)
sfxr.BITS_16 = 16 -- @field 44100 44.1 kHz (`= 44100`)
sfxr.BITS_8 = 8 sfxr.SAMPLERATE = {
[22050] = 22050 --- 22.05 kHz
[44100] = 44100, --- 44.1 kHz
}
--- Bit depth constants
-- (use the number values directly, these are just for lookup)
-- @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 constants
-- @field LITTLE little endian (`= 0`)
-- @field BIG big endian (`= 1`)
sfxr.ENDIANNESS = {
LITTLE = 0,
[0] = 0,
BIG = 1,
[1] = 1
}
-- Utilities -- Utilities
-- Simulates a C int cast --- Truncate a number to an unsigned integer.
-- @tparam number n a (signed) number
-- @treturn int the number, truncated and unsigned
local function trunc(n) local function trunc(n)
if n >= 0 then if n >= 0 then
return math.floor(n) return math.floor(n)
@ -50,7 +90,8 @@ local function trunc(n)
end end
end end
-- Sets the random seed and initializes the generator --- Set the random seed and initializes the generator.
-- @tparam number seed the random seed
local function setseed(seed) local function setseed(seed)
math.randomseed(seed) math.randomseed(seed)
for i=0, 5 do for i=0, 5 do
@ -58,22 +99,36 @@ local function setseed(seed)
end end
end end
-- Returns a random number between low and high --- 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) local function random(low, high)
return low + math.random() * (high - low) return low + math.random() * (high - low)
end end
-- Returns a random boolean weighted to false by n --- Return a random boolean weighted towards false by n.
local function maybe(n) -- w = 1: uniform distribution
return trunc(random(0, n or 1)) == 0 -- 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 end
-- Clamps n between min and max --- 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) local function clamp(n, min, max)
return math.max(min or -math.huge, math.min(max or math.huge, n)) return math.max(min or -math.huge, math.min(max or math.huge, n))
end end
-- Copies a table (shallow) or a primitive --- Copy a table (shallow) or a primitive.
-- @param t a table or primitive
-- @return a copy of t
local function shallowcopy(t) local function shallowcopy(t)
if type(t) == "table" then if type(t) == "table" then
local t2 = {} local t2 = {}
@ -86,7 +141,10 @@ local function shallowcopy(t)
end end
end end
-- Merges table t2 into t1 --- 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) local function mergetables(t1, t2)
for k, v in pairs(t2) do for k, v in pairs(t2) do
if type(v) == "table" then if type(v) == "table" then
@ -102,8 +160,10 @@ local function mergetables(t1, t2)
return t1 return t1
end end
-- Packs a number into a IEEE754 32-bit big-endian floating point binary string --- 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
-- @see https://stackoverflow.com/questions/14416734/
local function packIEEE754(number) local function packIEEE754(number)
if number == 0 then if number == 0 then
return string.char(0x00, 0x00, 0x00, 0x00) return string.char(0x00, 0x00, 0x00, 0x00)
@ -139,7 +199,10 @@ local function packIEEE754(number)
end end
end end
-- Unpacks a IEEE754 32-bit big-endian floating point string to a number --- Unpack a IEEE754 32-bit big-endian floating point string to a number.
-- @tparam string packed a binary string
-- @treturn number a number
-- @see https://stackoverflow.com/questions/14416734/
local function unpackIEEE754(packed) local function unpackIEEE754(packed)
local b1, b2, b3, b4 = string.byte(packed, 1, 4) local b1, b2, b3, b4 = string.byte(packed, 1, 4)
local exponent = (b1 % 0x80) * 0x02 + math.floor(b2 / 0x80) local exponent = (b1 % 0x80) * 0x02 + math.floor(b2 / 0x80)
@ -162,19 +225,21 @@ local function unpackIEEE754(packed)
return math.ldexp(mantissa, exponent - 0x7F) return math.ldexp(mantissa, exponent - 0x7F)
end end
-- Constructor --- The constructor for the Sound class.
-- @treturn Sound a Sound instance
function sfxr.newSound(...) function sfxr.newSound(...)
local instance = setmetatable({}, sfxr.Sound) local instance = setmetatable({}, sfxr.Sound)
instance:__init(...) instance:__init(...)
return instance return instance
end end
-- The main Sound class --- The main Sound class.
-- @type Sound
sfxr.Sound = {} sfxr.Sound = {}
sfxr.Sound.__index = sfxr.Sound sfxr.Sound.__index = sfxr.Sound
--- Initialize the Sound instance.
-- Called by @{sfxr.newSound|the constructor}.
function sfxr.Sound:__init() function sfxr.Sound:__init()
-- Build tables to store the parameters in -- Build tables to store the parameters in
self.volume = {} self.volume = {}
@ -187,49 +252,128 @@ function sfxr.Sound:__init()
self.lowpass = {} self.lowpass = {}
self.highpass = {} self.highpass = {}
-- These won't be affected by Sound.resetParameters() -- These are not affected by resetParameters()
self.supersamples = 8 --- Number of supersampling passes to perform (*default* 8)
self.supersampling = 8
--- Master volume (*default* 0.5)
self.volume.master = 0.5 self.volume.master = 0.5
--- Additional gain (*default* 0.5)
self.volume.sound = 0.5 self.volume.sound = 0.5
self:resetParameters() self:resetParameters()
end end
--- Set all parameters to their default values.
-- Called by @{sfxr.Sound:__init|the initializer}.
function sfxr.Sound:resetParameters() function sfxr.Sound:resetParameters()
-- Set all parameters to the default values --- Repeat speed:
-- Times to repeat the frequency slide over the course of the envelope
-- (*default* 0.0, *min* 0.0, *max* 1.0)
self.repeatspeed = 0.0 self.repeatspeed = 0.0
self.wavetype = sfxr.SQUARE --- (@{WAVEFORM|*WAVEFORM*}) The base wave form
self.waveform = sfxr.SQUARE
--- Attack time:
-- Time the sound takes to reach its peak volume
-- (*default* 0.0, *min* 0.0, *max* 1.0)
self.envelope.attack = 0.0 self.envelope.attack = 0.0
--- Sustain time:
-- Time the sound sustains on its peak volume
-- (*default* 0.0, *min* 0.0, *max* 1.0)
self.envelope.sustain = 0.3 self.envelope.sustain = 0.3
--- Sustain punch:
-- Amount by which the sound peak volume is increased at the start of the
-- sustain time
-- (*default* 0.0, *min* 0.0, *max* 1.0)
self.envelope.punch = 0.0 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)
self.envelope.decay = 0.4 self.envelope.decay = 0.4
--- Start frequency:
-- Base tone of the sound, before sliding
-- (*default* 0.0, *min* 0.0, *max* 1.0)
self.frequency.start = 0.3 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)
self.frequency.min = 0.0 self.frequency.min = 0.0
--- Slide:
-- Amount by which the frequency is increased or decreased through time
-- (*default* 0.0, *min* -1.0, *max* 1.0)
self.frequency.slide = 0.0 self.frequency.slide = 0.0
--- Delta slide:
-- Amount by which the slide is increased or decreased through time
-- (*default* 0.0, *min* -1.0, *max* 1.0)
self.frequency.dslide = 0.0 self.frequency.dslide = 0.0
--- Vibrato depth:
-- Amount of vibrato-like amplitude (volume) modulation
-- (*default* 0.0, *min* 0.0, *max* 1.0)
self.vibrato.depth = 0.0 self.vibrato.depth = 0.0
--- Vibrato speed:
-- Oscillation speed of the vibrato
-- (*default* 0.0, *min* 0.0, *max* 1.0)
self.vibrato.speed = 0.0 self.vibrato.speed = 0.0
--- Vibrato delay:
-- Unused and unimplemented
-- (*default* 0.0, *min* 0.0, *max* 1.0)
self.vibrato.delay = 0.0 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)
self.change.amount = 0.0 self.change.amount = 0.0
--- Change speed:
-- Time before the frequency change happens
-- (*default* 0.0, *min* 0.0, *max* 1.0)
self.change.speed = 0.0 self.change.speed = 0.0
--- Square duty:
-- Width of the square wave pulse cycle (doesn't affect other wave forms)
-- (*default* 0.0, *min* 0.0, *max* 1.0)
self.duty.ratio = 0.0 self.duty.ratio = 0.0
--- Duty sweep:
-- Amount by which the square duty is increased or decreased through time
-- (*default* 0.0, *min* -1.0, *max* 1.0)
self.duty.sweep = 0.0 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)
self.phaser.offset = 0.0 self.phaser.offset = 0.0
--- Phaser sweep:
-- Amount by which the phaser offset is increased or decreased through time
-- (*default* 0.0, *min* -1.0, *max* 1.0)
self.phaser.sweep = 0.0 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)
self.lowpass.cutoff = 1.0 self.lowpass.cutoff = 1.0
--- Lowpass filter cutoff sweep:
-- Amount by which the LP filter cutoff is increased or decreased
-- through time
-- (*default* 0.0, *min* -1.0, *max* 1.0)
self.lowpass.sweep = 0.0 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)
self.lowpass.resonance = 0.0 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)
self.highpass.cutoff = 0.0 self.highpass.cutoff = 0.0
--- Highpass filter cutoff sweep:
-- Amount by which the HP filter cutoff is increased or decreased
-- through time
-- (*default* 0.0, *min* -1.0, *max* 1.0)
self.highpass.sweep = 0.0 self.highpass.sweep = 0.0
end end
--- Clamp all parameters within sane ranges.
function sfxr.Sound:sanitizeParameters() function sfxr.Sound:sanitizeParameters()
self.repeatspeed = clamp(self.repeatspeed, 0, 1) self.repeatspeed = clamp(self.repeatspeed, 0, 1)
self.wavetype = clamp(self.wavetype, sfxr.SQUARE, sfxr.NOISE) self.wavetype = clamp(self.wavetype, sfxr.SQUARE, sfxr.NOISE)
@ -264,15 +408,20 @@ function sfxr.Sound:sanitizeParameters()
self.highpass.sweep = clamp(self.highpass.sweep, -1, 1) self.highpass.sweep = clamp(self.highpass.sweep, -1, 1)
end end
function sfxr.Sound:generate(freq, bits) --- Generate the sound and yield the sample data.
-- Basically the main synthesizing function, yields the sample data -- @tparam[opt=44100] SAMPLERATE rate the sample rate
-- @tparam[opt=0] BITDEPTH depth the bit depth
-- @treturn function() a generator that yields the sample data when called
-- @usage for s in sound:generate(44100, 0) do
-- -- do something with s
-- end
function sfxr.Sound:generate(rate, depth)
rate = rate or 44100
depth = depth or 0
assert(sfxr.SAMPLERATE[rate], "Invalid sample rate: " .. tostring(rate))
assert(sfxr.BITDEPTH[depth], "Invalid bit depth: " .. tostring(depth))
freq = freq or sfxr.FREQ_44100 -- Initialize all locals
bits = bits or sfxr.BITS_FLOAT
assert(freq == sfxr.FREQ_44100 or freq == sfxr.FREQ_22050, "Invalid freq argument")
assert(bits == sfxr.BITS_FLOAT or bits == sfxr.BITS_16 or bits == sfxr.BITS_8, "Invalid bits argument")
-- Initialize ALL the locals!
local fperiod, maxperiod local fperiod, maxperiod
local slide, dslide local slide, dslide
local square_duty, square_slide local square_duty, square_slide
@ -281,7 +430,7 @@ function sfxr.Sound:generate(freq, bits)
local phaserbuffer = {} local phaserbuffer = {}
local noisebuffer = {} local noisebuffer = {}
-- Reset the sample buffers -- Initialize the sample buffers
for i=1, 1024 do for i=1, 1024 do
phaserbuffer[i] = 0 phaserbuffer[i] = 0
end end
@ -290,6 +439,7 @@ function sfxr.Sound:generate(freq, bits)
noisebuffer[i] = random(-1, 1) noisebuffer[i] = random(-1, 1)
end end
--- Reset the sound period
local function reset() local function reset()
fperiod = 100 / (self.frequency.start^2 + 0.001) fperiod = 100 / (self.frequency.start^2 + 0.001)
maxperiod = 100 / (self.frequency.min^2 + 0.001) maxperiod = 100 / (self.frequency.min^2 + 0.001)
@ -314,6 +464,7 @@ function sfxr.Sound:generate(freq, bits)
chg_limit = trunc((1 - self.change.speed)^2 * 20000 + 32) chg_limit = trunc((1 - self.change.speed)^2 * 20000 + 32)
end end
end end
local phase = 0 local phase = 0
reset() reset()
@ -354,7 +505,7 @@ function sfxr.Sound:generate(freq, bits)
rep_limit = 0 rep_limit = 0
end end
-- Yay, the main closure -- The main closure (returned as a generator)
local function next() local function next()
-- Repeat when needed -- Repeat when needed
@ -434,7 +585,7 @@ function sfxr.Sound:generate(freq, bits)
-- And finally the actual tone generation and supersampling -- And finally the actual tone generation and supersampling
local ssample = 0 local ssample = 0
for si = 0, self.supersamples-1 do for si = 0, self.supersampling-1 do
local sample = 0 local sample = 0
phase = phase + 1 phase = phase + 1
@ -450,12 +601,12 @@ function sfxr.Sound:generate(freq, bits)
end end
end end
-- Tone generators ahead!!! -- Tone generators ahead
local fp = phase / period local fp = phase / period
-- Square, including square duty -- Square, including square duty
if self.wavetype == sfxr.SQUARE then if self.waveform == sfxr.SQUARE then
if fp < square_duty then if fp < square_duty then
sample = 0.5 sample = 0.5
else else
@ -463,15 +614,15 @@ function sfxr.Sound:generate(freq, bits)
end end
-- Sawtooth -- Sawtooth
elseif self.wavetype == sfxr.SAWTOOTH then elseif self.waveform == sfxr.SAWTOOTH then
sample = 1 - fp * 2 sample = 1 - fp * 2
-- Sine -- Sine
elseif self.wavetype == sfxr.SINE then elseif self.waveform == sfxr.SINE then
sample = math.sin(fp * 2 * math.pi) sample = math.sin(fp * 2 * math.pi)
-- Pitched white noise -- Pitched white noise
elseif self.wavetype == sfxr.NOISE then elseif self.waveform == sfxr.NOISE then
sample = noisebuffer[trunc(phase * 32 / period) % 32 + 1] sample = noisebuffer[trunc(phase * 32 / period) % 32 + 1]
end end
@ -505,7 +656,7 @@ function sfxr.Sound:generate(freq, bits)
end end
-- Apply the volumes -- Apply the volumes
ssample = (ssample / self.supersamples) * self.volume.master ssample = (ssample / self.supersampling) * self.volume.master
ssample = ssample * (2 * self.volume.sound) ssample = ssample * (2 * self.volume.sound)
-- Hard limit -- Hard limit
@ -513,7 +664,7 @@ function sfxr.Sound:generate(freq, bits)
-- Frequency conversion -- Frequency conversion
second_sample = not second_sample second_sample = not second_sample
if freq == sfxr.FREQ_22050 and second_sample then if rate == 22050 and second_sample then
-- hah! -- hah!
local nsample = next() local nsample = next()
if nsample then if nsample then
@ -524,9 +675,9 @@ function sfxr.Sound:generate(freq, bits)
end end
-- bit conversions -- bit conversions
if bits == sfxr.BITS_FLOAT then if depth == 0 then
return ssample return ssample
elseif bits == sfxr.BITS_16 then elseif depth == 16 then
return trunc(ssample * 32000) return trunc(ssample * 32000)
else else
return trunc(ssample * 127 + 128) return trunc(ssample * 127 + 128)
@ -536,93 +687,124 @@ function sfxr.Sound:generate(freq, bits)
return next return next
end end
function sfxr.Sound:getEnvelopeLimit(freq) --- Get the maximum sample limit allowed by the current envelope.
local env_length = {self.envelope.attack^2 * 100000, -- Does not take any other limits into account, so the returned count might be
self.envelope.sustain^2 * 100000, -- higher than samples actually generated. Still useful though.
self.envelope.decay^2 * 100000} -- @tparam[opt=44100] SAMPLERATE rate the sample rate
function sfxr.Sound:getEnvelopeLimit(rate)
rate = rate or 44100
assert(sfxr.SAMPLERATE[rate], "Invalid sample 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) local limit = trunc(env_length[1] + env_length[2] + env_length[3] + 2)
if freq == nil or freq == sfxr.FREQ_44100 then return math.ceil(limit / (rate / 44100))
return limit
elseif freq == sfxr.FREQ_22050 then
return math.ceil(limit / 2)
else
error("Invalid freq argument")
end
end end
function sfxr.Sound:generateTable(freq, bits) --- Generate the sound into a table.
local t = {} -- @tparam[opt=44100] SAMPLERATE rate the sample 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)
function sfxr.Sound:generateTable(rate, depth, tab)
rate = rate or 44100
depth = depth or 0
assert(sfxr.SAMPLERATE[rate], "Invalid sample 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 local i = 1
for v in self:generate(freq, bits) do for v in self:generate(rate, depth) do
t[i] = v t[i] = v
i = i + 1 i = i + 1
end end
return t return t, i
end end
function sfxr.Sound:generateString(freq, bits, endianness) --- Generate the sound to a binary string.
assert(bits == sfxr.BITS_16 or bits == sfxr.BITS_8, "Invalid bits argument") -- @tparam[opt=44100] SAMPLERATE rate the sample rate
assert(endianness == "big" or endianness == "little", "Invalid endianness") -- @tparam[opt=16] BITDEPTH depth the bit depth (may not be @{BITDEPTH|0})
local s = "" -- @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
function sfxr.Sound:generateString(rate, depth, endianness)
rate = rate or 44100
depth = depth or 16
assert(sfxr.SAMPLERATE[rate], "Invalid sample 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 = {} local buf = {}
buf[100] = 0 buf[100] = 0
local i = 1 local bi = 1
for v in self:generate(freq, bits) do local i = 0
if bits == sfxr.BITS_8 then for v in self:generate(rate, depth) do
if depth == 8 then
buf[i] = v buf[i] = v
i = i + 1 bi = bi + 1
else else
if endianness == "big" then if endianness == sfxr.ENDIANNESS.BIG then
buf[i] = bit.rshift(v, 8) buf[bi] = bit.rshift(v, 8)
buf[i + 1] = bit.band(v, 0xFF) buf[bi + 1] = bit.band(v, 0xFF)
i = i + 2 bi = bi + 2
else else
buf[i] = bit.band(v, 0xFF) buf[bi] = bit.band(v, 0xFF)
buf[i + 1] = bit.rshift(v, 8) buf[bi + 1] = bit.rshift(v, 8)
i = i + 2 bi = bi + 2
end end
end end
if i >= 100 then if bi >= 100 then
s = s .. string.char(unpack(buf)) s = s .. string.char(unpack(buf))
i = 0 bi = 0
end end
i = i + 1
end end
-- pass in up to 100 characters
s = s .. string.char(unpack(buf, i, 100)) s = s .. string.char(unpack(buf, i, 100))
return s return s, i
end end
function sfxr.Sound:generateSoundData(freq, bits) --- Synthesize the sound to a LÖVE SoundData instance.
freq = freq or sfxr.FREQ_44100 -- @tparam[opt=44100] SAMPLERATE rate the sample rate
local tab = self:generateTable(freq, sfxr.BITS_FLOAT) -- @tparam[opt=0] BITDEPTH depth the bit depth
-- @tparam[opt] love.sound.SoundData a SoundData instance (will be created if
-- not passed)
-- @treturn love.sound.SoundData a SoundData instance
-- @treturn int the number of written samples
function sfxr.Sound:generateSoundData(rate, depth, sounddata)
rate = rate or 44100
depth = depth or 0
assert(sfxr.SAMPLERATE[rate], "Invalid sample rate: " .. tostring(rate))
assert(sfxr.BITDEPTH[depth] and depth, "Invalid bit depth: " .. tostring(depth))
if #tab == 0 then local tab, count = self:generateTable(rate, depth)
if count == 0 then
return nil return nil
end end
local data = love.sound.newSoundData(#tab, freq, bits, 1) local data = sounddata or love.sound.newSoundData(count, freq, bits, 1)
for i = 0, #tab - 1 do for i = 0, #tab - 1 do
data:setSample(i, tab[i + 1]) data:setSample(i, tab[i + 1])
end end
return data return data, count
end
function sfxr.Sound:play(freq, bits)
local data = self:generateSoundData(freq, bits)
if data then
local source = love.audio.newSource(data)
source:play()
return source
end
end end
--- Randomize all sound parameters
-- @tparam[opt] number seed a random seed
function sfxr.Sound:randomize(seed) function sfxr.Sound:randomize(seed)
if seed then setseed(seed) end if seed then setseed(seed) end
@ -683,14 +865,18 @@ function sfxr.Sound:randomize(seed)
self:sanitizeParameters() self:sanitizeParameters()
end end
function sfxr.Sound:mutate(amount, seed, changeFreq) --- Mutate all sound parameters
-- @tparam[opt=1] amount by how much to mutate the parameters
-- @tparam[opt] number seed a random seed
-- @tparam[changefreq=true] bool whether to change the frequency parameters
function sfxr.Sound:mutate(amount, seed, changefreq)
if seed then setseed(seed) end if seed then setseed(seed) end
local amount = (amount or 1) local amount = (amount or 1)
local a = amount / 20 local a = amount / 20
local b = (1 - a) * 10 local b = (1 - a) * 10
local changeFreq = (changeFreq == nil) and true or changeFreq local changefreq = (changefreq == nil) and true or changefreq
if changeFreq == true then if changefreq == true then
if maybe(b) then self.frequency.start = self.frequency.start + random(-a, a) end 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.slide = self.frequency.slide + random(-a, a) end
if maybe(b) then self.frequency.dslide = self.frequency.dslide + random(-a, a) end if maybe(b) then self.frequency.dslide = self.frequency.dslide + random(-a, a) end
@ -725,6 +911,8 @@ function sfxr.Sound:mutate(amount, seed, changeFreq)
self:sanitizeParameters() self:sanitizeParameters()
end end
--- Randomize all sound parameters to generate an item pick up sound
-- @tparam[opt] number seed a random seed
function sfxr.Sound:randomPickup(seed) function sfxr.Sound:randomPickup(seed)
if seed then setseed(seed) end if seed then setseed(seed) end
self:resetParameters() self:resetParameters()
@ -740,6 +928,8 @@ function sfxr.Sound:randomPickup(seed)
end end
end end
--- Randomize all sound parameters to generate a laser sound
-- @tparam[opt] number seed a random seed
function sfxr.Sound:randomLaser(seed) function sfxr.Sound:randomLaser(seed)
if seed then setseed(seed) end if seed then setseed(seed) end
self:resetParameters() self:resetParameters()
@ -784,6 +974,8 @@ function sfxr.Sound:randomLaser(seed)
end end
end end
--- Randomize all sound parameters to generate an explosion sound
-- @tparam[opt] number seed a random seed
function sfxr.Sound:randomExplosion(seed) function sfxr.Sound:randomExplosion(seed)
if seed then setseed(seed) end if seed then setseed(seed) end
self:resetParameters() self:resetParameters()
@ -824,6 +1016,8 @@ function sfxr.Sound:randomExplosion(seed)
end end
end end
--- Randomize all sound parameters to generate a power up sound
-- @tparam[opt] number seed a random seed
function sfxr.Sound:randomPowerup(seed) function sfxr.Sound:randomPowerup(seed)
if seed then setseed(seed) end if seed then setseed(seed) end
self:resetParameters() self:resetParameters()
@ -850,6 +1044,8 @@ function sfxr.Sound:randomPowerup(seed)
self.envelope.decay = random(0.1, 0.5) self.envelope.decay = random(0.1, 0.5)
end end
--- Randomize all sound parameters to generate a hit sound
-- @tparam[opt] number seed a random seed
function sfxr.Sound:randomHit(seed) function sfxr.Sound:randomHit(seed)
if seed then setseed(seed) end if seed then setseed(seed) end
self:resetParameters() self:resetParameters()
@ -872,6 +1068,8 @@ function sfxr.Sound:randomHit(seed)
end end
end end
--- Randomize all sound parameters to generate a jump sound
-- @tparam[opt] number seed a random seed
function sfxr.Sound:randomJump(seed) function sfxr.Sound:randomJump(seed)
if seed then setseed(seed) end if seed then setseed(seed) end
self:resetParameters() self:resetParameters()
@ -893,6 +1091,8 @@ function sfxr.Sound:randomJump(seed)
end end
end end
--- Randomize all sound parameters to generate a blip sound
-- @tparam[opt] number seed a random seed
function sfxr.Sound:randomBlip(seed) function sfxr.Sound:randomBlip(seed)
if seed then setseed(seed) end if seed then setseed(seed) end
self:resetParameters() self:resetParameters()
@ -909,11 +1109,15 @@ function sfxr.Sound:randomBlip(seed)
self.highpass.cutoff = 0.1 self.highpass.cutoff = 0.1
end end
function sfxr.Sound:exportWAV(f, freq, bits) --- Generate and export the audio data to a PCM WAVE file.
freq = freq or sfxr.FREQ_44100 -- @tparam ?string|file|love.filesystem.File f a file path, a Lua file object, a love.filesystem.File instance
bits = bits or sfxr.BITS_16 -- @tparam[opt=44100] SAMPLERATE rate the sample rate
assert(freq == sfxr.FREQ_44100 or freq == sfxr.FREQ_22050, "Invalid freq argument") -- @tparam[opt=0] BITDEPTH depth the bit depth
assert(bits == sfxr.BITS_16 or bits == sfxr.BITS_8, "Invalid bits argument") function sfxr.Sound:exportWAV(f, rate, depth)
rate = rate or 44100
depth = depth or 16
assert(sfxr.SAMPLERATE[rate], "Invalid sample rate: " .. tostring(rate))
assert(sfxr.BITDEPTH[depth] and depth ~= 0, "Invalid bit depth: " .. tostring(depth))
local close = false local close = false
if type(f) == "string" then if type(f) == "string" then
@ -987,10 +1191,10 @@ function sfxr.Sound:exportWAV(f, freq, bits)
-- Aand write the actual sample data -- Aand write the actual sample data
local samples = 0 local samples = 0
for v in self:generate(freq, bits) do for v in self:generate(rate, depth) do
samples = samples + 1 samples = samples + 1
if bits == sfxr.BITS_16 then if depth == 16 then
-- wrap around a bit -- wrap around a bit
if v >= 256^2 then v = 0 end if v >= 256^2 then v = 0 end
if v < 0 then v = 256^2 + v end if v < 0 then v = 256^2 + v end
@ -1011,6 +1215,9 @@ function sfxr.Sound:exportWAV(f, freq, bits)
end end
end end
--- Save the sound parameters to a file as a Lua table
-- @tparam ?string|file|love.filesystem.File f a file path, a Lua file object, a love.filesystem.File instance
-- @tparam[opt=true] whether to minify the output
function sfxr.Sound:save(f, compressed) function sfxr.Sound:save(f, compressed)
local close = false local close = false
if type(f) == "string" then if type(f) == "string" then
@ -1065,6 +1272,8 @@ function sfxr.Sound:save(f, compressed)
end end
end end
--- Load the sound parameters from a file containing a Lua table
-- @tparam ?string|file|love.filesystem.File f a file path, a Lua file object, a love.filesystem.File instance
function sfxr.Sound:load(f) function sfxr.Sound:load(f)
local close = false local close = false
if type(f) == "string" then if type(f) == "string" then
@ -1082,7 +1291,7 @@ function sfxr.Sound:load(f)
local params, version = assert(loadstring(code))() local params, version = assert(loadstring(code))()
-- check version compatibility -- check version compatibility
if version > sfxr.VERSION then if version > sfxr.VERSION then
return version error("Incompatible version: " .. tostring(version))
end end
self:resetParameters() self:resetParameters()
@ -1094,6 +1303,8 @@ function sfxr.Sound:load(f)
end end
end end
--- Save the sound parameters to a file in the sfxr binary format
-- @tparam ?string|file|love.filesystem.File f a file path, a Lua file object, a love.filesystem.File instance
function sfxr.Sound:saveBinary(f) function sfxr.Sound:saveBinary(f)
local close = false local close = false
if type(f) == "string" then if type(f) == "string" then
@ -1148,6 +1359,8 @@ function sfxr.Sound:saveBinary(f)
end end
end end
--- Load the sound parameters from a file in the sfxr binary format
-- @tparam ?string|file|love.filesystem.File f a file path, a Lua file object, a love.filesystem.File instance
function sfxr.Sound:loadBinary(f) function sfxr.Sound:loadBinary(f)
local close = false local close = false
if type(f) == "string" then if type(f) == "string" then