├── examples ├── assets │ ├── people.mp3 │ └── drum_and_bass.mp3 └── main.lua ├── README.md ├── LICENSE ├── docs ├── .rtfm.lua ├── style.css ├── index.html └── rtfm.lua └── sone.lua /examples/assets/people.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camchenry/sone/HEAD/examples/assets/people.mp3 -------------------------------------------------------------------------------- /examples/assets/drum_and_bass.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/camchenry/sone/HEAD/examples/assets/drum_and_bass.mp3 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sone 2 | Sone is a sound processing library for LÖVE. 3 | 4 | [Documentation](https://camchenry.github.io/sone) 5 | 6 | # When to use sone 7 | Sone was made for quickly iterating on a sound effect, then going back and preprocessing the sound later. Use sone if: 8 | * You want to not have to export a new sound effect each time you make a change. 9 | * You can afford to generate effects in real time. 10 | * You just want cool sound effects 11 | 12 | # Features 13 | * Filters 14 | * Lowpass 15 | * Highpass 16 | * Bandpass 17 | * Allpass 18 | * Notch 19 | * Lowshelf 20 | * Highshelf 21 | * Peak EQ 22 | * Amplification 23 | * Panning 24 | * Fading in 25 | * Fading out 26 | 27 | # Example 28 | ```lua 29 | sone = require 'sone' 30 | sound = love.sound.newSoundData(...) 31 | 32 | -- NOTE: All sone functions will alter the sound data directly. 33 | 34 | -- Filter out all sounds above 150Hz. 35 | sone.filter(sound, { 36 | type = "lowpass", 37 | frequency = 150, 38 | }) 39 | 40 | -- Boost sound at 1000Hz 41 | sone.filter(sound, { 42 | type = "peakeq", 43 | frequency = 1000, 44 | gain = 9, 45 | }) 46 | 47 | -- Boost everything below 150Hz by 6dB 48 | sone.filter(sound, { 49 | type = "lowshelf", 50 | frequency = 150, 51 | gain = 6, 52 | }) 53 | 54 | -- Amplify sound by 3dB 55 | sone.amplify(sound, 3) 56 | 57 | -- Pan sound to the left ear 58 | sone.pan(sound, -1) 59 | 60 | -- Fade in sound over 5 seconds 61 | sone.fadeIn(sound, 5) 62 | 63 | -- Fade in sound over 5 seconds, and also fade out the last 5 seconds 64 | sone.fadeInOut(sound, 5) 65 | ``` 66 | 67 | # Building documentation 68 | To build the HTML documentation, run: 69 | ```bash 70 | cd docs 71 | lua rtfm.lua ../sone.lua > index.html 72 | ``` 73 | -------------------------------------------------------------------------------- /examples/main.lua: -------------------------------------------------------------------------------- 1 | package.path = package.path .. ";../?.lua" 2 | sone = require "sone" 3 | 4 | function love.load(arg) 5 | currentSound = nil 6 | 7 | samples = { 8 | "assets/drum_and_bass.mp3", 9 | "assets/people.mp3", 10 | } 11 | 12 | currentSample = 1 13 | 14 | function generateSounds() 15 | if currentSound then 16 | currentSound:stop() 17 | currentSound = nil 18 | end 19 | 20 | 21 | sounds = { 22 | original = love.sound.newSoundData(samples[currentSample]), 23 | } 24 | 25 | sounds.lowpass = sone.filter(sone.copy(sounds.original), { 26 | type = "lowpass", 27 | frequency = 150, 28 | }) 29 | 30 | sounds.highpass = sone.filter(sone.copy(sounds.original), { 31 | type = "highpass", 32 | frequency = 1000, 33 | }) 34 | 35 | sounds.bandpass = sone.filter(sone.copy(sounds.original), { 36 | type = "bandpass", 37 | frequency = 1000, 38 | Q = 0.866, 39 | gain = -3, 40 | }) 41 | 42 | sounds.notch = sone.filter(sone.copy(sounds.original), { 43 | type = "notch", 44 | frequency = 1000, 45 | Q = 0.8, 46 | gain = 6, 47 | }) 48 | 49 | sounds.allpass = sone.filter(sone.copy(sounds.original), { 50 | type = "allpass", 51 | frequency = 0, 52 | }) 53 | 54 | -- Boost sound at 1000Hz 55 | sounds.peakeq = sone.filter(sone.copy(sounds.original), { 56 | type = "peakeq", 57 | frequency = 1000, 58 | gain = 9, 59 | }) 60 | 61 | -- Boost everything below 150Hz by 6dB 62 | sounds.lowshelf = sone.filter(sone.copy(sounds.original), { 63 | type = "lowshelf", 64 | frequency = 150, 65 | gain = 6, 66 | }) 67 | 68 | -- Boost everything above 4kHz by 12dB 69 | sounds.highshelf = sone.filter(sone.copy(sounds.original), { 70 | type = "highshelf", 71 | frequency = 4000, 72 | gain = 12, 73 | }) 74 | 75 | -- Amplify sound by 4.5dB 76 | sounds.amplified = sone.amplify(sone.copy(sounds.original), 4.5) 77 | 78 | sounds.leftpan = sone.pan(sone.copy(sounds.original), -1) 79 | sounds.rightpan = sone.pan(sone.copy(sounds.original), 1) 80 | 81 | sounds.fadein = sone.fadeIn(sone.copy(sounds.original), 5) 82 | sounds.fadeout = sone.fadeOut(sone.copy(sounds.original), 5) 83 | 84 | sounds.fadeinout = sone.fadeInOut(sone.copy(sounds.original), 5) 85 | 86 | soundList = { 87 | sounds.original, 88 | sounds.lowpass, 89 | sounds.highpass, 90 | sounds.bandpass, 91 | sounds.amplified, 92 | sounds.peakeq, 93 | sounds.highshelf, 94 | sounds.leftpan, 95 | sounds.fadeinout, 96 | } 97 | end 98 | 99 | generateSounds() 100 | 101 | text = [[ 102 | Press TAB to change the sample sound 103 | Press 1 for original, unaltered sound 104 | Press 2 for lowpass 105 | Press 3 for highpass 106 | Press 4 for bandpass 107 | Press 5 for amplified (+4.5dB) 108 | Press 6 for peak EQ 109 | Press 7 for highshelf filter 110 | Press 8 for left pan 111 | Press 9 for fade in and fade out 112 | ]] 113 | love.graphics.setNewFont(love.window.toPixels(24)) 114 | 115 | 116 | function play(n) 117 | if currentSound ~= nil then 118 | currentSound:stop() 119 | end 120 | currentSound = love.audio.newSource(soundList[n]) 121 | currentSound:play() 122 | end 123 | end 124 | 125 | function love.update(dt) 126 | 127 | end 128 | 129 | function love.keypressed(key) 130 | if key == "tab" then 131 | currentSample = currentSample + 1 132 | if currentSample > #samples then 133 | currentSample = 1 134 | end 135 | generateSounds() 136 | return 137 | end 138 | 139 | key = tonumber(key) 140 | if key ~= nil and key > 0 and key < 10 then 141 | play(key) 142 | end 143 | end 144 | 145 | function love.draw() 146 | love.graphics.printf(text, 20, 20, love.graphics.getWidth(), "left") 147 | end 148 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Cameron McHenry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | =============================================================================== 24 | 25 | Some easing functions were adapted from Emmanual Oga's easing library. 26 | (https://github.com/EmmanuelOga/easing). 27 | 28 | Tweener authors, 29 | Yuichi Tateno, 30 | Emmanuel Oga 31 | 32 | The MIT License 33 | -------- 34 | Copyright (c) 2010, Emmanuel Oga. 35 | 36 | Permission is hereby granted, free of charge, to any person obtaining a copy 37 | of this software and associated documentation files (the "Software"), to deal 38 | in the Software without restriction, including without limitation the rights 39 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 40 | copies of the Software, and to permit persons to whom the Software is 41 | furnished to do so, subject to the following conditions: 42 | 43 | The above copyright notice and this permission notice shall be included in 44 | all copies or substantial portions of the Software. 45 | 46 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 47 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 48 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 49 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 50 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 51 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 52 | THE SOFTWARE. 53 | 54 | =============================================================================== 55 | Adapted from 56 | Tweener's easing functions (Penner's Easing Equations) 57 | and http://code.google.com/p/tweener/ (jstweener javascript version) 58 | 59 | Disclaimer for Robert Penner's Easing Equations license: 60 | 61 | TERMS OF USE - EASING EQUATIONS 62 | 63 | Open source under the BSD License. 64 | 65 | Copyright © 2001 Robert Penner 66 | All rights reserved. 67 | 68 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 69 | 70 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 71 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 72 | * Neither the name of the author nor the names of contributors may be used to endorse or promote products derived from this software without specific prior written permission. 73 | 74 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 75 | -------------------------------------------------------------------------------- /docs/.rtfm.lua: -------------------------------------------------------------------------------- 1 | template.title="Sone API Reference" 2 | template.escapePattern = '{@(.-)@}' 3 | template.outputPattern = '{@=(.-)@}' 4 | template.text=[[ 5 | 6 | 7 | 8 | 9 | {@= self.title @} 10 | {@ local cdn = 'https://cdnjs.cloudflare.com/ajax/libs' @} 11 | 12 | 13 | 14 | 15 | 16 | 17 | {@ 18 | local idMap = {} 19 | local typenames = {} 20 | 21 | for _, tag in ipairs(tags.flat) do 22 | if tag.typename then typenames[tag.typename] = tag end 23 | end 24 | 25 | local primitives = { 26 | ['nil'] = true, 27 | ['number'] = true, 28 | ['string'] = true, 29 | ['boolean'] = true, 30 | ['table'] = true, 31 | ['function'] = true, 32 | ['thread'] = true, 33 | ['userdata'] = true, 34 | } 35 | 36 | @} 37 | 38 | {@ define('list', 'name', function (tag) @} 39 | {@= tag.name @} 40 | {@ end) @} 41 | 42 | {@ define('list', 'name and prev and prev.id == id', function (tag) @} 43 | , {@= ' ' .. tag.name @} 44 | {@ end) @} 45 | 46 | {@ define('overview', 'typename', function (tag) @} 47 |
  • 48 | {@= tag.typename @} 49 | 50 |
  • 51 | {@ end) @} 52 | 53 | {@ define('type', 'type', function (tag) @} 54 | 55 | {@ 56 | for m1, m2, m3 in tag.type:gmatch('([^%a]*)([%a]+)(.?)') do 57 | if typenames[m2] then 58 | write(m1 .. '' 59 | .. m2 .. '' .. m3) 60 | elseif primitives[m2] then 61 | write(m1 .. '' 62 | .. m2 .. '' .. m3) 63 | else 64 | write(m1 .. '' 65 | .. m2 .. '' .. m3) 66 | end 67 | end 68 | @} 69 | 70 | {@ end) @} 71 | 72 | {@ define('typename', 'typename', function (tag) @} 73 | {@ 74 | local id = '' 75 | if not idMap[tag.typename] and tag.level < 4 then 76 | id = ' id="' .. tag.typename .. '"' 77 | idMap[tag.typename] = tag 78 | end 79 | write('' .. tag.typename .. ' ') 80 | @} 81 | {@ end) @} 82 | 83 | {@ define('link', 'type', function (tag) @} 84 | {@ defer(tag, 'type') @} 85 | {@ end) @} 86 | 87 | {@ define('link', 'type and name', function (tag) @} 88 | {@ defer(tag, 'type') @} 89 | {@= ' ' .. tag.name @} 90 | {@ end) @} 91 | 92 | {@ define('link', 'typename', function (tag) @} 93 | {@ defer(tag, 'typename') @} 94 | {@ end) @} 95 | 96 | {@ define('link', 'typename and parametric', function (tag) @} 97 | {@ defer(tag, 'typename') @} 98 | ({@ descend(tag, 'list', 'id=="param"') @}) 99 | {@ end) @} 100 | 101 | {@ define('article', function (tag) @} 102 |
    103 | {@ if tag.type or tag.typename then @} 104 | {@ 105 | local class = '' 106 | if tag.title then 107 | class = " class='".. tag.title .."'" 108 | end 109 | @} 110 | 111 | {@ defer(tag, 'link') @} 112 | 113 | {@ else @} 114 | {@ end @} 115 |
    116 | {@ if tag.note then @}

    {@= tag.note @}

    {@ end @} 117 | {@ if tag.code then @} 118 |
    119 |                         {@= tag.info @}
    120 |                     
    121 | {@ else @} 122 |

    {@= tag.info @}

    123 | {@ end @} 124 | {@ descend(tag, 'main') @} 125 |
    126 |
    127 | {@ end) @} 128 | 129 | {@ define('main', 'not hidden', function (tag) @} 130 | {@ defer(tag, 'article') @} 131 | {@ end) @} 132 | 133 | {@ define('main', '(prev and prev.id) ~= id and not hidden', function (tag) @} 134 |
    135 | 136 | {@= tag.title or tag.id @} 137 | 138 | {@ defer(tag, 'article') @} 139 |
    140 | {@ end) @} 141 | 142 | {@ define('main', 'level == 1 and not hidden', function (tag) @} 143 |
    144 | {@ defer(tag, 'article') @} 145 |
    146 | {@ end) @} 147 | 148 | {@ if self.overview then @} 149 |
    150 |

    {@= self.title @}

    151 | 154 |
    155 | {@ end @} 156 | 157 | {@ descend(tags, 'main') @} 158 | 159 | 165 | 166 | 167 | 168 | 192 | 193 | 194 | 195 | ]] 196 | -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /** 4 | * 1. Change the default font family in all browsers (opinionated). 5 | * 2. Prevent adjustments of font size after orientation changes in IE and iOS. 6 | */ 7 | 8 | html { 9 | font-family: sans-serif; /* 1 */ 10 | -ms-text-size-adjust: 100%; /* 2 */ 11 | -webkit-text-size-adjust: 100%; /* 2 */ 12 | } 13 | 14 | /** 15 | * Remove the margin in all browsers (opinionated). 16 | */ 17 | 18 | body { 19 | margin: 0; 20 | } 21 | 22 | /* HTML5 display definitions 23 | ========================================================================== */ 24 | 25 | /** 26 | * Add the correct display in IE 9-. 27 | * 1. Add the correct display in Edge, IE, and Firefox. 28 | * 2. Add the correct display in IE. 29 | */ 30 | 31 | article, 32 | aside, 33 | details, /* 1 */ 34 | figcaption, 35 | figure, 36 | footer, 37 | header, 38 | main, /* 2 */ 39 | menu, 40 | nav, 41 | section, 42 | summary { /* 1 */ 43 | display: block; 44 | } 45 | 46 | /** 47 | * Add the correct display in IE 9-. 48 | */ 49 | 50 | audio, 51 | canvas, 52 | progress, 53 | video { 54 | display: inline-block; 55 | } 56 | 57 | /** 58 | * Add the correct display in iOS 4-7. 59 | */ 60 | 61 | audio:not([controls]) { 62 | display: none; 63 | height: 0; 64 | } 65 | 66 | /** 67 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 68 | */ 69 | 70 | progress { 71 | vertical-align: baseline; 72 | } 73 | 74 | /** 75 | * Add the correct display in IE 10-. 76 | * 1. Add the correct display in IE. 77 | */ 78 | 79 | template, /* 1 */ 80 | [hidden] { 81 | display: none; 82 | } 83 | 84 | /* Links 85 | ========================================================================== */ 86 | 87 | /** 88 | * 1. Remove the gray background on active links in IE 10. 89 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 90 | */ 91 | 92 | a { 93 | background-color: transparent; /* 1 */ 94 | -webkit-text-decoration-skip: objects; /* 2 */ 95 | } 96 | 97 | /** 98 | * Remove the outline on focused links when they are also active or hovered 99 | * in all browsers (opinionated). 100 | */ 101 | 102 | a:active, 103 | a:hover { 104 | outline-width: 0; 105 | } 106 | 107 | /* Text-level semantics 108 | ========================================================================== */ 109 | 110 | /** 111 | * 1. Remove the bottom border in Firefox 39-. 112 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 113 | */ 114 | 115 | abbr[title] { 116 | border-bottom: none; /* 1 */ 117 | text-decoration: underline; /* 2 */ 118 | text-decoration: underline dotted; /* 2 */ 119 | } 120 | 121 | /** 122 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 123 | */ 124 | 125 | b, 126 | strong { 127 | font-weight: inherit; 128 | } 129 | 130 | /** 131 | * Add the correct font weight in Chrome, Edge, and Safari. 132 | */ 133 | 134 | b, 135 | strong { 136 | font-weight: bolder; 137 | } 138 | 139 | /** 140 | * Add the correct font style in Android 4.3-. 141 | */ 142 | 143 | dfn { 144 | font-style: italic; 145 | } 146 | 147 | /** 148 | * Correct the font size and margin on `h1` elements within `section` and 149 | * `article` contexts in Chrome, Firefox, and Safari. 150 | */ 151 | 152 | h1 { 153 | font-size: 2em; 154 | margin: 0.67em 0; 155 | } 156 | 157 | /** 158 | * Add the correct background and color in IE 9-. 159 | */ 160 | 161 | mark { 162 | background-color: #ff0; 163 | color: #000; 164 | } 165 | 166 | /** 167 | * Add the correct font size in all browsers. 168 | */ 169 | 170 | small { 171 | font-size: 80%; 172 | } 173 | 174 | /** 175 | * Prevent `sub` and `sup` elements from affecting the line height in 176 | * all browsers. 177 | */ 178 | 179 | sub, 180 | sup { 181 | font-size: 75%; 182 | line-height: 0; 183 | position: relative; 184 | vertical-align: baseline; 185 | } 186 | 187 | sub { 188 | bottom: -0.25em; 189 | } 190 | 191 | sup { 192 | top: -0.5em; 193 | } 194 | 195 | /* Embedded content 196 | ========================================================================== */ 197 | 198 | /** 199 | * Remove the border on images inside links in IE 10-. 200 | */ 201 | 202 | img { 203 | border-style: none; 204 | } 205 | 206 | /** 207 | * Hide the overflow in IE. 208 | */ 209 | 210 | svg:not(:root) { 211 | overflow: hidden; 212 | } 213 | 214 | /* Grouping content 215 | ========================================================================== */ 216 | 217 | /** 218 | * 1. Correct the inheritance and scaling of font size in all browsers. 219 | * 2. Correct the odd `em` font sizing in all browsers. 220 | */ 221 | 222 | code, 223 | kbd, 224 | pre, 225 | samp { 226 | font-family: monospace, monospace; /* 1 */ 227 | font-size: 1em; /* 2 */ 228 | } 229 | 230 | /** 231 | * Add the correct margin in IE 8. 232 | */ 233 | 234 | figure { 235 | margin: 1em 40px; 236 | } 237 | 238 | /** 239 | * 1. Add the correct box sizing in Firefox. 240 | * 2. Show the overflow in Edge and IE. 241 | */ 242 | 243 | hr { 244 | box-sizing: content-box; /* 1 */ 245 | height: 0; /* 1 */ 246 | overflow: visible; /* 2 */ 247 | } 248 | 249 | /* Forms 250 | ========================================================================== */ 251 | 252 | /** 253 | * 1. Change font properties to `inherit` in all browsers (opinionated). 254 | * 2. Remove the margin in Firefox and Safari. 255 | */ 256 | 257 | button, 258 | input, 259 | select, 260 | textarea { 261 | font: inherit; /* 1 */ 262 | margin: 0; /* 2 */ 263 | } 264 | 265 | /** 266 | * Restore the font weight unset by the previous rule. 267 | */ 268 | 269 | optgroup { 270 | font-weight: bold; 271 | } 272 | 273 | /** 274 | * Show the overflow in IE. 275 | * 1. Show the overflow in Edge. 276 | */ 277 | 278 | button, 279 | input { /* 1 */ 280 | overflow: visible; 281 | } 282 | 283 | /** 284 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 285 | * 1. Remove the inheritance of text transform in Firefox. 286 | */ 287 | 288 | button, 289 | select { /* 1 */ 290 | text-transform: none; 291 | } 292 | 293 | /** 294 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 295 | * controls in Android 4. 296 | * 2. Correct the inability to style clickable types in iOS and Safari. 297 | */ 298 | 299 | button, 300 | html [type="button"], /* 1 */ 301 | [type="reset"], 302 | [type="submit"] { 303 | -webkit-appearance: button; /* 2 */ 304 | } 305 | 306 | /** 307 | * Remove the inner border and padding in Firefox. 308 | */ 309 | 310 | button::-moz-focus-inner, 311 | [type="button"]::-moz-focus-inner, 312 | [type="reset"]::-moz-focus-inner, 313 | [type="submit"]::-moz-focus-inner { 314 | border-style: none; 315 | padding: 0; 316 | } 317 | 318 | /** 319 | * Restore the focus styles unset by the previous rule. 320 | */ 321 | 322 | button:-moz-focusring, 323 | [type="button"]:-moz-focusring, 324 | [type="reset"]:-moz-focusring, 325 | [type="submit"]:-moz-focusring { 326 | outline: 1px dotted ButtonText; 327 | } 328 | 329 | /** 330 | * Change the border, margin, and padding in all browsers (opinionated). 331 | */ 332 | 333 | fieldset { 334 | border: 1px solid #c0c0c0; 335 | margin: 0 2px; 336 | padding: 0.35em 0.625em 0.75em; 337 | } 338 | 339 | /** 340 | * 1. Correct the text wrapping in Edge and IE. 341 | * 2. Correct the color inheritance from `fieldset` elements in IE. 342 | * 3. Remove the padding so developers are not caught out when they zero out 343 | * `fieldset` elements in all browsers. 344 | */ 345 | 346 | legend { 347 | box-sizing: border-box; /* 1 */ 348 | color: inherit; /* 2 */ 349 | display: table; /* 1 */ 350 | max-width: 100%; /* 1 */ 351 | padding: 0; /* 3 */ 352 | white-space: normal; /* 1 */ 353 | } 354 | 355 | /** 356 | * Remove the default vertical scrollbar in IE. 357 | */ 358 | 359 | textarea { 360 | overflow: auto; 361 | } 362 | 363 | /** 364 | * 1. Add the correct box sizing in IE 10-. 365 | * 2. Remove the padding in IE 10-. 366 | */ 367 | 368 | [type="checkbox"], 369 | [type="radio"] { 370 | box-sizing: border-box; /* 1 */ 371 | padding: 0; /* 2 */ 372 | } 373 | 374 | /** 375 | * Correct the cursor style of increment and decrement buttons in Chrome. 376 | */ 377 | 378 | [type="number"]::-webkit-inner-spin-button, 379 | [type="number"]::-webkit-outer-spin-button { 380 | height: auto; 381 | } 382 | 383 | /** 384 | * 1. Correct the odd appearance in Chrome and Safari. 385 | * 2. Correct the outline style in Safari. 386 | */ 387 | 388 | [type="search"] { 389 | -webkit-appearance: textfield; /* 1 */ 390 | outline-offset: -2px; /* 2 */ 391 | } 392 | 393 | /** 394 | * Remove the inner padding and cancel buttons in Chrome and Safari on OS X. 395 | */ 396 | 397 | [type="search"]::-webkit-search-cancel-button, 398 | [type="search"]::-webkit-search-decoration { 399 | -webkit-appearance: none; 400 | } 401 | 402 | /** 403 | * Correct the text style of placeholders in Chrome, Edge, and Safari. 404 | */ 405 | 406 | ::-webkit-input-placeholder { 407 | color: inherit; 408 | opacity: 0.54; 409 | } 410 | 411 | /** 412 | * 1. Correct the inability to style clickable types in iOS and Safari. 413 | * 2. Change font properties to `inherit` in Safari. 414 | */ 415 | 416 | ::-webkit-file-upload-button { 417 | -webkit-appearance: button; /* 1 */ 418 | font: inherit; /* 2 */ 419 | } 420 | 421 | 422 | /** Custom CSS **/ 423 | * { 424 | box-sizing: border-box; 425 | } 426 | html, 427 | body { 428 | background-color: #ddd; 429 | color: #222; 430 | font-size: 100%; 431 | font-family: "Source Sans Pro", Helvetica, Arial, sans-serif; 432 | } 433 | pre { 434 | border: 1px solid #999; 435 | padding: 1rem; 436 | background: #eee; 437 | margin-left: 1rem; 438 | } 439 | code { 440 | font-family: "Source Code Pro", monospace; 441 | } 442 | 443 | /* Main block */ 444 | body > div, 445 | body > section > article { 446 | background: #fff; 447 | max-width: 960px; 448 | margin: auto; 449 | padding: 2rem; 450 | box-shadow: 0px 2px 3px #999; 451 | } 452 | 453 | /* Blocks with things beside them */ 454 | body > section > article + article, 455 | body > div + section > article { 456 | margin-top: 2rem; 457 | } 458 | section > h2 { 459 | display: none; 460 | } 461 | section > h3 { 462 | display: none; 463 | } 464 | 465 | /* Functions, methods, etc. title for module */ 466 | section > h4 { 467 | color: #aaa; 468 | font-style: italic; 469 | margin: 8px 0; 470 | } 471 | 472 | /* Arguments, etc */ 473 | section > h5 { 474 | font-weight: bold; 475 | color: #999; 476 | margin: 1rem 0; 477 | font-size: 110%; 478 | } 479 | /* argument description */ 480 | h5 + div > p { 481 | margin-left: 2rem; 482 | } 483 | a { 484 | color: #39c; 485 | text-decoration: none; 486 | } 487 | :target { 488 | background: #f1f824; 489 | } 490 | article > h2 { 491 | } 492 | 493 | /* Section headers */ 494 | article > h3 { 495 | font-size: 200%; 496 | margin-top: 5rem; 497 | } 498 | 499 | /* method/function/field name */ 500 | article > h4 { 501 | margin-bottom: 0; 502 | padding-bottom: 3px; 503 | font-size: 150%; 504 | border-bottom: 1px solid #ccc; 505 | } 506 | article > h4[class="Functions"] { 507 | margin-top: 4rem; 508 | } 509 | h4 + article > h4[class="Functions"] { 510 | margin-top: 0; 511 | } 512 | article > h4 .type { 513 | float: right; 514 | } 515 | article > h5 { 516 | margin: 0; 517 | margin-left: 1rem; 518 | margin-top: 1rem; 519 | font-size: 125%; 520 | font-weight: normal; 521 | } 522 | footer { 523 | text-align: center; 524 | font-style: italic; 525 | margin: 40px; 526 | } 527 | #sone { 528 | font-size: 200%; 529 | } 530 | .type { 531 | color: #666; 532 | } 533 | .unknown { 534 | color: #c39; 535 | } 536 | .primitive { 537 | color: #999; 538 | } 539 | 540 | /* Media queries */ 541 | 542 | @media (max-width: 600px) { 543 | 544 | body { 545 | font-size: 80%; 546 | } 547 | 548 | /* Main block */ 549 | body > div, 550 | body > section > article { 551 | padding: 0.5rem; 552 | } 553 | 554 | } 555 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sone API Reference 6 | 7 | 8 | 9 | 10 |
    11 |

    Sone API Reference

    12 | 54 |

    sone

    55 |

    Functions

    amplify (sound, gain)

    Amplifies a sound by some amount. Clipping will occur if the gain amount is too high. 56 | 57 | **Example** 58 | ```lua 59 | -- Amplify sound by 6dB. 60 | sone.amplify(sound, 6) 61 | 62 | -- Deamplify sound by -2.5dB. 63 | sone.amplify(sound, -2.5) 64 | ```

    65 |
    Arguments
    number gain

    Amplification amount in decibels.

    67 |
    68 |
    Returns
    70 |

    copy (sound, copyOverData)

    Makes a copy of a SoundData. 71 | 72 | **Example** 73 | ```lua 74 | copy = sone.copy(sound) 75 | ```

    76 |
    Arguments
    boolean copyOverData

    (optional) If false, only a new SoundData will be created with the same sample count, sample rate, bit depth, and channels. The actual signal data will not be copied.

    78 |
    79 |
    Returns
    81 |

    fadeIn (sound, seconds, fadeType)

    Fades in a sound to full volume over a number of seconds. 82 | 83 | **Example** 84 | ```lua 85 | -- Fade in sound linearly over 3 seconds. 86 | sone.fadeIn(sound, 3) 87 | 88 | -- Fade in sound exponentially over 10 seconds. 89 | sone.fadeIn(sound, 10, "inOutExpo") 90 | ```

    91 |
    Arguments
    number seconds

    How long the fade will take.

    93 |
    FadeType fadeType

    (optional) Which fade curve to use. Default is linear.

    94 |
    95 |
    Returns
    97 |

    fadeInOut (sound, seconds, fadeType)

    Fades a sound at the beginning and at the end. The first N seconds will be faded in, and the last N seconds will be faded out. 98 | 99 | **Example** 100 | ```lua 101 | -- Fade the first 5 seconds and last 5 seconds of a sound. 102 | sone.fadeInOut(sound, 5) 103 | ```

    104 |
    Arguments
    number seconds

    How long the fade will take.

    106 |
    FadeType fadeType

    (optional) Which fade curve to use. Default is linear.

    107 |
    108 |
    Returns
    110 |

    fadeOut (sound, seconds, fadeType)

    Fades out a sound to zero volume over a number of seconds. 111 | 112 | **Example** 113 | ```lua 114 | -- Fade out sound linearly over 3 seconds. 115 | sone.fadeOut(sound, 3) 116 | 117 | -- Fade out sound exponentially over 10 seconds. 118 | sone.fadeOut(sound, 10, "inOutExpo") 119 | ```

    120 |
    Arguments
    number seconds

    How long the fade will take.

    122 |
    FadeType fadeType

    (optional) Which fade curve to use. Default is linear.

    123 |
    124 |
    Returns
    126 |

    filter (sound, parameters)

    Filters a sound with different filters and settings. 127 | 128 | **Example** 129 | ```lua 130 | -- Filter out all sounds below 1000Hz. 131 | sone.filter(sound, { 132 | type = "highpass", 133 | frequency = 1000, 134 | }) 135 | ```

    136 |
    Arguments
    139 |
    Returns
    141 |

    pan (sound, pan)

    Pans a sound to either the left or right channel. Only works for stereo sounds. 142 | 143 | **Example** 144 | ```lua 145 | -- Play sound 85% in the right channel, 15% in the left channel. 146 | sone.pan(sound, 0.85) 147 | ```

    148 |
    Arguments
    number pan

    How to pan the input (range: -1.0 to 1.0), where -1.0 is far left, 1.0 is far right, and 0.0 is dead center.

    150 |
    151 |
    Returns
    153 |
    154 |

    Contexts

    Using sone for sound processing

    Examples of using sone to process a sound. 155 | ```lua 156 | sone = require 'sone' 157 | sound = love.sound.newSoundData(...) 158 | 159 | -- NOTE: All sone functions will alter the sound data directly. 160 | 161 | -- Filter out all sounds above 150Hz. 162 | sone.filter(sound, { 163 | type = "lowpass", 164 | frequency = 150, 165 | }) 166 | 167 | -- Boost sound at 1000Hz 168 | sone.filter(sound, { 169 | type = "peakeq", 170 | frequency = 1000, 171 | gain = 9, 172 | }) 173 | 174 | -- Boost everything below 150Hz by 6dB 175 | sone.filter(sound, { 176 | type = "lowshelf", 177 | frequency = 150, 178 | gain = 6, 179 | }) 180 | 181 | -- Amplify sound by 3dB 182 | sone.amplify(sound, 3) 183 | 184 | -- Pan sound to the left ear 185 | sone.pan(sound, -1) 186 | 187 | -- Fade in sound over 5 seconds 188 | sone.fadeIn(sound, 5) 189 | 190 | -- Fade in sound over 5 seconds, and also fade out the last 5 seconds 191 | sone.fadeInOut(sound, 5) 192 | 193 | -- Play the sound data 194 | love.audio.newSource(sound):play() 195 | ```

    196 |
    197 |

    Types

    FadeType

    198 |

    Fields

    string inCirc

    199 |

    string inCubic

    200 |

    string inExpo

    201 |

    string inOutCirc

    202 |

    string inOutCubic

    203 |

    string inOutExpo

    204 |

    string inOutQuad

    205 |

    string inOutQuart

    206 |

    string inOutQuint

    207 |

    string inOutSine

    208 |

    string inQuad

    209 |

    string inQuart

    210 |

    string inQuint

    211 |

    string inSine

    212 |

    string linear

    213 |

    string outCirc

    214 |

    string outCubic

    215 |

    string outExpo

    216 |

    string outInCirc

    217 |

    string outInCubic

    218 |

    string outInExpo

    219 |

    string outInQuad

    220 |

    string outInQuart

    221 |

    string outInQuint

    222 |

    string outInSine

    223 |

    string outQuad

    224 |

    string outQuart

    225 |

    string outQuint

    226 |

    string outSine

    227 |
    228 |

    FilterParameters

    A table of the possible parameters for the filter function.

    229 |

    Fields

    number Q

    (optional) The quality factor to use. 230 | Ranges from 0 to 100. Default: 1.

    231 |

    number finish

    (optional) The time (in seconds) for the finish of the filtered section. 232 | Default: the duration of the sound.

    233 |

    number finishSample

    (optional) The finish (in samples) of the filtered section. 234 | Default: the number of samples in the sound.

    235 |

    number frequency

    **REQUIRED** The center/target frequency (in Hz). 236 | Ranges from 0Hz to (Sampling rate) / 2 Hz.

    237 |

    number gain

    (optional) The gain (in dB) to use for EQ filters. 238 | Ranges from -60dB to 60dB. Default: 0dB.

    239 |

    number start

    (optional) The time (in seconds) for the start of the filtered section. 240 | Default: 0 seconds.

    241 |

    number startSample

    (optional) The start (in samples) of the filtered section. 242 | Default: 0.

    243 |

    FilterType type

    **REQUIRED** The type of filter to use.

    244 |
    245 |

    FilterType

    Filters that are able to be used with the filter function. (`sone.filter`)

    246 |

    Fields

    string allpass

    247 |

    string bandpass

    248 |

    string highpass

    249 |

    string highshelf

    250 |

    string lowpass

    251 |

    string lowshelf

    252 |

    string notch

    253 |

    string peakeq

    254 |
    255 |

    SoundData

    A SoundData object from LOVE. 256 | https://www.love2d.org/wiki/SoundData

    257 |
    258 |
    264 | 265 | 266 | 267 | 291 | 292 | 293 | 294 | -------------------------------------------------------------------------------- /sone.lua: -------------------------------------------------------------------------------- 1 | --- @module sone 2 | local sone = { 3 | _VERSION = 'sone v1.0.0', 4 | _DESCRIPTION = 'Sound processing library for LOVE.', 5 | _URL = 'https://github.com/camchenry/sone', 6 | -- See LICENSE file for a full license list. 7 | _LICENSE = [[ 8 | MIT License 9 | 10 | Copyright (c) 2016 Cameron McHenry 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy 13 | of this software and associated documentation files (the "Software"), to deal 14 | in the Software without restriction, including without limitation the rights 15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | copies of the Software, and to permit persons to whom the Software is 17 | furnished to do so, subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in all 20 | copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | SOFTWARE. 29 | ]] 30 | } 31 | 32 | local pow = math.pow 33 | local sin = math.sin 34 | local cos = math.cos 35 | local pi = math.pi 36 | local sqrt = math.sqrt 37 | local min = math.min 38 | local max = math.max 39 | 40 | -- easing library 41 | -- https://github.com/EmmanuelOga/easing 42 | local function linear(t, b, c, d) 43 | return c * t / d + b 44 | end 45 | 46 | local function inQuad(t, b, c, d) 47 | t = t / d 48 | return c * pow(t, 2) + b 49 | end 50 | 51 | local function outQuad(t, b, c, d) 52 | t = t / d 53 | return -c * t * (t - 2) + b 54 | end 55 | 56 | local function inOutQuad(t, b, c, d) 57 | t = t / d * 2 58 | if t < 1 then 59 | return c / 2 * pow(t, 2) + b 60 | else 61 | return -c / 2 * ((t - 1) * (t - 3) - 1) + b 62 | end 63 | end 64 | 65 | local function outInQuad(t, b, c, d) 66 | if t < d / 2 then 67 | return outQuad (t * 2, b, c / 2, d) 68 | else 69 | return inQuad((t * 2) - d, b + c / 2, c / 2, d) 70 | end 71 | end 72 | 73 | local function inCubic (t, b, c, d) 74 | t = t / d 75 | return c * pow(t, 3) + b 76 | end 77 | 78 | local function outCubic(t, b, c, d) 79 | t = t / d - 1 80 | return c * (pow(t, 3) + 1) + b 81 | end 82 | 83 | local function inOutCubic(t, b, c, d) 84 | t = t / d * 2 85 | if t < 1 then 86 | return c / 2 * t * t * t + b 87 | else 88 | t = t - 2 89 | return c / 2 * (t * t * t + 2) + b 90 | end 91 | end 92 | 93 | local function outInCubic(t, b, c, d) 94 | if t < d / 2 then 95 | return outCubic(t * 2, b, c / 2, d) 96 | else 97 | return inCubic((t * 2) - d, b + c / 2, c / 2, d) 98 | end 99 | end 100 | 101 | local function inQuart(t, b, c, d) 102 | t = t / d 103 | return c * pow(t, 4) + b 104 | end 105 | 106 | local function outQuart(t, b, c, d) 107 | t = t / d - 1 108 | return -c * (pow(t, 4) - 1) + b 109 | end 110 | 111 | local function inOutQuart(t, b, c, d) 112 | t = t / d * 2 113 | if t < 1 then 114 | return c / 2 * pow(t, 4) + b 115 | else 116 | t = t - 2 117 | return -c / 2 * (pow(t, 4) - 2) + b 118 | end 119 | end 120 | 121 | local function outInQuart(t, b, c, d) 122 | if t < d / 2 then 123 | return outQuart(t * 2, b, c / 2, d) 124 | else 125 | return inQuart((t * 2) - d, b + c / 2, c / 2, d) 126 | end 127 | end 128 | 129 | local function inQuint(t, b, c, d) 130 | t = t / d 131 | return c * pow(t, 5) + b 132 | end 133 | 134 | local function outQuint(t, b, c, d) 135 | t = t / d - 1 136 | return c * (pow(t, 5) + 1) + b 137 | end 138 | 139 | local function inOutQuint(t, b, c, d) 140 | t = t / d * 2 141 | if t < 1 then 142 | return c / 2 * pow(t, 5) + b 143 | else 144 | t = t - 2 145 | return c / 2 * (pow(t, 5) + 2) + b 146 | end 147 | end 148 | 149 | local function outInQuint(t, b, c, d) 150 | if t < d / 2 then 151 | return outQuint(t * 2, b, c / 2, d) 152 | else 153 | return inQuint((t * 2) - d, b + c / 2, c / 2, d) 154 | end 155 | end 156 | 157 | local function inSine(t, b, c, d) 158 | return -c * cos(t / d * (pi / 2)) + c + b 159 | end 160 | 161 | local function outSine(t, b, c, d) 162 | return c * sin(t / d * (pi / 2)) + b 163 | end 164 | 165 | local function inOutSine(t, b, c, d) 166 | return -c / 2 * (cos(pi * t / d) - 1) + b 167 | end 168 | 169 | local function outInSine(t, b, c, d) 170 | if t < d / 2 then 171 | return outSine(t * 2, b, c / 2, d) 172 | else 173 | return inSine((t * 2) -d, b + c / 2, c / 2, d) 174 | end 175 | end 176 | 177 | local function inExpo(t, b, c, d) 178 | if t == 0 then 179 | return b 180 | else 181 | return c * pow(2, 10 * (t / d - 1)) + b - c * 0.001 182 | end 183 | end 184 | 185 | local function outExpo(t, b, c, d) 186 | if t == d then 187 | return b + c 188 | else 189 | return c * 1.001 * (-pow(2, -10 * t / d) + 1) + b 190 | end 191 | end 192 | 193 | local function inOutExpo(t, b, c, d) 194 | if t == 0 then return b end 195 | if t == d then return b + c end 196 | t = t / d * 2 197 | if t < 1 then 198 | return c / 2 * pow(2, 10 * (t - 1)) + b - c * 0.0005 199 | else 200 | t = t - 1 201 | return c / 2 * 1.0005 * (-pow(2, -10 * t) + 2) + b 202 | end 203 | end 204 | 205 | local function outInExpo(t, b, c, d) 206 | if t < d / 2 then 207 | return outExpo(t * 2, b, c / 2, d) 208 | else 209 | return inExpo((t * 2) - d, b + c / 2, c / 2, d) 210 | end 211 | end 212 | 213 | local function inCirc(t, b, c, d) 214 | t = t / d 215 | return(-c * (sqrt(1 - pow(t, 2)) - 1) + b) 216 | end 217 | 218 | local function outCirc(t, b, c, d) 219 | t = t / d - 1 220 | return(c * sqrt(1 - pow(t, 2)) + b) 221 | end 222 | 223 | local function inOutCirc(t, b, c, d) 224 | t = t / d * 2 225 | if t < 1 then 226 | return -c / 2 * (sqrt(1 - t * t) - 1) + b 227 | else 228 | t = t - 2 229 | return c / 2 * (sqrt(1 - t * t) + 1) + b 230 | end 231 | end 232 | 233 | local function outInCirc(t, b, c, d) 234 | if t < d / 2 then 235 | return outCirc(t * 2, b, c / 2, d) 236 | else 237 | return inCirc((t * 2) - d, b + c / 2, c / 2, d) 238 | end 239 | end 240 | 241 | local easing = { 242 | linear = linear, 243 | inQuad = inQuad, 244 | outQuad = outQuad, 245 | inOutQuad = inOutQuad, 246 | outInQuad = outInQuad, 247 | inCubic = inCubic , 248 | outCubic = outCubic, 249 | inOutCubic = inOutCubic, 250 | outInCubic = outInCubic, 251 | inQuart = inQuart, 252 | outQuart = outQuart, 253 | inOutQuart = inOutQuart, 254 | outInQuart = outInQuart, 255 | inQuint = inQuint, 256 | outQuint = outQuint, 257 | inOutQuint = inOutQuint, 258 | outInQuint = outInQuint, 259 | inSine = inSine, 260 | outSine = outSine, 261 | inOutSine = inOutSine, 262 | outInSine = outInSine, 263 | inExpo = inExpo, 264 | outExpo = outExpo, 265 | inOutExpo = inOutExpo, 266 | outInExpo = outInExpo, 267 | inCirc = inCirc, 268 | outCirc = outCirc, 269 | inOutCirc = inOutCirc, 270 | outInCirc = outInCirc, 271 | } 272 | 273 | local function clamp(val, low, hi) 274 | return max(min(val, hi), low) 275 | end 276 | 277 | -- Biquad filter 278 | -- Taken from (http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt) 279 | local function biquadFilter(sound, parameters) 280 | -- Sample rate 281 | local sr = sound:getSampleRate() 282 | local ch = sound:getChannels() 283 | -- Center frequency 284 | assert(parameters.frequency, "Frequency must be specified for filter") 285 | local freq = clamp(parameters.frequency, 0, sr / 2) 286 | -- Resonance / quality factor 287 | local Q = clamp(parameters.Q or 1, 0, 100) 288 | -- EQ filter gain 289 | local gain = clamp(parameters.gain or 0, -60, 60) 290 | local wet = clamp(parameters.wet or 1, 0, 1) 291 | local type = parameters.type 292 | 293 | local a0, a1, a2, b0, b1, b2 294 | local x0, x1, x2 = 0, 0, 0 295 | local y0, y1, y2 = 0, 0, 0 296 | 297 | local w0 = 2 * pi * freq / sr 298 | local alpha = sin(w0) / (2 * Q) 299 | local cos_w0 = cos(w0) 300 | local A = 10 ^ (gain / 40) 301 | 302 | local function process(x0) 303 | y2, y1 = y1, y0 304 | y0 = (b0 / a0) * x0 + (b1 / a0) * x1 + (b2 / a0) * x2 - (a1 / a0) * y1 - (a2 / a0) * y2 305 | x2, x1 = x1, x0 306 | return y0 307 | end 308 | 309 | if type == "lowpass" then 310 | b0 = (1 - cos_w0)/2 311 | b1 = 1 - cos_w0 312 | b2 = (1 - cos_w0)/2 313 | a0 = 1 + alpha 314 | a1 = -2*cos_w0 315 | a2 = 1 - alpha 316 | elseif type == "highpass" then 317 | b0 = (1 + cos_w0)/2 318 | b1 = -(1 + cos_w0) 319 | b2 = (1 + cos_w0)/2 320 | a0 = 1 + alpha 321 | a1 = -2*cos_w0 322 | a2 = 1 - alpha 323 | elseif type == "bandpass" then 324 | b0 = Q * alpha 325 | b1 = 0 326 | b2 = Q * alpha 327 | a0 = 1 + alpha 328 | a1 = -2*cos_w0 329 | a2 = 1 - alpha 330 | elseif type == "notch" then 331 | b0 = 1 332 | b1 = -2*cos_w0 333 | b2 = 1 334 | a0 = 1 + alpha 335 | a1 = -2*cos_w0 336 | a2 = 1 - alpha 337 | elseif type == "allpass" then 338 | b0 = 1 - alpha 339 | b1 = -2*cos_w0 340 | b2 = 1 + alpha 341 | a0 = 1 + alpha 342 | a1 = -2*cos_w0 343 | a2 = 1 - alpha 344 | elseif type == "peakeq" then 345 | b0 = 1 + alpha*A 346 | b1 = -2*cos_w0 347 | b2 = 1 - alpha*A 348 | a0 = 1 + alpha/A 349 | a1 = -2*cos_w0 350 | a2 = 1 - alpha/A 351 | elseif type == "lowshelf" then 352 | local tsaa = 2 * sqrt(A) * alpha 353 | b0 = A*( (A+1) - (A-1)*cos_w0 + tsaa ) 354 | b1 = 2*A*( (A-1) - (A+1)*cos_w0 ) 355 | b2 = A*( (A+1) - (A-1)*cos_w0 - tsaa ) 356 | a0 = (A+1) + (A-1)*cos_w0 + tsaa 357 | a1 = -2*( (A-1) + (A+1)*cos_w0 ) 358 | a2 = (A+1) + (A-1)*cos_w0 - tsaa 359 | elseif type == "highshelf" then 360 | local tsaa = 2 * sqrt(A) * alpha 361 | b0 = A*( (A+1) + (A-1)*cos_w0 + tsaa ) 362 | b1 = -2*A*( (A-1) + (A+1)*cos_w0 ) 363 | b2 = A*( (A+1) + (A-1)*cos_w0 - tsaa ) 364 | a0 = (A+1) - (A-1)*cos_w0 + tsaa 365 | a1 = 2*( (A-1) - (A+1)*cos_w0 ) 366 | a2 = (A+1) - (A-1)*cos_w0 - tsaa 367 | else 368 | if type == nil then 369 | error("Filter type is a nil value") 370 | else 371 | error("Unsupported filter type: '"..type.."'") 372 | end 373 | end 374 | 375 | local sampleCount = sound:getSampleCount() * ch - 1 376 | local startSample = 0 377 | local finishSample = sampleCount 378 | 379 | if parameters.start then 380 | startSample = parameters.start * sr * ch 381 | elseif parameters.startSample then 382 | startSample = parameters.startSample 383 | end 384 | 385 | if parameters.finish then 386 | -- subtract one because sound indexes are zero-based 387 | finishSample = parameters.finish * sr * ch - 1 388 | elseif parameters.finishSample then 389 | finishSample = parameters.finishSample 390 | end 391 | 392 | startSample = math.floor(startSample) 393 | finishSample = math.floor(finishSample) 394 | assert(startSample >= 0, "Start time cannot be less than zero") 395 | assert(finishSample <= sampleCount, "Finish time cannot be longer than the sound") 396 | 397 | for j = 1, ch do 398 | x0, x1, x2 = 0, 0, 0 399 | y0, y1, y2 = 0, 0, 0 400 | for i=startSample + j - 1, finishSample, ch do 401 | local inputSample = sound:getSample(i) 402 | local outputSample = process(sound:getSample(i)) 403 | outputSample = inputSample * (1 - wet) + (outputSample * wet) 404 | sound:setSample(i, clamp(outputSample, -1, 1)) 405 | end 406 | end 407 | 408 | return sound 409 | end 410 | 411 | --- @function filter 412 | --- Filters a sound with different filters and settings. 413 | --[=[-- 414 | 415 | **Example** 416 | ```lua 417 | -- Filter out all sounds below 1000Hz. 418 | sone.filter(sound, { 419 | type = "highpass", 420 | frequency = 1000, 421 | }) 422 | ``` 423 | --]=] 424 | --- @param SoundData sound 425 | --- @param FilterParameters parameters 426 | --- @return SoundData 427 | function sone.filter(sound, parameters) 428 | return biquadFilter(sound, parameters) 429 | end 430 | 431 | --- @function amplify 432 | --- Amplifies a sound by some amount. Clipping will occur if the gain amount is too high. 433 | --[=[-- 434 | 435 | **Example** 436 | ```lua 437 | -- Amplify sound by 6dB. 438 | sone.amplify(sound, 6) 439 | 440 | -- Deamplify sound by -2.5dB. 441 | sone.amplify(sound, -2.5) 442 | ``` 443 | --]=] 444 | --- @param SoundData sound 445 | --- @param number gain Amplification amount in decibels. 446 | --- @return SoundData 447 | function sone.amplify(sound, gain) 448 | return sone.filter(sound, { 449 | type = "highshelf", 450 | frequency = 0, 451 | gain = gain, 452 | }) 453 | end 454 | 455 | --- @function pan 456 | --- Pans a sound to either the left or right channel. Only works for stereo sounds. 457 | --[=[-- 458 | 459 | **Example** 460 | ```lua 461 | -- Play sound 85% in the right channel, 15% in the left channel. 462 | sone.pan(sound, 0.85) 463 | ``` 464 | --]=] 465 | --- @param SoundData sound 466 | --- @param number pan How to pan the input (range: -1.0 to 1.0), where -1.0 is far left, 1.0 is far right, and 0.0 is dead center. 467 | --- @return SoundData 468 | function sone.pan(sound, pan) 469 | assert(sound:getChannels() == 2, "Pan only works for stereo sounds.") 470 | 471 | pan = clamp((1 + pan) * 0.5, 0, 1) 472 | 473 | local leftGain = sqrt(pan) 474 | local rightGain = sqrt(1 - pan) 475 | 476 | local gains = { 477 | [0] = rightGain, 478 | [1] = leftGain, 479 | } 480 | 481 | local sampleCount = sound:getSampleCount() * sound:getChannels() - 1 482 | for i=0, sampleCount do 483 | sound:setSample(i, sound:getSample(i) * gains[i%2]) 484 | end 485 | 486 | return sound 487 | end 488 | 489 | --- @function fadeIn 490 | --- Fades in a sound to full volume over a number of seconds. 491 | --[=[-- 492 | 493 | **Example** 494 | ```lua 495 | -- Fade in sound linearly over 3 seconds. 496 | sone.fadeIn(sound, 3) 497 | 498 | -- Fade in sound exponentially over 10 seconds. 499 | sone.fadeIn(sound, 10, "inOutExpo") 500 | ``` 501 | --]=] 502 | --- @param SoundData sound 503 | --- @param number seconds How long the fade will take. 504 | --- @param FadeType fadeType (optional) Which fade curve to use. Default is linear. 505 | --- @return SoundData 506 | function sone.fadeIn(sound, seconds, fadeType) 507 | fadeType = fadeType or "linear" 508 | local ease = easing[fadeType] 509 | 510 | local sampleCount = sound:getSampleCount() * sound:getChannels() - 1 511 | local start = 0 512 | local finish = seconds * sound:getSampleRate() * sound:getChannels() 513 | local t 514 | 515 | assert(finish <= sampleCount, "Fade in cannot be longer than the sound") 516 | 517 | for i=start, finish do 518 | t = ease(i, start, 1, finish) 519 | sound:setSample(i, t * sound:getSample(i)) 520 | end 521 | 522 | return sound 523 | end 524 | 525 | --- @function fadeOut 526 | --- Fades out a sound to zero volume over a number of seconds. 527 | --[=[-- 528 | 529 | **Example** 530 | ```lua 531 | -- Fade out sound linearly over 3 seconds. 532 | sone.fadeOut(sound, 3) 533 | 534 | -- Fade out sound exponentially over 10 seconds. 535 | sone.fadeOut(sound, 10, "inOutExpo") 536 | ``` 537 | --]=] 538 | --- @param SoundData sound 539 | --- @param number seconds How long the fade will take. 540 | --- @param FadeType fadeType (optional) Which fade curve to use. Default is linear. 541 | --- @return SoundData 542 | function sone.fadeOut(sound, seconds, fadeType) 543 | fadeType = fadeType or "linear" 544 | local ease = easing[fadeType] 545 | 546 | local sampleCount = sound:getSampleCount() * sound:getChannels() - 1 547 | local duration = seconds * sound:getSampleRate() * sound:getChannels() 548 | local finish = sound:getSampleCount() * sound:getChannels() - 1 549 | local start = finish - duration 550 | local t 551 | 552 | assert(start >= 0, "Fade out cannot be longer than the sound") 553 | 554 | for i=start, finish do 555 | t = 1 - ease(i - start, 0, 1, finish - start) 556 | sound:setSample(i, t * sound:getSample(i)) 557 | end 558 | 559 | return sound 560 | end 561 | 562 | --- @function fadeInOut 563 | --- Fades a sound at the beginning and at the end. The first N seconds will be faded in, and the last N seconds will be faded out. 564 | --[=[-- 565 | 566 | **Example** 567 | ```lua 568 | -- Fade the first 5 seconds and last 5 seconds of a sound. 569 | sone.fadeInOut(sound, 5) 570 | ``` 571 | --]=] 572 | --- @param SoundData sound 573 | --- @param number seconds How long the fade will take. 574 | --- @param FadeType fadeType (optional) Which fade curve to use. Default is linear. 575 | --- @return SoundData 576 | function sone.fadeInOut(sound, seconds, fadeType) 577 | sone.fadeIn(sound, seconds, fadeType) 578 | return sone.fadeOut(sound, seconds, fadeType) 579 | end 580 | 581 | --- @function copy 582 | --- Makes a copy of a SoundData. 583 | --[=[-- 584 | 585 | **Example** 586 | ```lua 587 | copy = sone.copy(sound) 588 | ``` 589 | --]=] 590 | --- @param SoundData sound The sound to copy. 591 | --- @param boolean copyOverData (optional) If false, only a new SoundData will be created with the same sample count, sample rate, bit depth, and channels. The actual signal data will not be copied. 592 | --- @return SoundData The copied sound. 593 | function sone.copy(sound, copyOverData) 594 | local copy = love.sound.newSoundData(sound:getSampleCount(), sound:getSampleRate(), sound:getBitDepth(), sound:getChannels()) 595 | copyOverData = copyOverData == nil and true or copyOverData 596 | 597 | if copyOverData then 598 | local sampleCount = sound:getSampleCount() * sound:getChannels() - 1 599 | for i=0, sampleCount do 600 | copy:setSample(i, sound:getSample(i)) 601 | end 602 | end 603 | 604 | return copy 605 | end 606 | 607 | --- @env Using sone for sound processing 608 | --- Examples of using sone to process a sound. 609 | --[=[-- 610 | ```lua 611 | sone = require 'sone' 612 | sound = love.sound.newSoundData(...) 613 | 614 | -- NOTE: All sone functions will alter the sound data directly. 615 | 616 | -- Filter out all sounds above 150Hz. 617 | sone.filter(sound, { 618 | type = "lowpass", 619 | frequency = 150, 620 | }) 621 | 622 | -- Boost sound at 1000Hz 623 | sone.filter(sound, { 624 | type = "peakeq", 625 | frequency = 1000, 626 | gain = 9, 627 | }) 628 | 629 | -- Boost everything below 150Hz by 6dB 630 | sone.filter(sound, { 631 | type = "lowshelf", 632 | frequency = 150, 633 | gain = 6, 634 | }) 635 | 636 | -- Amplify sound by 3dB 637 | sone.amplify(sound, 3) 638 | 639 | -- Pan sound to the left ear 640 | sone.pan(sound, -1) 641 | 642 | -- Fade in sound over 5 seconds 643 | sone.fadeIn(sound, 5) 644 | 645 | -- Fade in sound over 5 seconds, and also fade out the last 5 seconds 646 | sone.fadeInOut(sound, 5) 647 | 648 | -- Play the sound data 649 | love.audio.newSource(sound):play() 650 | ``` 651 | --]=] 652 | 653 | --- @type FilterType 654 | --- Filters that are able to be used with the filter function. (`sone.filter`) 655 | -- TODO: descriptions for these 656 | --- @field string lowpass 657 | --- @field string highpass 658 | --- @field string bandpass 659 | --- @field string notch 660 | --- @field string allpass 661 | --- @field string peakeq 662 | --- @field string lowshelf 663 | --- @field string highshelf 664 | --- @end type 665 | 666 | --- @type FadeType 667 | --- @field string linear 668 | --- @field string inQuad 669 | --- @field string outQuad 670 | --- @field string inOutQuad 671 | --- @field string outInQuad 672 | --- @field string inCubic 673 | --- @field string outCubic 674 | --- @field string inOutCubic 675 | --- @field string outInCubic 676 | --- @field string inQuart 677 | --- @field string outQuart 678 | --- @field string inOutQuart 679 | --- @field string outInQuart 680 | --- @field string inQuint 681 | --- @field string outQuint 682 | --- @field string inOutQuint 683 | --- @field string outInQuint 684 | --- @field string inSine 685 | --- @field string outSine 686 | --- @field string inOutSine 687 | --- @field string outInSine 688 | --- @field string inExpo 689 | --- @field string outExpo 690 | --- @field string inOutExpo 691 | --- @field string outInExpo 692 | --- @field string inCirc 693 | --- @field string outCirc 694 | --- @field string inOutCirc 695 | --- @field string outInCirc 696 | 697 | --- @type FilterParameters 698 | --- A table of the possible parameters for the filter function. 699 | --- @field FilterType type **REQUIRED** The type of filter to use. 700 | 701 | --- @field number frequency **REQUIRED** The center/target frequency (in Hz). 702 | --- Ranges from 0Hz to (Sampling rate) / 2 Hz. 703 | 704 | --- @field number Q (optional) The quality factor to use. 705 | --- Ranges from 0 to 100. Default: 1. 706 | 707 | --- @field number gain (optional) The gain (in dB) to use for EQ filters. 708 | --- Ranges from -60dB to 60dB. Default: 0dB. 709 | 710 | --- @field number start (optional) The time (in seconds) for the start of the filtered section. 711 | --- Default: 0 seconds. 712 | 713 | --- @field number finish (optional) The time (in seconds) for the finish of the filtered section. 714 | --- Default: the duration of the sound. 715 | 716 | --- @field number startSample (optional) The start (in samples) of the filtered section. 717 | --- Default: 0. 718 | 719 | --- @field number finishSample (optional) The finish (in samples) of the filtered section. 720 | --- Default: the number of samples in the sound. 721 | 722 | --- @field number wet (optional) The wetness of the filtered sound, or the percentage of the effect that will be applied. 723 | --- Default: 1 (100%). Ranges from 0 (0%) to 1 (100%). 724 | 725 | --- @type SoundData 726 | --- A SoundData object from LOVE. 727 | --- https://www.love2d.org/wiki/SoundData 728 | --- @end type 729 | 730 | return sone 731 | -------------------------------------------------------------------------------- /docs/rtfm.lua: -------------------------------------------------------------------------------- 1 | --[=[-- 2 | @module rtfm Read the fucking manual. 3 | 4 | @env Configuration Environment 5 | 6 | This script can be configured by command switches and a config file. 7 | 8 | Configuration options can be specified with command line switches. 9 | They should appear before the list of soure files being processed. 10 | 11 | lua rtfm.lua --template.title='My API Docs' myapi.lua 12 | 13 | A *config file* is a file named `.rtfm.lua` in the current working directory. 14 | Command line switches always override config file settings. 15 | 16 | ```lua 17 | -- .rtfm.lua 18 | template.title = 'My API Docs' 19 | ``` 20 | 21 | The configuration environment runs as an instance of `Generator`; 22 | all members can be accessed as locals. This allows almost every 23 | aspect of RTFM to be configured; even core functionality can be 24 | monkey-patched from the config file. 25 | 26 | @env Custom Template Environment 27 | 28 | Custom templates generate documentation from a list of tags. 29 | 30 | Set a custom template with the `template.text` or `template.path` 31 | configuration options. 32 | 33 | ### Command line: 34 | 35 | lua rtfm.lua --template.path='mytemplate.html' src.lua > out.html 36 | 37 | ### Config file: 38 | 39 | ```lua 40 | template.path='mytemplate.html' 41 | -- or 42 | template.text=[[ ... ]] 43 | ``` 44 | 45 | The custom template environment has access to these locals: 46 | 47 | @field Template self The `Template` object. 48 | 49 | @field {number:TagDef} tags List of tags to apply the template to. 50 | 51 | @function write Append text to output. 52 | @param string text Text to write. 53 | 54 | @function define Define a transformation rule. 55 | @param string mode Transformation mode. 56 | @param string selector Optional node selector. 57 | @param function transform Transformation callback function. 58 | 59 | @function defer Defer to another transformation rule. 60 | @param string context Context node. 61 | @param string mode Transformation mode. 62 | @param string selector Node selector. 63 | 64 | @function descend Descend into child nodes and defer. 65 | @param string context Context node. 66 | @param string mode Transformation mode. 67 | @param string selector Node selector. 68 | 69 | @type TagDef 70 | 71 | @field number level Tag level. Lower levels are parents of higher levels. 72 | @field number group Group priority level. Lower groups come first. 73 | @field string sort The name of a field to sort by after grouping. 74 | @field string pattern Matching pattern. Captures are inserted into fields. 75 | @field string fields Comma-delimited list of fields to populate from pattern. 76 | @field string alias The name of another `TagDef` to inherit from. 77 | @field string title A name to display in the template for this tag. 78 | @field boolean parametric Whether to display a parameter list overview. 79 | 80 | @end type 81 | --]=]-- 82 | local rtfm = {} 83 | 84 | local ALL = '(.*)' 85 | local ONE_WORD = '([^%s]*)%s*(.*)' 86 | local TWO_WORD = '([^%s]+)%s*([^%s]*)%s*(.*)' 87 | 88 | local CREATE_DEFAULT_TAGDEFS = function () 89 | return { 90 | ['file'] = { level = 1, group = 11, title = 'Files', merge = 'typename', 91 | pattern = ONE_WORD, fields = 'typename,info', sort = 'typename', 92 | nest = rtfm.nestMergedTag }, 93 | ['module'] = { alias = 'file', 94 | title = 'Modules', group = 12 }, 95 | ['script'] = { alias = 'file', 96 | title = 'Scripts', group = 13 }, 97 | ['type'] = { level = 2, group = 29, title = 'Types', 98 | pattern = ONE_WORD, fields = 'typename,info', sort = 'typename', 99 | nest = rtfm.nestDocTag }, 100 | ['env'] = { alias = 'type', 101 | pattern = ALL, title = 'Contexts', group = 21 }, 102 | ['class'] = { alias = 'type', 103 | title = 'Classes', group = 22 }, 104 | ['object'] = { alias = 'type', 105 | title = 'Objects', group = 23 }, 106 | ['table'] = { alias = 'type', 107 | title = 'Tables', group = 24 }, 108 | ['interface'] = { alias = 'type', 109 | title = 'Interfaces', group = 25 }, 110 | ['field'] = { level = 3, group = 31, title = 'Fields', 111 | pattern = TWO_WORD, fields = 'type,name,info', sort = 'name', 112 | nest = rtfm.nestDocTag }, 113 | ['function'] = { level = 3, group = 33, title = 'Functions', 114 | pattern = ONE_WORD, fields = 'typename,info', sort = 'typename', 115 | parametric = true, nest = rtfm.nestDocTag }, 116 | ['constructor'] = { alias = 'function', 117 | title = 'Constructors', group = 32 }, 118 | ['method'] = { alias = 'function', 119 | title = 'Methods', group = 34 }, 120 | ['callback'] = { alias = 'function', 121 | title = 'Callbacks', group = 35 }, 122 | ['continue'] = { alias = 'function', 123 | title = 'Continuations', group = 36 }, 124 | ['param'] = { level = 4, title = 'Arguments', 125 | pattern = TWO_WORD, fields = 'type,name,info', 126 | nest = rtfm.nestDocTag }, 127 | ['return'] = { level = 4, title = 'Returns', 128 | pattern = ONE_WORD, fields = 'type,info', 129 | nest = rtfm.nestDocTag }, 130 | ['unknown'] = { level = 4, pattern = TWO_WORD, 131 | fields = 'type,name,info', nest = rtfm.nestDocTag }, 132 | ['end'] = { pattern = ALL, fields = 'what', 133 | nest = rtfm.nestEndTag }, 134 | } 135 | end 136 | 137 | local DEFAULT_TEMPLATE = [[ 138 | 139 | <@= self.title @> 140 | <@ local cdn = 'https://cdnjs.cloudflare.com/ajax/libs' @> 141 | 143 | 187 | <@ 188 | local idMap = {} 189 | local typenames = {} 190 | 191 | for _, tag in ipairs(tags.flat) do 192 | if tag.typename then typenames[tag.typename] = tag end 193 | end 194 | 195 | local primitives = { ['nil'] = true, ['number'] = true, 196 | ['string'] = true, ['boolean'] = true, ['table'] = true, 197 | ['function'] = true, ['thread'] = true, ['userdata'] = true } 198 | @> 199 | 200 | <@ define('list', 'name', function (tag) @> 201 | <@= tag.name @> 202 | <@ end) @> 203 | 204 | <@ define('list', 'name and prev and prev.id == id', function (tag) @> 205 | , <@= ' ' .. tag.name @> 206 | <@ end) @> 207 | 208 | <@ define('overview', 'typename', function (tag) @> 209 |
  • 210 | <@= tag.typename @> 211 |

    <@= tag.info:gsub('%..*', '.') @>

    212 | 213 |
  • 214 | <@ end) @> 215 | 216 | <@ define('type', 'type', function (tag) @> 217 | 218 | <@ 219 | for m1, m2, m3 in tag.type:gmatch('([^%a]*)([%a]+)(.?)') do 220 | if typenames[m2] then 221 | write(m1 .. '' 222 | .. m2 .. '' .. m3) 223 | elseif primitives[m2] then 224 | write(m1 .. '' 225 | .. m2 .. '' .. m3) 226 | else 227 | write(m1 .. '' 228 | .. m2 .. '' .. m3) 229 | end 230 | end 231 | @> 232 | 233 | <@ end) @> 234 | 235 | <@ define('typename', 'typename', function (tag) @> 236 | <@ 237 | local id = '' 238 | if not idMap[tag.typename] and tag.level < 4 then 239 | id = ' id="' .. tag.typename .. '"' 240 | idMap[tag.typename] = tag 241 | end 242 | write('' .. tag.typename .. ' ') 243 | @> 244 | <@ end) @> 245 | 246 | <@ define('link', 'type', function (tag) @> 247 | <@ defer(tag, 'type') @> 248 | <@ end) @> 249 | 250 | <@ define('link', 'type and name', function (tag) @> 251 | <@ defer(tag, 'type') @> 252 | <@= ' ' .. tag.name @> 253 | <@ end) @> 254 | 255 | <@ define('link', 'typename', function (tag) @> 256 | <@ defer(tag, 'typename') @> 257 | <@ end) @> 258 | 259 | <@ define('link', 'typename and parametric', function (tag) @> 260 | <@ defer(tag, 'typename') @> 261 | (<@ descend(tag, 'list', 'id=="param"') @>) 262 | <@ end) @> 263 | 264 | <@ define('article', function (tag) @> 265 |
    266 | <@ if tag.type or tag.typename then @> 267 | > 268 | <@ defer(tag, 'link') @> 269 | > 270 | <@ end @> 271 |
    272 | <@ if tag.note then @>

    <@= tag.note @>

    <@ end @> 273 | <@ if tag.code then @> 274 |
    <@= tag.info @>
    275 | <@ else @> 276 |

    <@= tag.info @>

    277 | <@ end @> 278 | <@ descend(tag, 'main') @> 279 |
    280 |
    281 | <@ end) @> 282 | 283 | <@ define('main', 'not hidden', function (tag) @> 284 | <@ defer(tag, 'article') @> 285 | <@ end) @> 286 | 287 | <@ define('main', '(prev and prev.id) ~= id and not hidden', function (tag) @> 288 |
    289 | > 290 | <@= tag.title or tag.id @> 291 | > 292 | <@ defer(tag, 'article') @> 293 |
    294 | <@ end) @> 295 | 296 | <@ define('main', 'level == 1 and not hidden', function (tag) @> 297 |
    298 | <@ defer(tag, 'article') @> 299 |
    300 | <@ end) @> 301 | 302 | <@ if self.overview then @> 303 |
    304 |

    <@= self.title @>

    305 | 308 |
    309 | <@ end @> 310 | 311 | <@ descend(tags, 'main') @> 312 | 313 | 317 | 318 | 319 | 320 | 333 | 334 | 335 | ]] 336 | 337 | --- @function rtfm.launch Launch the generator from the command line. 338 | --- @param string ... Arguments passed in from command line. 339 | function rtfm.launch (...) 340 | -- Try to load config file 341 | local option = {} 342 | local env = setmetatable({}, { __index = function (self, index) 343 | return option.at[index] or _G[index] 344 | end }) 345 | local configure = loadfile('.rtfm.lua', 't', env) 346 | if not configure then 347 | configure = function () end 348 | end 349 | if setfenv then 350 | setfenv(configure, env) 351 | end 352 | -- Parse command line args 353 | local source = 'local o, c = ... return function (t) o.at = t; c()\n' 354 | local argIndex = 1 355 | for i = 1, select('#', ...) do 356 | local option = select(i, ...) 357 | local s, e, k, v = option:find('^%-%-(.*)=(.*)') 358 | if not s then 359 | break 360 | end 361 | v = (v == 'true' or v == 'false' or v == 'nil') and v 362 | or tonumber(v) or ('%q'):format(v) 363 | source = source .. 't.' .. k .. '=' .. tostring(v) .. '\n' 364 | argIndex = i + 1 365 | end 366 | source = source .. 'end' 367 | -- Create and run a generator 368 | local generator = rtfm.Generator(loadstring(source)(option, configure)) 369 | generator:run({ select(argIndex, ...) }) 370 | end 371 | 372 | 373 | --- @function rtfm.nestDocTag Default nesting function for regular tags. 374 | function rtfm.nestDocTag (tag, levels, tags) 375 | -- put tag at top of level stack, fill holes, pop unrelated tags 376 | levels[tag.level] = tag 377 | for i = 1, tag.level - 1 do 378 | levels[i] = levels[i] or false 379 | end 380 | while #levels > tag.level do 381 | levels[#levels] = nil 382 | end 383 | -- move the tag into the appropriate parent tag (next level down stack) 384 | local parent 385 | local level = tag.level - 1 386 | while level > 0 and not parent do 387 | parent = levels[level] 388 | level = level - 1 389 | end 390 | if parent then 391 | tag.parent = parent 392 | parent[#parent + 1] = tag 393 | return true 394 | end 395 | end 396 | 397 | --- @function rtfm.nestEndTag Default nesting function for end tags. 398 | function rtfm.nestEndTag (tag, levels, tags) 399 | for i = #levels, 1, -1 do 400 | if levels[i] and levels[i].id == tag.what then 401 | for j = i, #levels do 402 | levels[j] = nil 403 | end 404 | return true 405 | end 406 | end 407 | io.stderr:write('\nMismatched "end" tag on line ' .. tag.line .. '\n') 408 | return true 409 | end 410 | 411 | --- @function rtfm.nestMergedTag Default nesting function for merged tags. 412 | function rtfm.nestMergedTag (tag, levels, tags) 413 | for _, other in ipairs(tags.flat) do 414 | if other[tag.merge] == tag[tag.merge] 415 | and other.id == tag.id then 416 | isMerged = other ~= tag 417 | tag = other 418 | break 419 | end 420 | end 421 | return rtfm.nestDocTag(tag, levels, tags) or isMerged 422 | end 423 | 424 | --[[-- 425 | @class Generator 426 | 427 | Documentation generator. 428 | 429 | Configuration files run with the Generator as their environment. 430 | The first segment of any configuration option, such as `input` or 431 | `template`, represents a Generator field. 432 | 433 | The Generator is also responsible for organizing tags into a tree 434 | structure after their extraction from input files. 435 | --]]-- 436 | 437 | --- @method Generator:nestTags Nest tags. 438 | local function nestTags (self, tags) 439 | local levels = {} 440 | local i = 0 441 | while i < #tags do 442 | i = i + 1 443 | if tags[i]:nest(levels, tags) then 444 | table.remove(tags, i) 445 | i = i - 1 446 | end 447 | end 448 | end 449 | 450 | --- @function Generator.sortFunc Function passed to `table.sort`. 451 | local function sortFunc (a, b) 452 | if a.level ~= b.level then 453 | return a.level > b.level 454 | end 455 | if a.group and b.group then 456 | if a.group ~= b.group then 457 | return a.group < b.group 458 | end 459 | if a.sort and a.sort == b.sort then 460 | for sort in a.sort:gmatch('(%a+)') do 461 | if a[sort] < b[sort] then 462 | return true 463 | elseif a[sort] > b[sort] then 464 | return false 465 | end 466 | end 467 | end 468 | end 469 | return a.index < b.index 470 | end 471 | 472 | --- @method Generator:sortTags Sort nested tags and link them to siblings. 473 | local function sortTags (self, tags) 474 | table.sort(tags, self.sortFunc) 475 | for i, tag in ipairs(tags) do 476 | tag.prev = tags[i - 1] 477 | tag.next = tags[i + 1] 478 | self:sortTags(tag) 479 | end 480 | end 481 | 482 | --- @method Generator:run Run the generator on a list of files. 483 | --- @param {number:string} files A table of source files to parse. 484 | local function run (self, files) 485 | local tags = self.input:read(files) 486 | self:nestTags(tags) 487 | self:sortTags(tags) 488 | self.output:write(self.template:apply(tags)) 489 | end 490 | 491 | --- @constructor rtfm.Generator Creates a Generator instance. 492 | --- @param ConfigCallback configure An optional configuration callback. 493 | function rtfm.Generator (configure) 494 | local generator = {} 495 | 496 | --- @field Template template The template for generated output. 497 | generator.template = rtfm.Template(generator) 498 | 499 | --- @field Reader input The source file reader. 500 | generator.input = rtfm.Reader(generator) 501 | 502 | --- @field Writer output The documentation writer. 503 | generator.output = rtfm.Writer(generator) 504 | 505 | --- @field {string:TagDef} tag Tag definitions, keyed by ID. 506 | generator.tag = CREATE_DEFAULT_TAGDEFS() 507 | 508 | generator.sortFunc = sortFunc 509 | generator.nestTags = nestTags 510 | generator.sortTags = sortTags 511 | generator.run = run 512 | 513 | if configure then 514 | configure(generator) 515 | end 516 | for _, tag in pairs(generator.tag) do 517 | if tag.alias then 518 | setmetatable(tag, { __index = generator.tag[tag.alias] }) 519 | end 520 | end 521 | 522 | return generator 523 | end 524 | 525 | --- @class NodeSet Internal template transformation helper. 526 | 527 | --- @method NodeSet:test Test a node to see if it meets a condition. 528 | --- @param table node The node to test. 529 | --- @param string condition The condition to check. 530 | --- @return mixed Returns a truthy value if the test passed. 531 | local function test (self, node, condition) 532 | local env = setmetatable({}, { __index = node }) 533 | local f = assert((loadstring or load)( 534 | 'local self = ... return ' .. condition, nil, 't', env)) 535 | return (setfenv and setfenv(f, env) or f)(node) 536 | end 537 | 538 | --- @method NodeSet:match Test a node to see if it meets a condition. 539 | --- @param string condition The condition to check. 540 | --- @param boolean descend Whether to descend into child nodes. 541 | --- @return NodeSet Returns a new NodeSet containing matched nodes. 542 | local function match (self, condition, descend) 543 | local ns = rtfm.NodeSet() 544 | condition = condition or 'true' 545 | for _, node in ipairs(self) do 546 | if descend then 547 | for _, child in ipairs(node) do 548 | if self:test(child, condition) then 549 | ns[#ns + 1] = child 550 | end 551 | end 552 | elseif self:test(node, condition) then 553 | ns[#ns + 1] = node 554 | end 555 | end 556 | return ns 557 | end 558 | 559 | --- @constructor rtfm.NodeSet Creates a NodeSet instance. 560 | --- @param table ... A list of nodes in the set. 561 | function rtfm.NodeSet (...) 562 | return { test = test, match = match, ... } 563 | end 564 | 565 | --- @class Template Transforms tag data to desired output format. 566 | 567 | --- @method Template:applyText Apply the template. 568 | --- @param string text The full text of the template. 569 | --- @param {number:TagDef} tags List of tags to apply the template to. 570 | local function applyText (self, text, tags) 571 | local open = '\nwrite[============[\n' 572 | local close = ']============]\n' 573 | if self.condense then 574 | local s, e, left, right = self.escapePattern:find('(.*)%(.*%)(.*)') 575 | text = text:gsub('[%s]*' .. left, left):gsub(right .. '[%s]*', right) 576 | end 577 | local source = 'local self, tags, write, define, defer, descend = ... ' 578 | .. open .. text 579 | :gsub(self.outputPattern, close .. 'write(%1\n)' .. open) 580 | :gsub(self.escapePattern, close .. '%1' .. open) 581 | .. close 582 | local func, reason = loadstring(source) 583 | if func then 584 | local buffer = {} 585 | func( 586 | self, 587 | tags, 588 | function (text) buffer[#buffer + 1] = text end, 589 | function (...) return self:define(...) end, 590 | function (...) return self:delegate(false, ...) end, 591 | function (...) return self:delegate(true, ...) end) 592 | return table.concat(buffer) 593 | else 594 | return nil, reason 595 | end 596 | end 597 | 598 | --- @method Template:apply Apply the template to a list of tags. 599 | --- @param {number:TagDef} tags List of tags to apply the template to. 600 | --- @return string Returns the generated output. 601 | local function apply (self, tags) 602 | local text 603 | if self.path then 604 | local file = io.open(self.path) 605 | if file then 606 | text = file:read('*a') 607 | else 608 | io.stderr:write('\nTemplate file "' .. self.path 609 | .. '" not found.\nUsing built-in template.\n\n') 610 | end 611 | local result, reason = self:applyText(text, tags) 612 | if not result then 613 | io.stderr:write('\nError in template file.\n' .. reason .. '\n') 614 | text = nil 615 | end 616 | end 617 | if not text then 618 | return assert(self:applyText(self.text, tags)) 619 | end 620 | end 621 | 622 | --- @method Template:define Define a transformation rule. 623 | --- @param string mode Transformation mode. 624 | --- @param string selector Node selector. 625 | --- @param function transform Transformation callback function. 626 | local function define (self, mode, selector, transform) 627 | self.rules[#self.rules + 1] = { 628 | mode = mode, 629 | selector = transform and selector or 'true', 630 | transform = transform or selector 631 | } 632 | end 633 | 634 | --- @method Template:delegate Delegate to another transformation rule. 635 | --- @param string context Context node. 636 | --- @param string mode Transformation mode. 637 | --- @param string selector Node selector. 638 | local function delegate (self, descend, node, mode, selector) 639 | local context = rtfm.NodeSet(node):match(selector, descend) 640 | local map = {} 641 | for _, rule in ipairs(self.rules) do 642 | if rule.mode == mode then 643 | local matches = context:match(rule.selector, false) 644 | for _, node in ipairs(matches) do 645 | map[node] = rule 646 | end 647 | end 648 | end 649 | for index, node in ipairs(context) do 650 | if map[node] then 651 | map[node].transform(node, index, context) 652 | end 653 | end 654 | end 655 | 656 | --- @constructor rtfm.Template Creates a Template instance. 657 | function rtfm.Template () 658 | local template = {} 659 | --- @field string title Main title to display in generated output. 660 | template.title = 'API Docs' 661 | --- @field string path Path to a custom template. 662 | template.path = nil 663 | --- @field string text Full text of the output template. 664 | template.text = DEFAULT_TEMPLATE 665 | --- @field boolean overview Whether to display an API overview. 666 | template.overview = true 667 | --- @field string escapePattern Pattern to escape Lua code. 668 | template.escapePattern = '<@(.-)@>' 669 | --- @field string outputPattern Pattern to output results of expressions. 670 | template.outputPattern = '<@=(.-)@>' 671 | --- @field boolean condense Eliminate whitespace around escape sequences. 672 | --- Whitespace may be explicitly written with `write` or `outputPattern`. 673 | template.condense = true 674 | 675 | template.rules = {} 676 | template.apply = apply 677 | template.applyText = applyText 678 | template.define = define 679 | template.delegate = delegate 680 | 681 | return template 682 | end 683 | 684 | --- @class Reader Parses the source files. 685 | 686 | --- @method Reader:parseLine Parse a line from a source file. 687 | --- @param string line Line of text to parse. 688 | --- @param number lineNumber Line number. 689 | local function parseLine (self, line, lineNumber) 690 | local tags = self.tags 691 | local lastTag = tags[#tags] 692 | local column, _, id, data = line:find(self.sigil .. '([^%s]+)%s*(.*)') 693 | -- is this line a new tag? 694 | if id then 695 | if lastTag then 696 | lastTag.info = lastTag.info:gsub('\n*$', '') 697 | end 698 | local tag = setmetatable({}, { 699 | __index = self.generator.tag[id] or self.generator.tag.unknown 700 | }) 701 | local m = { data:find(tag.pattern) } 702 | local i = 2 703 | for field in tag.fields:gmatch('[^,]+') do 704 | i = i + 1 705 | tag[field] = m[i] 706 | end 707 | tags[#tags + 1] = tag 708 | tags.flat[#tags.flat + 1] = tag 709 | tag.index = #tags 710 | tag.line = lineNumber 711 | tag.column = column 712 | tag.data = data 713 | tag.id = id 714 | tag.info = tag.info or '' 715 | return tag 716 | -- it's more info for the previous tag 717 | elseif lastTag then 718 | local left = line:sub(1, lastTag.column - 1) 719 | local right = line:sub(lastTag.column, -1) 720 | line = left:gsub('^[%s-]*', '') .. right 721 | if lastTag.info == '' then 722 | lastTag.info = line 723 | else 724 | lastTag.info = lastTag.info .. '\n' .. line 725 | end 726 | end 727 | end 728 | 729 | --- @method Reader:parseFile Read a file and create tags from it. 730 | --- @param string name Name of file to parse. 731 | local function parseFile (self, name) 732 | local file = io.open(name) 733 | local inBlock = false 734 | local n = 0 735 | for line in file:lines() do 736 | n = n + 1 737 | if line:find(self.blockEndPattern) then -- found end of block 738 | inBlock = false 739 | end 740 | if inBlock or line:find(self.linePattern) then -- in block or line 741 | self:parseLine(line, n) 742 | end 743 | if line:find(self.blockStartPattern) then -- found start of block 744 | inBlock = true 745 | end 746 | end 747 | file:close() 748 | end 749 | 750 | --- @method Reader:read Read some files return a list of tags. 751 | --- @param {number:string} files List of files to parse. 752 | --- @return {number:TagDef} Returns a list of extracted tags. 753 | local function read (self, files) 754 | for _, name in ipairs(files) do 755 | self:parseFile(name) 756 | end 757 | return self.tags 758 | end 759 | 760 | --- @constructor rtfm.Reader Creates a Reader instance. 761 | --- @param Generator generator The generator instance. 762 | function rtfm.Reader (generator) 763 | local reader = {} 764 | 765 | reader.generator = generator 766 | reader.tags = { flat = {} } 767 | 768 | --- @field string sigil The prefix character for tags; "@" by default. 769 | reader.sigil = '@' 770 | --- @field string blockStartPattern Matches the start of a docblock. 771 | reader.blockStartPattern = '%-%-%[=*%[%-%-+' 772 | --- @field string blockEndPattern Matches the end of a docblock. 773 | reader.blockEndPattern = '%-%-+%]=*%]' 774 | --- @field string linePattern Matches a line with a docblock. 775 | reader.linePattern = '%-%-%-' 776 | 777 | reader.parseFile = parseFile 778 | reader.parseLine = parseLine 779 | reader.read = read 780 | 781 | return reader 782 | end 783 | 784 | --- @class Writer Outputs the generated text. 785 | 786 | --- @method Writer:write Write to a file or stdout. 787 | --- @param string text Text (or binary) to write. 788 | local function write (self, text) 789 | if self.path then 790 | local file = io.open(self.path, 'wb') 791 | file:write(text) 792 | else 793 | io.write(text) 794 | end 795 | end 796 | 797 | --- @constructor rtfm.Writer Creates a Writer instance. 798 | function rtfm.Writer () 799 | local writer = {} 800 | --- @field string path Path to output file. Uses stdout if omitted. 801 | writer.path = nil 802 | 803 | writer.write = write 804 | 805 | return writer 806 | end 807 | 808 | -- If running from the command line, launch the generator. 809 | if arg and arg[0] and arg[0]:find('rtfm.lua$') then 810 | rtfm.launch(...) 811 | end 812 | 813 | return rtfm 814 | --------------------------------------------------------------------------------