├── 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 | {@ descend(tag, 'overview') @}
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 |
Table of Contents
152 | {@ descend(tags, 'overview') @}
153 |
154 |
155 | {@ end @}
156 |
157 | {@ descend(tags, 'main') @}
158 |
159 |
160 |
161 | Documentation generated by RTFM .
162 | {@= os.date("%c") @}
163 |
164 |
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 |
Table of Contents
13 |
53 |
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 | ```
number gain Amplification amount in decibels.
67 |
70 | copy (sound, copyOverData)Makes a copy of a SoundData.
71 |
72 | **Example**
73 | ```lua
74 | copy = sone.copy(sound)
75 | ```
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 |
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 | ```
number seconds How long the fade will take.
93 | (optional) Which fade curve to use. Default is linear.
94 |
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 | ```
number seconds How long the fade will take.
106 | (optional) Which fade curve to use. Default is linear.
107 |
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 | ```
number seconds How long the fade will take.
122 | (optional) Which fade curve to use. Default is linear.
123 |
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 | ```
138 |
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 | ```
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 |
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 | **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 |
259 |
260 | Documentation generated by RTFM .
261 | Mon Oct 3 00:40:03 2016
262 |
263 |
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 | <@ descend(tag, 'overview') @>
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 |
Table of Contents
306 | <@ descend(tags, 'overview') @>
307 |
308 |
309 | <@ end @>
310 |
311 | <@ descend(tags, 'main') @>
312 |
313 |
314 | Documentation generated by
315 | RTFM .
316 |
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 |
--------------------------------------------------------------------------------