├── .gitignore ├── COPYING.txt ├── Cargo.lock ├── Cargo.toml ├── README.md ├── how-it-works.md ├── inputs ├── a4 ├── eine_kleine_nachtmusik ├── für_elise ├── hungarian_dance_5 └── the_tempest └── src ├── args.rs ├── gui.rs ├── main.rs └── parser.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /COPYING.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "autocfg" 7 | version = "1.1.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 10 | 11 | [[package]] 12 | name = "bitflags" 13 | version = "1.3.2" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 16 | 17 | [[package]] 18 | name = "cfg-if" 19 | version = "1.0.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 22 | 23 | [[package]] 24 | name = "getrandom" 25 | version = "0.2.7" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" 28 | dependencies = [ 29 | "cfg-if", 30 | "libc", 31 | "wasi", 32 | ] 33 | 34 | [[package]] 35 | name = "lazy_static" 36 | version = "1.4.0" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 39 | 40 | [[package]] 41 | name = "libc" 42 | version = "0.2.126" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" 45 | 46 | [[package]] 47 | name = "libm" 48 | version = "0.2.3" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "da83a57f3f5ba3680950aa3cbc806fc297bc0b289d42e8942ed528ace71b8145" 51 | 52 | [[package]] 53 | name = "num-traits" 54 | version = "0.2.15" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 57 | dependencies = [ 58 | "autocfg", 59 | "libm", 60 | ] 61 | 62 | [[package]] 63 | name = "ppv-lite86" 64 | version = "0.2.16" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" 67 | 68 | [[package]] 69 | name = "rand" 70 | version = "0.8.5" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 73 | dependencies = [ 74 | "libc", 75 | "rand_chacha", 76 | "rand_core", 77 | ] 78 | 79 | [[package]] 80 | name = "rand_chacha" 81 | version = "0.3.1" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 84 | dependencies = [ 85 | "ppv-lite86", 86 | "rand_core", 87 | ] 88 | 89 | [[package]] 90 | name = "rand_core" 91 | version = "0.6.3" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 94 | dependencies = [ 95 | "getrandom", 96 | ] 97 | 98 | [[package]] 99 | name = "rand_distr" 100 | version = "0.4.3" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" 103 | dependencies = [ 104 | "num-traits", 105 | "rand", 106 | ] 107 | 108 | [[package]] 109 | name = "sdl2" 110 | version = "0.35.2" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "f7959277b623f1fb9e04aea73686c3ca52f01b2145f8ea16f4ff30d8b7623b1a" 113 | dependencies = [ 114 | "bitflags", 115 | "lazy_static", 116 | "libc", 117 | "sdl2-sys", 118 | ] 119 | 120 | [[package]] 121 | name = "sdl2-sys" 122 | version = "0.35.2" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "e3586be2cf6c0a8099a79a12b4084357aa9b3e0b0d7980e3b67aaf7a9d55f9f0" 125 | dependencies = [ 126 | "cfg-if", 127 | "libc", 128 | "version-compare", 129 | ] 130 | 131 | [[package]] 132 | name = "tempest-lcd" 133 | version = "0.1.0" 134 | dependencies = [ 135 | "rand", 136 | "rand_distr", 137 | "sdl2", 138 | ] 139 | 140 | [[package]] 141 | name = "version-compare" 142 | version = "0.1.0" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73" 145 | 146 | [[package]] 147 | name = "wasi" 148 | version = "0.11.0+wasi-snapshot-preview1" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 151 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tempest-lcd" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | sdl2 = "0.35" 8 | rand = "0.8" 9 | rand_distr = "0.4" 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | https://user-images.githubusercontent.com/83558186/183791965-dbcf0ff7-1fc1-4fc6-8043-4a30dbe81e75.mp4 2 | 3 | ## What is this? 4 | 5 | Some time ago, an amazing project named [Tempest for Eliza][1] was created 6 | to show that old CRT monitors leaked electromagnetic radiation, 7 | some of which was in the usual radio wave frequency, and that, 8 | by displaying specific images on-screen, it's actually possible to manipulate 9 | the electromagnetic waves' shape so that, if a radio tunes to a certain 10 | frequency, it'll play a song. 11 | 12 | Nowadays, CRT monitors are a rare sight: everyone uses LCD monitors instead. 13 | However, as it turns out, it is also possible to do the exact same thing 14 | with LCD monitors, albeit rendering slightly different images. 15 | This program is essentially meant to achieve the same as Tempest for Eliza, 16 | but for LCD monitors, hence the name __Tempest LCD__. 17 | 18 | ## Prerequisites 19 | 20 | First and foremost, you'll need a radio that can also handle AM waves 21 | (as opposed to FM only). 22 | 23 | Additionally, the key information you'll need to run this program is 24 | your monitor's __horizontal refresh rate__, which is the amount of pixel rows 25 | it is currently rendering per second. 26 | You can obtain this value by simply multiplying the height of the 27 | current resolution by the number of frames per second (refresh rate) currently 28 | in use. 29 | On Linux X11, one can obtain this information with the command `xrandr`, 30 | which marks the current mode with a asterisk. On my computer, this gives: 31 | 32 | ``` 33 | 1366x768 60.06*+ 34 | ``` 35 | 36 | To the left is the resolution (`width x height`), and to the right is the 37 | number of frames per second currently in use. Therefore, 38 | the horizontal refresh rate of the current mode in my monitor is 39 | `60.06 * 768 ~ 46126`. 40 | 41 | **Note**: monitors usually support not only several resolutions, but often 42 | several refresh rates as well. Make sure to take the currently used ones. 43 | 44 | ## Running 45 | 46 | [Make sure you have cargo installed][2], then run 47 | 48 | ```bash 49 | cargo run -- HORIZONTAL_REFRESH_RATE FILENAME 50 | ``` 51 | 52 | Where `FILENAME` is any file under the `inputs/` folder, 53 | and `HORIZONTAL_REFRESH_RATE` is the value explained above. 54 | 55 | On the first run, this command will download all dependencies needed to run this 56 | program. Note, however, that you will also need [SDL2][3] installed. 57 | Consult your Linux distro's documentation for how to properly install it. 58 | 59 | Then, ensure your radio is on AM mode, and slowly change the frequency it's 60 | tuned to until you hear the sound. You might have to run the command a few times 61 | until finding the right frequency. 62 | 63 | You may optionally pass the string `cosine` as a third command line argument 64 | (after FILENAME), which causes the program to use sinusoidal waves instead of 65 | square waves. [See how-it-works.md](how-it-works.md). 66 | 67 | ## Who on Earth had this idea? (Or: a brief history of this project) 68 | 69 | It all started with [this paper][7], where the authors describe a way to make 70 | (CRT) monitors broadcast an AM signal. 71 | 72 | From it, a GPL2 project named [Tempest AM][8] was implemented, closely following 73 | the ideas presented on the paper, and using an X11 graphics library named G2, 74 | that I've frankly never heard of. (Maybe it was popular back then?) 75 | 76 | Then, another developer proceeded to greatly simplify the signal drawing 77 | function, using SDL (version 1) as the graphics library. 78 | That project was [Tempest for Eliza][1], which was previously mentioned 79 | as the predecessor to this project. 80 | 81 | Finally, my role on this was to change Tempest for Eliza so it could work on 82 | LCD monitors instead of CRT ones. Meanwhile, I've also rewritten the code from 83 | scratch (it wasn't a large codebase, and still isn't) to use SDL2, and to have, 84 | hopefully, a clearer input file format. 85 | 86 | ## How does that even work?? 87 | 88 | The explanation is rather long: [see how-it-works.md](how-it-works.md) 89 | 90 | ## Input files 91 | 92 | What this program needs as input is essentially a list of notes and rests, 93 | along with their respective durations. 94 | Therefore, ideally, this project would be able to take Standard MIDI files 95 | (SMF) as input, since that is probably the most widespread media format that 96 | conveys that sort of information. 97 | However, parsing MIDI files is by itself a fairly complex task; on top of that, 98 | MIDI files allow any number of notes to be playing simultaneously, which is not 99 | straightforward to implement with this setup. 100 | Therefore, in the end, we've had to settle for a custom text format conveying 101 | note information, like our predecessor, Tempest for Eliza, also used. 102 | (However, hopefully, this format is more readable than the one that project 103 | used...) 104 | 105 | The input file format is as follows. 106 | 107 | The file must have a number in its first line, and nothing else: 108 | that number is interpreted as the BPM (beats per minute) of the song. 109 | Currently, there's no way to make this value change throughout the song. 110 | 111 | On the subsequent lines, there can be any number of notes, separated from 112 | each other by at least one space, with the following case insensitive fields, 113 | from left to right: 114 | 115 | - Note name (mandatory): Either `R`, indicating a rest, or `A` through `G`, 116 | according to the English note naming convention. 117 | Non-rest notes can be followed by a `#` (sharp, i.e. raises 1 semitone). 118 | There's no way to indicate flats, so one must use sharps instead, 119 | e.g. A sharp instead of B flat. 120 | 121 | - Octave digit (mandatory for non-rest notes): 122 | A single digit indicating the octave number the note belongs to. 123 | The central octave is `4`, and [`A4` is assigned 440Hz frequency][5], 124 | with other notes' frequencies being calculated with the number of semitones 125 | from `A4`, using [twelve-tone equal temperament][6]. 126 | 127 | - Note duration (optional): The duration of a note, taken as the first letter 128 | of the American English name of the [note value][4]. If this field is omitted, 129 | it defaults to quarter. Currently, there's no way to specify values shorter 130 | than thirty-second. 131 | - W: whole 132 | - H: half 133 | - Q: quarter 134 | - E: eighth 135 | - S: sixteenth 136 | - T: thirty-second 137 | 138 | Therefore, `c3`, `a#4h` and `d5e` would all be valid notes under this notation. 139 | 140 | It should be noted that, if the same note is played consecutively two or more 141 | times, it will actually sound as a single note with their added duration. 142 | That is, `e3 e3` will not sound as two quarter notes, but as a single half one 143 | (and therefore equivalent to `e3h`). This happens because notes never "fade out" 144 | after being played. This fact can be exploited to achieve ties and dotted notes. 145 | However, if one does want the same notes to sound as individual notes, 146 | the only way around this is placing a small rest between the notes. 147 | 148 | [1]: http://www.erikyyy.de/tempest 149 | [2]: https://doc.rust-lang.org/cargo/getting-started/installation.html 150 | [3]: https://www.libsdl.org/index.php 151 | [4]: https://en.wikipedia.org/wiki/Note_value 152 | [5]: https://en.wikipedia.org/wiki/A440_(pitch_standard) 153 | [6]: https://en.wikipedia.org/wiki/12_equal_temperament 154 | [7]: https://www.cl.cam.ac.uk/~mgk25/ih98-tempest.pdf 155 | [8]: https://github.com/priikone/tempest-AM 156 | -------------------------------------------------------------------------------- /how-it-works.md: -------------------------------------------------------------------------------- 1 | # That's cool and all, but how does that even work?? 2 | 3 | ## The overview 4 | 5 | There are two hardware components behind this phenomenon: an LCD monitor, 6 | and a radio. 7 | 8 | In normal circumstances, a radio only works because there's a station somewhere 9 | broadcasting the waves that it will turn into audio. 10 | However, it just happens that a monitor -- a hardware part meant to emit several 11 | frequencies of visible light -- also emits several frequencies of radio waves. 12 | This isn't so surprising if you consider that visible light and radio waves are 13 | all electromagnetic waves, after all. 14 | 15 | Further, not only does the monitor emit radio waves, we can also control 16 | (to some extent) the shape of those waves, through the image we make the monitor 17 | render. Using a specific image allows us to broadcast an AM wave that will make 18 | the radio play a music note: therefore, with a succession of images, we can play 19 | a song. 20 | 21 | Of course, this overview is too shallow, and leaves out the most important 22 | questions: 23 | - What does an AM wave that makes a radio play a note look like? 24 | - Why does the monitor emit specific waves when certain images are being 25 | displayed? 26 | - How can a program know what image corresponds to a given note? 27 | 28 | We'll answer that in the following sections. 29 | 30 | ## Part 1: waves, radios, and modulation 31 | 32 | In the end of the day, a radio is meant to turn electromagnetic waves into 33 | sound. But how does it do that? Does a radio simply maintain everything about 34 | the wave it receives -- amplitudes, frequencies, phases -- and just turns it 35 | into audio? 36 | 37 | The answer is no. You probably know that to use a radio, we must set it to 38 | "listen" to a given frequency. This frequency never changes, even though the 39 | sound waves the radio plays, often conveying speech or songs, constantly 40 | change their frequency. Plus, if every emitter simply decided to turn sound 41 | into EM waves while maintaining frequency, and they decided to emit them all 42 | at the same time, the waves would certainly interfere with each other. 43 | 44 | What does happen is there are two waves involved in the process. One of them is 45 | the **message signal**: the wave we expect the receiving end will hear. 46 | But, in order to transmit the message, we use another wave, called **carrier**, 47 | whose frequency always falls into a fixed narrow range. This way, as long as 48 | each station is given non-overlapping ranges and actually stay in their own 49 | range, the signals will not interfere with each other, and as long as ranges 50 | don't change often, listeners will know which frequency to tune into in order 51 | to hear which station. 52 | 53 | The question that remains is how to "insert" the message into the carrier. The 54 | emitter must do this in such a way that is reversible, so that the receiving 55 | radio must be able to separate the carrier from the message, and convert only 56 | the latter into sound. The process of "inserting" a wave into another wave 57 | so that the former can be retrieved later is called **modulation**. Generally, 58 | this process involves making some property of the carrier change according to 59 | the values of the signal at any given time. 60 | As it turns out, there are multiple ways to do that, but the 61 | most commonly used are Amplitude Modulation (AM) and Frequency Modulation (FM). 62 | 63 | ### Amplitude Modulation 64 | 65 | Since this project uses AM, let's take a closer look at it. 66 | If the message is $m(t)$ and the carrier $c(t)$ is a sinusoidal wave 67 | of amplitude $A > 0$ and frequency $F$, that is 68 | 69 | $$c(t) = A \sin(2\pi Ft)$$ 70 | 71 | Then, the modulated signal is given by 72 | 73 | $$(A + m(t)) \sin(2\pi Ft)$$ 74 | 75 | Which means the amplitude of the signal is changed from $A$ to $A + m(t)$. 76 | Note that amplitude modulation only occurs properly if $-A \leq m(t) \leq A$ 77 | for all $t$; otherwise, a condition named overmodulation occurs, and the message 78 | cannot be retrieved correctly. 79 | 80 | Let's look at the special case where our message is a square wave of frequency 81 | $f$ and the same amplitude as the carrier: 82 | 83 | $$m(t) = \begin{cases} 84 | A & \text{if } \sin(2\pi ft) \geq 0 \\ 85 | -A & \text{otherwise} 86 | \end{cases}$$ 87 | 88 | This means the modulated signal, which we'll call $s(t)$, will be 89 | 90 | $$s(t) = \begin{cases} 91 | 2A \sin(2\pi Ft) & \text{if } \sin(2\pi ft) \geq 0 \\ 92 | 0 & \text{otherwise} 93 | \end{cases}$$ 94 | 95 | Which means the modulated signal will alternate every $1/f$ seconds between 96 | a simple sine wave and no signal at all. This is close to the actual signal 97 | we're making the monitor send with this program -- which leads us to the second 98 | part of this equation. 99 | 100 | ## Part 2: the LCD Monitor 101 | 102 | Now, let's take a look at the LCD monitor's role in this phenomenon. 103 | 104 | First and foremost, as [this page][2] will tell you, LCD monitors have a 105 | **grid** or matrix, which directly map to pixels when the monitor is being used 106 | in its preferred (aka native) resolution. 107 | From now on, we'll assume the resolution in use is always the native one. 108 | 109 | [This page][3] has an interesting overview of how LCD works, though you don't 110 | have to read it: I'm only using it as a source for the following 111 | (mostly unsurprising) claims: 112 | 113 | - While reflective LCD screens do exist, most (if not all) computer monitors 114 | have a built-in source of electromagnetic radiation. 115 | 116 | - Regardless of whether the LCD matrix is passive or active, white pixels emit 117 | the most intense electromagnetic waves, and black pixels, the least intense, 118 | with grayscale having proportionate intermediate intensity. 119 | However, due to light components being implemented as sub-pixels, their 120 | contribution to signal strength is not so straightforward. 121 | 122 | In addition to this, one of the authors of the paper that originated the 123 | Tempest AM project [also wrote a paper][4] on electromagnetic waves leaked by 124 | LCD monitors. 125 | The paper actually explains how to reconstruct text rendered by such monitors 126 | (and, to that end, quite sophisticated hardware is used). However, we can still 127 | learn a few things about LCD monitors from it: 128 | 129 | - "these technologies [LCD] update all pixels in a row simultaneously. This 130 | makes it impractical to separate the contribution of individual pixels in a 131 | row to the overall light emitted." (page 2) 132 | 133 | - "[LCD monitors] still have to continuously refresh the entire image content 134 | [...] This continuous refresh ensures that the signals on the video interface 135 | are periodic" (page 3) 136 | 137 | Armed with all this information, we can finally figure out what image we want 138 | to give our monitor: 139 | 140 | - Sub-pixels are complicated: stick to grayscale. 141 | 142 | - Changing colors mid-row is probably a waste of time, as it will do nothing 143 | but change the average intensity of that row, which we could do by picking 144 | a different grayscale color for the entire row anyway. 145 | 146 | As such, our image should be made of rows of the same grayscale color. 147 | 148 | In the specific case of wanting to send a square wave as a message, which means 149 | we'll want our monitor to send a signal like the $s(t)$ presented in the 150 | previous section, we want to alternate between maximum and minimum signal 151 | emission -- meaning the image we want should switch between white and black 152 | pixel rows. 153 | 154 | Also, recall that $s(t)$ switches between maximum and minimum levels with 155 | the same frequency $f$ of the message. To find the image we desire to render, 156 | we make the following assumptions: 157 | 158 | - Rows are rendered top-to-bottom. 159 | 160 | - The time taken to render any row is roughly the same. I don't see why any 161 | row would be different from any other in how much time it takes to render it. 162 | 163 | - If the monitor renders $N$ frames per second and each frame is $h$ pixels 164 | high, the monitor renders $N h$ rows per second. 165 | I'm honestly not quite sure of this, since I expected the finished frame to be 166 | "held" for a bit longer, but this assumption does seem to work in practice. 167 | 168 | The product $N h$ is the **horizontal refresh rate** passed as an argument to 169 | the program. Let's call $y$ the Y-coordinate of the current pixel row, 170 | measured in pixels and from the **top** of the screen 171 | (which, conveniently, is precisely what SDL2 does in its coordinate system). 172 | Then, by dividing $y$ by the horizontal refresh rate, we obtain 173 | an estimate of the time $t$ at which we arrived at this row since the frame 174 | started being rendered. Thus, we can finally calculate $\sin(2\pi ft)$ and check 175 | whether it is positive or not, to decide if we should paint the row white or 176 | black. (The actual code doesn't calculate any sine or cosine for this -- but you 177 | may as well read the function `draw_square_wave` and see by yourself!) 178 | 179 | ## Part 3: the carrier, and how we got away knowing almost nothing about it 180 | 181 | You might remember that in part 1, while calculating $s(t)$, we 182 | assumed our carrier was a sine wave. Is this the case here? I have no idea what 183 | sort of radio wave the monitor is emitting, but it certainly isn't a 184 | well-behaved sine wave. However, as it turns out, that doesn't really matter. 185 | We know that whatever this wave looks like, it must be periodic. 186 | Also, a good fellow named Fourier (which is one of those prodigious 187 | mathematicians whose names seem to pop up pretty much everywhere, like Euler, 188 | Laplace and Gauss), found out that almost any periodic function can be written 189 | as a (possibly infinite) sum of sines and cosines, which has been naturally 190 | named [Fourier series][5] after him. That is, for almost any periodic function 191 | $f$, 192 | 193 | $$ 194 | f(t) = \frac{a_0}{2} + \sum_{n=1}^{\infty} a_n \cos(n t) + 195 | \sum_{n=1}^{\infty} b_n \sin(n t) 196 | $$ 197 | 198 | For some set of coefficients $\{a_n\}$ and $\{b_n\}$. 199 | Each of these sines and cosines is called a **component** of the signal $f$. 200 | 201 | When a radio is tuned to a certain frequency $F$, it effectively "filters out" 202 | or "ignores" any component of the original signal that is not inside a narrow 203 | range centered on $F$. As such, we can completely ignore how the carrier looks 204 | like, and simply look at its components, which are always sinusoidal waves, 205 | and therefore our assumption of having a sinusoidal carrier pretty much always 206 | works. 207 | (Note that sinusoidal waves can be either sines or cosines: 208 | since $\cos(x) = \sin(x + \pi/2)$ for any $x$, the only difference between 209 | those waves is their phase, but phases are irrelevant in this context.) 210 | 211 | Now, you might remember that, in order to hear the sound produced here, 212 | one has to slowly change the frequency the radio is tuned to. The reason for 213 | that is we don't really know the values of the coefficients $a_n$ and $b_n$ 214 | of each component; more importantly, we do not know the frequency of the 215 | components that have a high enough coefficient that allows the radio to produce 216 | audible sound. 217 | Of course, we could try to obtain the raw signal using a special antenna, 218 | and proceed to make a Fourier analysis to find out the component with the 219 | maximum coefficient whose frequency is also in the range of our radio... 220 | Or, we could just change the frequency until we hear the sound. 221 | 222 | However, that begs the question: "what if the signal coming from my monitor 223 | doesn't have **any** component strong enough to produce sound in the frequency 224 | range that my radio can tune to?" Unfortunately, yes, that may happen. The only 225 | solution then is to either use a different radio, or use a different monitor. 226 | 227 | [2]: https://computer.howstuffworks.com/monitor6.htm 228 | [3]: https://electronics.howstuffworks.com/lcd.htm 229 | [4]: https://www.cl.cam.ac.uk/~mgk25/pet2004-fpd.pdf 230 | [5]: https://mathworld.wolfram.com/FourierSeries.html 231 | -------------------------------------------------------------------------------- /inputs/a4: -------------------------------------------------------------------------------- 1 | 10 2 | a4w 3 | -------------------------------------------------------------------------------- /inputs/eine_kleine_nachtmusik: -------------------------------------------------------------------------------- 1 | 120 2 | g5 rE d5E 3 | g5 rE d5E 4 | g5E d5E g5E b5E d6 5 | r 6 | c6 rE a5E 7 | c6 rE a5E 8 | c6E a5E f#5E a5E d5 9 | r 10 | g5E rE 11 | g5 g5E b5E a5E g5S rS 12 | g5E f#5S rS 13 | f#5 f#5E a5E c6E f#5S rS 14 | a5E g5S rS 15 | g5 g5E b5E a5E g5S rS 16 | g5E f#5S rS 17 | f#5 f#5E a5E c6E f#5S rS 18 | g5S rS g5S rS 19 | g5S f#5S e5S f#5S 20 | g5S rS g5S rS 21 | b5S a5S g5S a5S 22 | b5S rS b5S rS 23 | d6S c6S b5S c6S d6 24 | -------------------------------------------------------------------------------- /inputs/für_elise: -------------------------------------------------------------------------------- 1 | 160 2 | e5E d#5E e5E d#5E e5E b4E d5E c5E a4 3 | rE c4E e4E a4E b4 4 | rE e4E g#4e b4E c5 5 | rE e4E 6 | e5E d#5E e5E d#5E e5E b4E d5E c5E a4 7 | rE c4E e4E a4E b4 8 | rE e4E c5E b4E a4 9 | r 10 | e5E d#5E e5E d#5E e5E b4E d5E c5E a4 11 | rE c4E e4E a4E b4 12 | rE e4E g#4e b4E c5 13 | rE e4E 14 | e5E d#5E e5E d#5E e5E b4E d5E c5E a4 15 | rE c4E e4E a4E b4 16 | rE e4E c5E b4E a4 17 | -------------------------------------------------------------------------------- /inputs/hungarian_dance_5: -------------------------------------------------------------------------------- 1 | 130 2 | d4 d4E g4E 3 | a#4 a#4E g4E 4 | f#4 f#4E g4S a4S g4H 5 | d#4 d#4E f4S g4S d4H 6 | c4S a#3S a#3S a3S rS a3E d4S g3H 7 | d4 d4E g4S a#4S 8 | d5 d5E a#4E 9 | a4 a4E a#4S c5S a#4H 10 | d#5S f5S a5S d#5S 11 | d5S d#5S f5S d5S 12 | c5S d5S d#5S c5S 13 | a#4S c5S d5S a#4S 14 | c5S a#4S a#4S a4S 15 | rS a4E d5S g4 g5 16 | rE 17 | d5 d5E g5E 18 | a#5 a#5E g5E 19 | f#5 f#5E g5S a5S g5H 20 | d#5 d#5E f5S g5S d5H 21 | c5S a#4S a#4S a4S rS a4E d5S g4H 22 | d4E d4S g4S a#4S d5S g5S a#5S 23 | d6 d6E a#5E 24 | a5 a5E a#5S c6S a#5H 25 | d#6S f6S g6S d#6S 26 | d6S d#6S f6S d6S 27 | c6S d6S d#6S c6S 28 | a#5S c6S d6S a#5S 29 | c6S a#5S a#5S a5S 30 | rS a5E d6S g5 g6 31 | -------------------------------------------------------------------------------- /inputs/the_tempest: -------------------------------------------------------------------------------- 1 | 110 2 | a4S f5S e5S d5E rS 3 | a4S f5S e5S d5E rS 4 | a4S f5S e5S d5E rS 5 | a4S f5S d5S e5E rS 6 | 7 | a4S g5S f5S e5E rS 8 | a4S g5S f5S e5E rS 9 | a4S g5S f5S e5E rS 10 | a4S g5S e5S f5E rS 11 | 12 | d5S a5S d6S a#5E rS 13 | a#4S d5S a#5S g5E rS 14 | g4S a#4S g5S e5E rS 15 | e4S a4S e5S f5E rS 16 | f4S a4S f5S d5E rS 17 | d4S a4S d5S c#5E rS 18 | c#4S a4S c#5S d5E rS 19 | 20 | f4S a4S d5S f5S a5S f5S d6S a5S a#5E rS 21 | a#4S d5S a#5S g5E rS 22 | g4s a#4S g5S e5E rS 23 | e4S a4S e5S f5E rS 24 | f4S a4S f5S d5E rS 25 | 26 | d4S a4S d5S c#5E rS 27 | c#4S a4S c#5S d5S 28 | rT a4T a5 g#5S g5S f#5S f5S e5S d#5S d5E rS 29 | d4S a4S d5S c#5E rS 30 | c#4S a4S c#5S d5S 31 | rT d5T d6 c#6S c6S b5S a#5S a5S g#5S a5E rS 32 | d4S a4S d5S c#5E rS 33 | c#5S a5S c#6S d6S 34 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 Luana Martins Barbosa 2 | // 3 | // This file is part of tempest-lcd. 4 | // tempest-lcd is free software, released under the 5 | // GNU Public License, version 2 only. 6 | // See COPYING.txt. 7 | 8 | use std::env; 9 | 10 | pub struct ArgData { 11 | pub horiz_refresh_rate: f64, 12 | pub filename: String, 13 | pub wave_is_cosine: bool, 14 | } 15 | 16 | pub fn parse_args() -> ArgData { 17 | let mut args = env::args().skip(1); 18 | let horiz_refresh_rate = match args.next() { 19 | None => { 20 | usage(); 21 | panic!("missing horizontal refresh rate"); 22 | }, 23 | Some(arg1) => { 24 | let freq = arg1.parse::() 25 | .unwrap_or_else(|e| panic!("1st argument is not a valid f64: {}", e)); 26 | freq 27 | }, 28 | }; 29 | let filename = match args.next() { 30 | None => { 31 | usage(); 32 | panic!("missing file name"); 33 | }, 34 | Some(arg2) => arg2, 35 | }; 36 | let wave_is_cosine = match args.next() { 37 | Some(s) => &s == "cosine", 38 | _ => false, 39 | }; 40 | ArgData { 41 | horiz_refresh_rate, 42 | filename, 43 | wave_is_cosine, 44 | } 45 | } 46 | 47 | fn usage() { 48 | println!("usage: [cosine]"); 49 | } 50 | -------------------------------------------------------------------------------- /src/gui.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 Luana Martins Barbosa 2 | // 3 | // This file is part of tempest-lcd. 4 | // tempest-lcd is free software, released under the 5 | // GNU Public License, version 2 only. 6 | // See COPYING.txt. 7 | 8 | use std::{ 9 | f64::consts, 10 | time::{Duration, Instant}, 11 | thread, 12 | }; 13 | use rand::Rng; 14 | use rand_distr::StandardNormal; 15 | use sdl2::{ 16 | EventPump, 17 | Sdl, 18 | VideoSubsystem, 19 | event::{Event, WindowEvent}, 20 | keyboard::Keycode, 21 | pixels::Color, 22 | rect::Point, 23 | render::WindowCanvas, 24 | }; 25 | use crate::parser::Note; 26 | 27 | const SLEEP_INTERVAL: Duration = Duration::from_millis(5); 28 | 29 | pub struct Gui { 30 | // note: these two are never used directly, but must be held here to ensure 31 | // they're not dropped until after the GUI stopped. 32 | _sdl_context: Sdl, 33 | _video_subsys: VideoSubsystem, 34 | canvas: WindowCanvas, 35 | event_pump: EventPump, 36 | horiz_refresh_rate: f64, 37 | res_x: i32, 38 | res_y: i32, 39 | running: bool, 40 | paused: bool, 41 | wave_is_cosine: bool, 42 | } 43 | 44 | impl Gui { 45 | pub fn create(horiz_refresh_rate: f64, wave_is_cosine: bool) -> Self { 46 | let sdl_context = sdl2::init() 47 | .unwrap_or_else(|e| panic!("failed to initialize SDL2: {}", e)); 48 | let video_subsys = sdl_context.video() 49 | .unwrap_or_else(|e| panic!("failed to initialize video subsystem: {}", e)); 50 | 51 | let mut window_builder = video_subsys.window("tempest LCD", 0, 0); 52 | window_builder.fullscreen_desktop(); 53 | window_builder.borderless(); 54 | 55 | let window = window_builder.build() 56 | .unwrap_or_else(|e| panic!("failed to create window: {}", e)); 57 | let (res_x_uint, res_y_uint) = window.size(); 58 | 59 | let res_x: i32 = res_x_uint.try_into() 60 | .unwrap_or_else(|e| panic!("failed to convert X resolution {} to i32: {}", res_x_uint, e)); 61 | let res_y: i32 = res_y_uint.try_into() 62 | .unwrap_or_else(|e| panic!("failed to convert Y resolution {} to i32: {}", res_y_uint, e)); 63 | 64 | let mut canvas = window.into_canvas() 65 | .build() 66 | .unwrap_or_else(|e| panic!("failed to make renderer from window: {}", e)); 67 | 68 | let event_pump = sdl_context.event_pump() 69 | .unwrap_or_else(|e| panic!("failed to get event pump: {}", e)); 70 | 71 | clear_and_present(&mut canvas, Color::GRAY); 72 | 73 | Gui { 74 | _sdl_context: sdl_context, 75 | _video_subsys: video_subsys, 76 | canvas, 77 | event_pump, 78 | horiz_refresh_rate, 79 | res_x, 80 | res_y, 81 | paused: false, 82 | running: false, 83 | wave_is_cosine, 84 | } 85 | } 86 | 87 | pub fn run(&mut self, notes: &Vec) { 88 | if self.running || notes.len() == 0 { 89 | return; 90 | } 91 | 92 | self.running = true; 93 | let mut cur_index = 0; 94 | let mut time_playing_cur_note = Duration::ZERO; 95 | let mut iteration_start; 96 | let mut previously_paused = false; 97 | 98 | // Special care must be taken to ensure first note is actually played. 99 | // We must handle events before playing it as well, because there's 100 | // a good chance we'll receive some event (like Shown or FocusGained) 101 | // that would cause the screen to go blank. 102 | self.handle_events(); 103 | self.play_note(¬es[0]); 104 | 105 | 'main_loop: loop { 106 | iteration_start = Instant::now(); 107 | let cur_note = ¬es[cur_index]; 108 | self.handle_events(); 109 | if !self.running { 110 | break 'main_loop; 111 | } 112 | if self.paused { 113 | thread::sleep(SLEEP_INTERVAL); 114 | previously_paused = true; 115 | continue; 116 | } else if previously_paused { 117 | self.play_note(cur_note); 118 | previously_paused = false; 119 | } 120 | 121 | if time_playing_cur_note > cur_note.duration { 122 | time_playing_cur_note = Duration::ZERO; 123 | cur_index += 1; 124 | if cur_index >= notes.len() { 125 | break 'main_loop; 126 | } 127 | let new_note = ¬es[cur_index]; 128 | self.play_note(new_note); 129 | } 130 | thread::sleep(SLEEP_INTERVAL); 131 | time_playing_cur_note += iteration_start.elapsed(); 132 | } 133 | self.running = false; 134 | } 135 | 136 | fn play_note(&mut self, new_note: &Note) { 137 | match new_note.freq { 138 | // note 139 | Some(freq) => { 140 | if self.wave_is_cosine { 141 | self.draw_cosine_wave(freq); 142 | } else { 143 | self.draw_square_wave(freq); 144 | } 145 | }, 146 | None => clear_and_present(&mut self.canvas, Color::BLACK), // rest 147 | }; 148 | } 149 | 150 | fn draw_square_wave(&mut self, note_freq: f64) { 151 | self.canvas.set_draw_color(Color::BLACK); 152 | self.canvas.clear(); 153 | 154 | self.canvas.set_draw_color(Color::WHITE); 155 | for y in 0..self.res_y { 156 | // approx time when arriving at this row 157 | let t = (y as f64) / self.horiz_refresh_rate; 158 | // Note that `cosine_is_positive` is true if and only if 159 | // cos(2pi*t*note_freq) > 0 160 | // 161 | // PROOF: let's abbreviate note_freq to f. 162 | // Since t > 0 and f > 0, the cast to i64 works as floor(), thus 163 | // cosine_is_positive <==> floor(2tf) mod 2 == 0 164 | // <==> floor(2tf) == 2n for some integer n 165 | // <==> 2n <= 2tf < 2n + 1 166 | // <==> 2pi*n <= 2pi*tf < 2pi*n + pi 167 | // In this interval, for any integer n, `cos` is monotonically 168 | // decreasing, and so 169 | // cosine_is_positive 170 | // <==> cos(2pi*n + pi) < cos(2pi*tf) <= cos(2pi*n) 171 | // <==> 0 < cos(2pi*tf) <= 1 QED 172 | // 173 | let cosine_is_positive = ((2.0 * t * note_freq) as i64) % 2 == 0; 174 | if cosine_is_positive { 175 | let origin = Point::new(0, y); 176 | let dest = Point::new(self.res_x, y); 177 | self.canvas.draw_line(origin, dest) 178 | .unwrap_or_else(|e| panic!("failed to draw line: {}", e)); 179 | } 180 | } 181 | self.canvas.present(); 182 | } 183 | 184 | fn draw_cosine_wave(&mut self, note_freq: f64) { 185 | self.canvas.set_draw_color(Color::BLACK); 186 | self.canvas.clear(); 187 | for y in 0..self.res_y { 188 | // approx time when arriving at this row 189 | let t = (y as f64) / self.horiz_refresh_rate; 190 | let dither: f64 = rand::thread_rng().sample(StandardNormal); 191 | // note: TAU = 2 * PI 192 | let raw_ampl = (consts::TAU * t * note_freq).cos(); 193 | let color_component = (127.5 * (1.0 + raw_ampl) + dither) as u8; 194 | let color = Color::RGB(color_component, 195 | color_component, 196 | color_component); 197 | self.canvas.set_draw_color(color); 198 | let origin = Point::new(0, y); 199 | let dest = Point::new(self.res_x, y); 200 | self.canvas.draw_line(origin, dest) 201 | .unwrap_or_else(|e| panic!("failed to draw line: {}", e)); 202 | } 203 | self.canvas.present(); 204 | } 205 | 206 | fn handle_events(&mut self) { 207 | for ev in self.event_pump.poll_iter() { 208 | match ev { 209 | Event::Quit {..} => self.running = false, 210 | Event::Window { win_event, .. } => match win_event { 211 | WindowEvent::Close => self.running = false, 212 | WindowEvent::FocusLost => { 213 | clear_and_present(&mut self.canvas, Color::BLACK); 214 | self.paused = true; 215 | }, 216 | WindowEvent::Shown 217 | | WindowEvent::Exposed 218 | | WindowEvent::FocusGained => { 219 | clear_and_present(&mut self.canvas, Color::BLACK); 220 | }, 221 | _ => {}, 222 | }, 223 | Event::KeyDown { keycode: Some(key), .. } => match key { 224 | Keycode::Q => self.running = false, 225 | Keycode::P | Keycode::Space => { 226 | if !self.paused { 227 | clear_and_present(&mut self.canvas, Color::BLACK); 228 | self.paused = true; 229 | } else { 230 | self.paused = false; 231 | // since we don't have access to current note, 232 | // let the main loop re-render it. 233 | } 234 | }, 235 | _ => {}, 236 | }, 237 | _ => {}, 238 | } 239 | } 240 | } 241 | 242 | } 243 | 244 | fn clear_and_present(canvas: &mut WindowCanvas, clear_color: Color) { 245 | canvas.set_draw_color(clear_color); 246 | canvas.clear(); 247 | canvas.present(); 248 | } 249 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 Luana Martins Barbosa 2 | // 3 | // This file is part of tempest-lcd. 4 | // tempest-lcd is free software, released under the 5 | // GNU Public License, version 2 only. 6 | // See COPYING.txt. 7 | 8 | mod args; 9 | mod gui; 10 | mod parser; 11 | 12 | use std::fs; 13 | use gui::Gui; 14 | 15 | fn main() { 16 | let arg_data = args::parse_args(); 17 | let filename = &arg_data.filename; 18 | let file_contents = fs::read_to_string(filename) 19 | .unwrap_or_else(|e| panic!("failed to read file {}: {}", filename, e)); 20 | 21 | let notes = parser::parse_file_contents(&file_contents); 22 | let mut gui = Gui::create(arg_data.horiz_refresh_rate, 23 | arg_data.wave_is_cosine); 24 | gui.run(¬es); 25 | } 26 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 Luana Martins Barbosa 2 | // 3 | // This file is part of tempest-lcd. 4 | // tempest-lcd is free software, released under the 5 | // GNU Public License, version 2 only. 6 | // See COPYING.txt. 7 | 8 | use std::time::Duration; 9 | 10 | pub struct Note { 11 | pub freq: Option, // in Hz; None means the note is a rest 12 | pub duration: Duration, 13 | } 14 | 15 | pub fn parse_file_contents(file_contents: &str) -> Vec { 16 | let mut lines = file_contents.lines(); 17 | let bpm = lines.next() 18 | .expect("empty first line, should have BPM value") 19 | .parse::() 20 | .unwrap_or_else(|e| panic!("failed to parse BPM (1st line) as f64: {}", e)); 21 | 22 | lines.filter(|line| !line.is_empty()) 23 | .flat_map(|line| line.split(' ')) 24 | .filter(|word| !word.is_empty()) 25 | .map(|word| parse_note(bpm, word)) 26 | .collect() 27 | } 28 | 29 | fn parse_note(bpm: f64, note: &str) -> Note { 30 | let mut chars = note.chars(); 31 | // we've filtered for word.is_empty() so this should have at least one char 32 | let note_name = chars.next().unwrap(); 33 | let semitone_offset = semitone_offset_from_la(note_name); 34 | let freq = if let Some(mut semitone) = semitone_offset { 35 | if let Some('#') = chars.clone().next() { 36 | chars.next(); // skip sharp 37 | semitone += 1; 38 | } 39 | let octave_char = chars.next() 40 | .expect("missing octave number in note!"); 41 | let note_octave = octave_char.to_digit(10) 42 | .expect(&format!("note octave should be a digit: got {}", octave_char)); 43 | 44 | // counted from A0 = 0. This is NOT the same as the MIDI number. 45 | let key_number = ((note_octave as i32) * 12 + semitone as i32) as f64; 46 | let note_freq = 440.0 * 2.0_f64.powf(key_number / 12.0 - 4.0); 47 | Some(note_freq) 48 | } else { 49 | None 50 | }; 51 | let duration_factor = match chars.next() { 52 | Some(c) => duration_factor_from_quarter(c), 53 | None => 1.0, // notes are quarters by default 54 | }; 55 | let duration_ms = (duration_factor * 60_000.0 / bpm).round(); 56 | Note { 57 | freq, 58 | duration: Duration::from_millis(duration_ms as u64), 59 | } 60 | } 61 | 62 | fn semitone_offset_from_la(note_name: char) -> Option { 63 | match note_name.to_ascii_lowercase() { 64 | 'r' => None, // rest 65 | 'b' => Some(2), 66 | 'a' => Some(0), 67 | 'g' => Some(-2), 68 | 'f' => Some(-4), 69 | 'e' => Some(-5), 70 | 'd' => Some(-7), 71 | 'c' => Some(-9), 72 | chr => panic!("unknown note name `{}'", chr), 73 | } 74 | } 75 | 76 | fn duration_factor_from_quarter(note_dur: char) -> f64 { 77 | match note_dur.to_ascii_lowercase() { 78 | 'w' => 4.0, // whole 79 | 'h' => 2.0, // half 80 | 'q' => 1.0, // quarter 81 | 'e' => 0.5, // eighth 82 | 's' => 0.25, // sixteenth 83 | 't' => 0.125, // thirthy-second 84 | chr => panic!("unknown note duration name `{}'", chr), 85 | } 86 | } 87 | 88 | #[cfg(test)] 89 | mod tests { 90 | use super::*; 91 | 92 | const FREQ_TOLERANCE: f64 = 0.01; 93 | 94 | fn test_note(note: &Note, expected_freq: f64, expected_duration: Duration) { 95 | let note_freq = note.freq.unwrap(); 96 | 97 | assert_eq!(note.duration, expected_duration); 98 | if (note_freq - expected_freq).abs() >= FREQ_TOLERANCE { 99 | panic!("note_freq = {}, expected_freq = {}", note_freq, expected_freq); 100 | } 101 | } 102 | 103 | #[test] 104 | fn parse_a4() { 105 | let note = parse_note(120.0, "a4"); 106 | let expected_freq = 440.0; 107 | let expected_duration = Duration::from_millis(500); 108 | test_note(¬e, expected_freq, expected_duration); 109 | } 110 | 111 | #[test] 112 | fn parse_c3h() { 113 | let note = parse_note(150.0, "c3h"); 114 | let expected_freq = 130.81; 115 | let expected_duration = Duration::from_millis(800); 116 | test_note(¬e, expected_freq, expected_duration); 117 | } 118 | 119 | #[test] 120 | fn parse_gsharp5e() { 121 | let note = parse_note(125.0, "G#5e"); 122 | let expected_freq = 830.61; 123 | let expected_duration = Duration::from_millis(240); 124 | test_note(¬e, expected_freq, expected_duration); 125 | } 126 | 127 | #[test] 128 | fn parse_rest() { 129 | let note = parse_note(120.0, "rE"); 130 | let expected_duration = Duration::from_millis(250); 131 | assert_eq!(note.freq, None); 132 | assert_eq!(note.duration, expected_duration); 133 | } 134 | #[test] 135 | fn test_parse_contents1() { 136 | let contents = "150\na2 c#3 e3"; 137 | let notes = parse_file_contents(contents); 138 | let expected_duration = Duration::from_millis(400); 139 | assert_eq!(notes.len(), 3); 140 | test_note(¬es[0], 110.0, expected_duration); 141 | test_note(¬es[1], 138.59, expected_duration); 142 | test_note(¬es[2], 164.81, expected_duration); 143 | } 144 | 145 | #[test] 146 | fn test_parse_contents2() { 147 | let contents = "80\nc5h e4w\ng3q f5e"; 148 | let notes = parse_file_contents(contents); 149 | assert_eq!(notes.len(), 4); 150 | test_note(¬es[0], 523.25, Duration::from_millis(1500)); 151 | test_note(¬es[1], 329.63, Duration::from_millis(3000)); 152 | test_note(¬es[2], 196.0, Duration::from_millis(750)); 153 | test_note(¬es[3], 698.46, Duration::from_millis(375)); 154 | } 155 | } 156 | --------------------------------------------------------------------------------