From b40e21afe7d55413fda7a7d6fae3294589cf8918 Mon Sep 17 00:00:00 2001 From: Martijn de Boer Date: Sat, 15 Oct 2022 17:39:32 +0200 Subject: [PATCH] threading, sound generation, effects --- assets.lua | 5 +- assets/explosion.png | Bin 0 -> 6909 bytes assets/sounds/explosion.ogg | Bin 0 -> 40070 bytes captain/player.lua | 3 +- conf.lua | 5 +- main.lua | 8 +- planet/mapthread.lua | 38 + planet/planet.lua | 83 +- scene/game.lua | 164 +++- ship/blackstar5.lua | 2 + ship/decorator.lua | 2 + ship/excaliburiv.lua | 2 + vendor/flux.lua | 224 +++++ vendor/sfxr.lua | 1536 +++++++++++++++++++++++++++++++++++ 14 files changed, 1995 insertions(+), 77 deletions(-) create mode 100644 assets/explosion.png create mode 100644 assets/sounds/explosion.ogg create mode 100644 planet/mapthread.lua create mode 100644 vendor/flux.lua create mode 100644 vendor/sfxr.lua diff --git a/assets.lua b/assets.lua index 45b022e..51a70fe 100644 --- a/assets.lua +++ b/assets.lua @@ -10,7 +10,8 @@ multilily = lily.loadMulti({ {lily.newImage, "assets/ships/decorator.png"}, {lily.newImage, "assets/ships/particle.png"}, {lily.newImage, "assets/particle.png"}, - {lily.newImage, "assets/tiles/gf.png"} + {lily.newImage, "assets/tiles/gf.png"}, + {lily.newImage, "assets/explosion.png"} }) multilily:onComplete(function(_, lilies) gameLogo = lilies[1][1] @@ -24,8 +25,8 @@ multilily:onComplete(function(_, lilies) CaptainJohn.ship.image = lilies[9][1] boosterParticle = lilies[10][1] titleParticle = lilies[11][1] - tileGroundFull = lilies[12][1] + explosionParticle = lilies[13][1] windowWidth = love.graphics.getWidth() windowHeight = love.graphics.getHeight() diff --git a/assets/explosion.png b/assets/explosion.png new file mode 100644 index 0000000000000000000000000000000000000000..69c6dacf841326f13466cd0b523fb8d8c9f27bea GIT binary patch literal 6909 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 DrhB~A}&1Jaq$dLl&e?U>%PN@xT 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

P1>(dl6l!`Rnx1e8F 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 g{v^G2@w}=C%FneobU 0 then + love.graphics.setCanvas(self.canvas) + love.graphics.setColor(1,1,1,1) - love.graphics.setColor(1,1,1,1) - - local sprite = nil - local pos = 0 - for y=1, 20 do - for x=1, 25 do - sprite = nil - pos = self.map[y][x] - if pos > 0 then - sprite = self.spritemap["s"..pos] - love.graphics.draw(sprite, (x-1)*32, (600-98) - (y-1)*32) + local sprite = nil + local pos = 0 + for y=1, math.min(maplen,20) do + for x=1, 25 do + sprite = nil + pos = self.map[y][x] + if pos > 0 then + sprite = self.spritemap["s"..pos] + love.graphics.draw(sprite, (x-1)*32, (600-98) - (y-1)*32) + end end end - end - love.graphics.setCanvas() + love.graphics.setCanvas() + end end + +function Planet:generateMapBlock() + self.channel.tomap:push( { message= "generateblock", width= 25, height= 60 } ) +end \ No newline at end of file diff --git a/scene/game.lua b/scene/game.lua index 47fd2d8..5034760 100644 --- a/scene/game.lua +++ b/scene/game.lua @@ -7,6 +7,8 @@ function GameScene:init() GameScene:playerChanged(currentPlayer) movementDelta = 0 + love.audio.setPosition( windowWidth/2, windowHeight/2,0) + pSystem = love.graphics.newParticleSystem(boosterParticle,128) pSystem:setParticleLifetime(0,0.6) pSystem:setLinearAcceleration(0,20,0,25) @@ -28,46 +30,81 @@ function GameScene:init() 1,1,1,0.3 ) + eSystem = love.graphics.newParticleSystem(explosionParticle,64) + eSystem:setParticleLifetime(0,0.7) + eSystem:setLinearAcceleration(-2,-2,2,2) + eSystem:setSpeed(2,8) + eSystem:setRotation(1,7) + eSystem:setSpin(1,2) + eSystem:setRadialAcceleration(10,20) + eSystem:setTangentialAcceleration(10,20) + eSystem:setSizeVariation(0.9) + eSystem:setSizes(0.1, 0.2, 0.4, 0.6, 0.8, 1.0, 0.6, 0.1) + eSystem:setColors( + 1,1,1,1, + 1,1,1,1, + 1,1,1,1, + 1,1,1,1, + 1,1,1,0.8, + 1,1,1,0.6, + 1,1,1,0.4, + 1,1,1,0.3 + ) + + GameScene.explosionObj = {angle=100, radius=100} + GameScene.godsrayObj = {weight=0} + currentPlanet:init() currentPlanet:draw() end function GameScene:update(dt) - pSystem:update(dt) - GameScene:updateInput(dt) - GameScene:updateBounds(dt) - if currentPlanet ~= nil then - love.graphics.print("hebebs",100,100); - currentPlanet.update(dt) + if currentPlayer.death == false and currentPlanet.hasGeneratedMap == true then + pSystem:update(dt) + + GameScene:updateInput(dt) + GameScene:updateBounds(dt) + + + GameScene:checkCollisions() + else + eSystem:update(dt) end + + currentPlanet:update(dt) + self.updateEffects(); end function GameScene:draw() - love.graphics.setColor(255,255,255, 1) + love.graphics.setColor(1,1,1, 1) if currentPlanet ~= nil and currentPlanet.canvas ~= nil then love.graphics.draw(currentPlanet.canvas,0,0) end - love.graphics.draw(pSystem, currentPlayer.px-2, currentPlayer.py) - love.graphics.draw(pSystem, currentPlayer.px+2, currentPlayer.py) - love.graphics.draw( - currentPlayer.ship.image, - currentPlayer.px, - currentPlayer.py, - currentPlayer.rot*(math.pi/180), - 1, 1, - currentPlayer.ship.image:getWidth()/2, - currentPlayer.ship.image:getHeight()/2 - ) + if currentPlayer.death == false then - love.graphics.print(currentPlayer.px..","..currentPlayer.py.." s: " .. currentPlayer.speed .. " a: " ..currentPlayer.acc,10,10) + love.graphics.setColor(1,1,1, 1) + love.graphics.draw(pSystem, currentPlayer.px-2, currentPlayer.py) + love.graphics.draw(pSystem, currentPlayer.px+2, currentPlayer.py) + love.graphics.draw( + currentPlayer.ship.image, + currentPlayer.px, + currentPlayer.py, + currentPlayer.rot*(math.pi/180), + 1, 1, + currentPlayer.ship.image:getWidth()/2, + currentPlayer.ship.image:getHeight()/2 + ) + else + love.graphics.draw(eSystem, currentPlayer.px, currentPlayer.py) + end end function GameScene:drawHud() - love.graphics.setColor(255,255,255, 1) + love.graphics.setColor(1,1,1, 1) love.graphics.draw( gameHud, 0, 600-66 ) love.graphics.setColor(255,255,255, 0.75) @@ -77,6 +114,17 @@ function GameScene:drawHud() love.graphics.print( currentPlayer.name, 48, 550 ) love.graphics.print( currentPlayer:getShipname(), 48, 566 ) + love.graphics.print( + "glbl: " .. currentPlayer.px..","..currentPlayer.py.." s: " .. currentPlayer.ship.speed .. " a: " ..currentPlayer.acc, + 10,10 + ) + + local deathval = "0" + if currentPlayer.death then + deathval = "1" + end + + love.graphics.print( "death: " .. deathval, 10, 24 ) end function GameScene:keypressed(key,unicode) @@ -91,11 +139,12 @@ end function GameScene:playerChanged(to) currentPlayer = to - currentPlayer.px = windowWidth/2 - currentPlayer.py = windowHeight/2 + currentPlayer.px = math.floor(windowWidth/2) + currentPlayer.py = math.floor(windowHeight/2) currentPlayer.rot = 0 currentPlayer.acc = 0 currentPlayer.bounds = { x1= 20, y1= 20, x2= windowWidth-20, y2= windowHeight - 86} + currentPlayer.death = false end function GameScene:updateInput(dt) @@ -143,9 +192,9 @@ function GameScene:updateInput(dt) end if isUp or isDown or isLeft or isRight then - currentPlayer.acc = currentPlayer.acc + currentPlayer.accIncr + currentPlayer.acc = currentPlayer.acc + currentPlayer.ship.accIncr elseif currentPlayer.acc > 0 then - currentPlayer.acc = currentPlayer.acc - currentPlayer.accIncr + currentPlayer.acc = currentPlayer.acc - currentPlayer.ship.accIncr end currentPlayer.acc = math.clamp(0, currentPlayer.acc, 1) @@ -156,24 +205,79 @@ function GameScene:updateInput(dt) end if isUp then - currentPlayer.py = currentPlayer.py - (currentPlayer.speed*currentPlayer.acc) + currentPlayer.py = math.floor(currentPlayer.py - (currentPlayer.ship.speed*currentPlayer.acc)) end if isDown then - currentPlayer.py = currentPlayer.py + (currentPlayer.speed*currentPlayer.acc) + currentPlayer.py = math.floor(currentPlayer.py + (currentPlayer.ship.speed*currentPlayer.acc)) end if isLeft then - currentPlayer.px = currentPlayer.px - (currentPlayer.speed*currentPlayer.acc) + currentPlayer.px = math.floor(currentPlayer.px - (currentPlayer.ship.speed*currentPlayer.acc)) end if isRight then - currentPlayer.px = currentPlayer.px + (currentPlayer.speed*currentPlayer.acc) + currentPlayer.px = math.floor(currentPlayer.px + (currentPlayer.ship.speed*currentPlayer.acc)) end end end function GameScene:updateBounds(dt) - currentPlayer.px = math.clamp( currentPlayer.bounds.x1, currentPlayer.px, currentPlayer.bounds.x2 ) - currentPlayer.py = math.clamp( currentPlayer.bounds.y1, currentPlayer.py, currentPlayer.bounds.y2 ) + currentPlayer.px = math.floor(math.clamp( currentPlayer.bounds.x1, currentPlayer.px, currentPlayer.bounds.x2 )) + currentPlayer.py = math.floor(math.clamp( currentPlayer.bounds.y1, currentPlayer.py, currentPlayer.bounds.y2 )) +end + +function GameScene:checkCollisions() + -- Get basic alpha blended collision + local cp = currentPlanet.canvas:newImageData( nil, nil, currentPlayer.px, currentPlayer.py, 1, 1):getPixel( 0,0 ) + if cp == 0 then + currentPlayer.death = true + self:generateExplosion( currentPlayer.px, currentPlayer.py ) + end +end + +function GameScene:generateExplosion(px,py) + GameScene:generateExplosionSound(px,py) + GameScene.explosionObj = {angle=100, radius=100} + GameScene.godsrayObj = {weight=0} + + flux.to(GameScene.explosionObj, 0.5, {angle= 100, radius=1600}):ease("quintout"):after( GameScene.explosionObj, 0.5, { angle= 100, radius= 100 }) + flux.to(GameScene.godsrayObj, 0.5, {weight=100}):ease("quintout"):after( GameScene.godsrayObj, 0.5, { weight=1 }) + + eSystem:emit(32) +end + +function GameScene:updateEffects() + effect.chromasep.angle= math.floor(GameScene.explosionObj.angle/100) + effect.chromasep.radius= math.floor(GameScene.explosionObj.radius/100) + effect.godsray.weight= GameScene.godsrayObj.weight/100 +end + +function GameScene:generateExplosionSound(px,py) + local sound = sfxr.newSound() + sound:resetParameters() + sound.waveform = sfxr.WAVEFORM.NOISE + sound.frequency.start = GameScene:variance(0.19907648104897,150) + sound.frequency.slide = -0.057634852747835 + sound.repeatspeed = 0 + sound.envelope.attack = 0 + sound.envelope.sustain = GameScene:variance(0.30484231377123, 150) + sound.envelope.punch = GameScene:variance(0.7393770288859, 150) + sound.envelope.decay = 0.43495283918775 + sound.phaser.offset = GameScene:variance(0.53418301267674, 150) + sound.phaser.sweep = -0.26648187020582 + sound.vibrato.depth = GameScene:variance(0.24860915303688, 150) + sound.vibrato.speed = 0.44703997451237 + sound.change.speed = 0.83105037145914 + sound.change.amount = -0.66873272021076 + + local soundData = sound:generateSoundData() + local source = love.audio.newSource(soundData) + source:setVolume(2) + source:setPosition(px,py) + source:play() +end + +function GameScene:variance(seed, variance) + return seed * ((math.random( 100 - (variance/2), 100 + (variance/2)))/100) end \ No newline at end of file diff --git a/ship/blackstar5.lua b/ship/blackstar5.lua index 96d4afd..03c57e6 100644 --- a/ship/blackstar5.lua +++ b/ship/blackstar5.lua @@ -4,4 +4,6 @@ function Blackstar5:new() Blackstar5.super.new(self) self.name = "Blackstar 5"; self.image = nil + self.speed = 5 + self.accIncr = 0.1 end diff --git a/ship/decorator.lua b/ship/decorator.lua index 3b03f6c..0719449 100644 --- a/ship/decorator.lua +++ b/ship/decorator.lua @@ -4,4 +4,6 @@ function Decorator:new() Decorator.super.new(self) self.name = "The Decorator"; self.image = nil + self.speed = 6 + self.accIncr = 0.2 end diff --git a/ship/excaliburiv.lua b/ship/excaliburiv.lua index 06ed0b3..d96d3dd 100644 --- a/ship/excaliburiv.lua +++ b/ship/excaliburiv.lua @@ -4,4 +4,6 @@ function ExcaliburIV:new() ExcaliburIV.super.new(self) self.name = "Excalibur IV"; self.image = nil + self.speed = 4 + self.accIncr = 0.3 end diff --git a/vendor/flux.lua b/vendor/flux.lua new file mode 100644 index 0000000..fd4d799 --- /dev/null +++ b/vendor/flux.lua @@ -0,0 +1,224 @@ +-- +-- flux +-- +-- Copyright (c) 2016 rxi +-- +-- This library is free software; you can redistribute it and/or modify it +-- under the terms of the MIT license. See LICENSE for details. +-- + +local flux = { _version = "0.1.5" } +flux.__index = flux + +flux.tweens = {} +flux.easing = { linear = function(p) return p end } + +local easing = { + quad = "p * p", + cubic = "p * p * p", + quart = "p * p * p * p", + quint = "p * p * p * p * p", + expo = "2 ^ (10 * (p - 1))", + sine = "-math.cos(p * (math.pi * .5)) + 1", + circ = "-(math.sqrt(1 - (p * p)) - 1)", + back = "p * p * (2.7 * p - 1.7)", + elastic = "-(2^(10 * (p - 1)) * math.sin((p - 1.075) * (math.pi * 2) / .3))" +} + +local makefunc = function(str, expr) + local load = loadstring or load + return load("return function(p) " .. str:gsub("%$e", expr) .. " end")() +end + +for k, v in pairs(easing) do + flux.easing[k .. "in"] = makefunc("return $e", v) + flux.easing[k .. "out"] = makefunc([[ + p = 1 - p + return 1 - ($e) + ]], v) + flux.easing[k .. "inout"] = makefunc([[ + p = p * 2 + if p < 1 then + return .5 * ($e) + else + p = 2 - p + return .5 * (1 - ($e)) + .5 + end + ]], v) +end + + + +local tween = {} +tween.__index = tween + +local function makefsetter(field) + return function(self, x) + local mt = getmetatable(x) + if type(x) ~= "function" and not (mt and mt.__call) then + error("expected function or callable", 2) + end + local old = self[field] + self[field] = old and function() old() x() end or x + return self + end +end + +local function makesetter(field, checkfn, errmsg) + return function(self, x) + if checkfn and not checkfn(x) then + error(errmsg:gsub("%$x", tostring(x)), 2) + end + self[field] = x + return self + end +end + +tween.ease = makesetter("_ease", + function(x) return flux.easing[x] end, + "bad easing type '$x'") +tween.delay = makesetter("_delay", + function(x) return type(x) == "number" end, + "bad delay time; expected number") +tween.onstart = makefsetter("_onstart") +tween.onupdate = makefsetter("_onupdate") +tween.oncomplete = makefsetter("_oncomplete") + + +function tween.new(obj, time, vars) + local self = setmetatable({}, tween) + self.obj = obj + self.rate = time > 0 and 1 / time or 0 + self.progress = time > 0 and 0 or 1 + self._delay = 0 + self._ease = "quadout" + self.vars = {} + for k, v in pairs(vars) do + if type(v) ~= "number" then + error("bad value for key '" .. k .. "'; expected number") + end + self.vars[k] = v + end + return self +end + + +function tween:init() + for k, v in pairs(self.vars) do + local x = self.obj[k] + if type(x) ~= "number" then + error("bad value on object key '" .. k .. "'; expected number") + end + self.vars[k] = { start = x, diff = v - x } + end + self.inited = true +end + + +function tween:after(...) + local t + if select("#", ...) == 2 then + t = tween.new(self.obj, ...) + else + t = tween.new(...) + end + t.parent = self.parent + self:oncomplete(function() flux.add(self.parent, t) end) + return t +end + + +function tween:stop() + flux.remove(self.parent, self) +end + + + +function flux.group() + return setmetatable({}, flux) +end + + +function flux:to(obj, time, vars) + return flux.add(self, tween.new(obj, time, vars)) +end + + +function flux:update(deltatime) + for i = #self, 1, -1 do + local t = self[i] + if t._delay > 0 then + t._delay = t._delay - deltatime + else + if not t.inited then + flux.clear(self, t.obj, t.vars) + t:init() + end + if t._onstart then + t._onstart() + t._onstart = nil + end + t.progress = t.progress + t.rate * deltatime + local p = t.progress + local x = p >= 1 and 1 or flux.easing[t._ease](p) + for k, v in pairs(t.vars) do + t.obj[k] = v.start + x * v.diff + end + if t._onupdate then t._onupdate() end + if p >= 1 then + flux.remove(self, i) + if t._oncomplete then t._oncomplete() end + end + end + end +end + + +function flux:clear(obj, vars) + for t in pairs(self[obj]) do + if t.inited then + for k in pairs(vars) do t.vars[k] = nil end + end + end +end + + +function flux:add(tween) + -- Add to object table, create table if it does not exist + local obj = tween.obj + self[obj] = self[obj] or {} + self[obj][tween] = true + -- Add to array + table.insert(self, tween) + tween.parent = self + return tween +end + + +function flux:remove(x) + if type(x) == "number" then + -- Remove from object table, destroy table if it is empty + local obj = self[x].obj + self[obj][self[x]] = nil + if not next(self[obj]) then self[obj] = nil end + -- Remove from array + self[x] = self[#self] + return table.remove(self) + end + for i, v in ipairs(self) do + if v == x then + return flux.remove(self, i) + end + end +end + + + +local bound = { + to = function(...) return flux.to(flux.tweens, ...) end, + update = function(...) return flux.update(flux.tweens, ...) end, + remove = function(...) return flux.remove(flux.tweens, ...) end, +} +setmetatable(bound, flux) + +return bound \ No newline at end of file diff --git a/vendor/sfxr.lua b/vendor/sfxr.lua new file mode 100644 index 0000000..05f7268 --- /dev/null +++ b/vendor/sfxr.lua @@ -0,0 +1,1536 @@ +-- 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 \ No newline at end of file