├── .gitignore ├── .vscode └── settings.json ├── Cargo.toml ├── LICENSE ├── README.md ├── assets └── the-rig-in-action-may-2018.jpg ├── scripts ├── build ├── push └── rust-crosscompiler-arm │ ├── Dockerfile │ └── include │ ├── arm-linux-gnueabihf-g++-with-link-search │ ├── arm-linux-gnueabihf-gcc-with-link-search │ ├── cargo │ ├── config │ ├── fixQualifiedLibraryPaths.sh │ ├── sources-armhf.list │ └── sources.list ├── src ├── chunk.rs ├── config.rs ├── controllers │ ├── clock_pulse.rs │ ├── init.rs │ ├── mod.rs │ ├── twister.rs │ ├── umi3.rs │ └── vt4_key.rs ├── devices │ ├── midi_keys.rs │ ├── midi_triggers.rs │ ├── mod.rs │ ├── multi.rs │ ├── offset.rs │ ├── pitch_offset_chunk.rs │ ├── root_offset_chunk.rs │ ├── root_select.rs │ ├── scale_offset_chunk.rs │ └── scale_select.rs ├── lfo.rs ├── loop_event.rs ├── loop_grid_launchpad.rs ├── loop_recorder.rs ├── loop_state.rs ├── loop_transform.rs ├── main.rs ├── midi_connection.rs ├── midi_time.rs ├── output_value.rs ├── scale.rs ├── scheduler.rs ├── throttled_output.rs └── trigger_envelope.rs └── zoia ├── 060_zoia_drums_and_bass.bin └── 061_zoia_smplr_and_synth.bin /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | .DS_Store 13 | node_modules 14 | 15 | loopdrop-config.json -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "adsr", 4 | "arecord", 5 | "blackbox", 6 | "bluebox", 7 | "guard", 8 | "highpass", 9 | "humantime", 10 | "indexmap", 11 | "kaoss", 12 | "kboard", 13 | "kmix", 14 | "lowpass", 15 | "micromonsta", 16 | "midir", 17 | "mutex", 18 | "name", 19 | "plughw", 20 | "port", 21 | "porta", 22 | "remoteable", 23 | "resync", 24 | "schedulable", 25 | "sidechain", 26 | "streichfett", 27 | "triggerable", 28 | "unswing", 29 | "unswug", 30 | "zoia" 31 | ], 32 | "editor.formatOnPaste": true, 33 | "editor.defaultFormatter": "matklad.rust-analyzer", 34 | "editor.formatOnSave": true 35 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | [package] 4 | name = "loop-drop" 5 | version = "0.0.0" 6 | publish = false 7 | 8 | [profile.release] 9 | debug = true 10 | 11 | [dependencies] 12 | indexmap = "1.6.1" 13 | midir = "0.5.0" 14 | lazy_static = "1.0" 15 | regex = "0.2.5" 16 | circular-queue = "0.2.0" 17 | rand = "0.6" 18 | serde = { version = "1.0", features = ["derive"] } 19 | serde_json = "1.0" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rust-loop-drop 2 | [WIP] Midi-only version of [Loop Drop](https://github.com/mmckegg/loop-drop-app) for running on low power machines like Raspberry Pi and Beaglebone 3 | 4 | You can listen to some of my jams made with rust-loop-drop here: https://soundcloud.com/destroy-with-science 5 | 6 | > :warning: **This codebase is not currently intended for use outside of my [music setup][2]** 7 | > 8 | > It follows 0-refactor _just make music_ development style, so... eh. But here's the code in all its gory glory, because open source is best source! 9 | 10 | [![The rig in action, May 2018](assets/the-rig-in-action-may-2018.jpg)][2] 11 | 12 | ## DESTROY WITH SCIENCE - Digital Devices 🎶 13 | 14 | This repo contains all of the code used to make the album! The head was [1323ff9](https://github.com/mmckegg/rust-loop-drop/commit/1323ff968e169f276c185834e2d93e147c3aebc0) at the time. 15 | 16 | [][1] 17 | 18 | > 19 | 20 | You can get it on [Bandcamp](https://destroywithscience.bandcamp.com/album/digital-devices), [Spotify]() and all the other usual places! 21 | 22 | It can also be "obtained" for free via #dat and the [Beaker Browser](https://beakerbrowser.com/) on the condition that you seed it! 23 | 24 | ``` 25 | dat://filez.destroywithscience.com/ 26 | ``` 27 | 28 | # License 29 | 30 | GNU Affero General Public License v3.0 31 | 32 | [1]: https://destroywithscience.bandcamp.com/album/digital-devices 33 | [2]: https://www.youtube.com/watch?v=TPSqQRR517o 34 | -------------------------------------------------------------------------------- /assets/the-rig-in-action-may-2018.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/rust-loop-drop/87d6cef47a9874ffdad4a2d91bccd48fec477811/assets/the-rig-in-action-may-2018.jpg -------------------------------------------------------------------------------- /scripts/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker build -t rust-ld-crosscompiler-arm ./scripts/rust-crosscompiler-arm && \ 3 | docker run -it --rm \ 4 | -v $(pwd):/source \ 5 | -v ~/.cargo/git:/root/.cargo/git \ 6 | -v ~/.cargo/registry:/root/.cargo/registry \ 7 | rust-ld-crosscompiler-arm "$@" -------------------------------------------------------------------------------- /scripts/push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | scripts/build && \ 3 | ssh pi@raspberrypi.local "sudo systemctl stop loop-drop" && \ 4 | scp target/arm-unknown-linux-gnueabihf/release/loop-drop pi@raspberrypi.local:loop-drop && \ 5 | #ssh pi@raspberrypi.local -t "RUST_BACKTRACE=1 sudo --preserve-env ./loop-drop" 6 | ssh pi@raspberrypi.local -t "./loop-drop" 7 | -------------------------------------------------------------------------------- /scripts/rust-crosscompiler-arm/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:jessie 2 | MAINTAINER Damien Lecan 3 | 4 | #ENV USER root 5 | ENV CHANNEL stable 6 | 7 | ENV CC_DIR /opt/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin 8 | ENV REAL_CC $CC_DIR/arm-linux-gnueabihf-gcc 9 | ENV CC arm-linux-gnueabihf-gcc-with-link-search 10 | ENV CXX arm-linux-gnueabihf-g++-with-link-search 11 | ENV PATH $CC_DIR:$PATH:/root/.cargo/bin 12 | ENV ROOT_FS / 13 | ENV OBJCOPY $CC_DIR/arm-linux-gnueabihf-objcopy 14 | ENV PKG_CONFIG_ALLOW_CROSS 1 15 | 16 | COPY include/config /tmp/.cargo/ 17 | COPY include/arm-linux-gnueabihf-gcc-with-link-search /usr/local/sbin/ 18 | COPY include/arm-linux-gnueabihf-g++-with-link-search /usr/local/sbin/ 19 | COPY include/fixQualifiedLibraryPaths.sh /usr/local/sbin/ 20 | COPY include/cargo /usr/local/sbin/ 21 | COPY include/sources.list /etc/apt/ 22 | #COPY include/sources-armhf.list /etc/apt/sources.list.d/ 23 | 24 | RUN mv /tmp/.cargo $HOME && \ 25 | dpkg --add-architecture armhf && \ 26 | apt-key adv --recv-keys --keyserver keys.gnupg.net 9165938D90FDDD2E && \ 27 | apt-get update && \ 28 | DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ 29 | build-essential \ 30 | ca-certificates \ 31 | file \ 32 | pkg-config \ 33 | curl \ 34 | libssl-dev \ 35 | libssl-dev:armhf && \ 36 | curl https://sh.rustup.rs -sSf | sh /dev/stdin -y && \ 37 | PATH=$PATH:$HOME/.cargo/bin && \ 38 | rustup target add arm-unknown-linux-gnueabihf && \ 39 | curl -sSL https://github.com/raspberrypi/tools/archive/master.tar.gz \ 40 | | tar -zxC /opt tools-master/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64 --strip=2 && \ 41 | fixQualifiedLibraryPaths.sh $ROOT_FS $REAL_CC && \ 42 | DEBIAN_FRONTEND=noninteractive apt-get remove --purge -y curl && \ 43 | DEBIAN_FRONTEND=noninteractive apt-get autoremove -y && \ 44 | rm -rf \ 45 | /var/lib/apt/lists/* \ 46 | /tmp/* \ 47 | /var/tmp/* && \ 48 | mkdir -p /source 49 | 50 | #FOR LOOP DROP: 51 | RUN apt-get update && \ 52 | DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ 53 | libasound2-dev \ 54 | libasound2-dev:armhf 55 | 56 | VOLUME ["/root/.cargo/git", "/root/.cargo/registry"] 57 | 58 | VOLUME ["/source"] 59 | WORKDIR /source 60 | 61 | CMD ["cargo", "build", "--release"] -------------------------------------------------------------------------------- /scripts/rust-crosscompiler-arm/include/arm-linux-gnueabihf-g++-with-link-search: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # /!\ Same config for gcc 4 | 5 | arm-linux-gnueabihf-g++ \ 6 | -isystem/usr/include/arm-linux-gnueabihf \ 7 | -isystem/usr/include \ 8 | -L/usr/lib/arm-linux-gnueabihf \ 9 | -L/usr/lib \ 10 | $@ 11 | 12 | -------------------------------------------------------------------------------- /scripts/rust-crosscompiler-arm/include/arm-linux-gnueabihf-gcc-with-link-search: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # /!\ Same config for g++ 4 | 5 | arm-linux-gnueabihf-gcc \ 6 | -isystem/usr/include/arm-linux-gnueabihf \ 7 | -isystem/usr/include \ 8 | -L/usr/lib/arm-linux-gnueabihf \ 9 | -L/usr/lib \ 10 | $@ 11 | 12 | -------------------------------------------------------------------------------- /scripts/rust-crosscompiler-arm/include/cargo: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | /root/.cargo/bin/cargo $@ --target=arm-unknown-linux-gnueabihf 4 | -------------------------------------------------------------------------------- /scripts/rust-crosscompiler-arm/include/config: -------------------------------------------------------------------------------- 1 | [target.arm-unknown-linux-gnueabihf] 2 | linker = "arm-linux-gnueabihf-gcc-with-link-search" 3 | -------------------------------------------------------------------------------- /scripts/rust-crosscompiler-arm/include/fixQualifiedLibraryPaths.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #This script is ugly, feel free to fix it 3 | 4 | if [ "$#" -ne 2 ]; then 5 | echo "usage ./cmd target-rootfs target-toolchain" 6 | exit -1 7 | fi 8 | 9 | #passed args 10 | ROOTFS=$1 11 | TOOLCHAIN=$2 12 | 13 | if [ -x $TOOLCHAIN ]; then 14 | echo "Passed valid toolchain" 15 | MACHINE=$($TOOLCHAIN -dumpmachine) 16 | DEB_MULTI_ARCH_MADNESS=$ROOTFS/usr/lib/$MACHINE 17 | fi 18 | 19 | CURRENTDIR=$PWD 20 | 21 | function adjustSymLinks 22 | { 23 | echo "Adjusting the symlinks in $1 to be relative" 24 | cd $1 25 | find . -maxdepth 1 -type l | while read i; 26 | do qualifies=$(file $i | sed -e "s/.*\`\(.*\)'/\1/g" | grep ^/lib) 27 | if [ -n "$qualifies" ]; then 28 | newPath=$(file $i | sed -e "s/.*\`\(.*\)'/\1/g" | sed -e "s,\`,,g" | sed -e "s,',,g" | sed -e "s,^/lib,$2/lib,g"); 29 | echo $i 30 | echo $newPath; 31 | #sudo rm $i; 32 | rm $i; 33 | #sudo ln -s $newPath $i; 34 | ln -s $newPath $i; 35 | fi 36 | done 37 | } 38 | 39 | adjustSymLinks $ROOTFS/usr/lib "../.." 40 | 41 | if [ -n "$DEB_MULTI_ARCH_MADNESS" -a -d "$DEB_MULTI_ARCH_MADNESS" ]; then 42 | echo "Debian multiarch dir exists, adjusting" 43 | adjustSymLinks $DEB_MULTI_ARCH_MADNESS "../../.." 44 | fi 45 | 46 | cd $CURRENTDIR 47 | -------------------------------------------------------------------------------- /scripts/rust-crosscompiler-arm/include/sources-armhf.list: -------------------------------------------------------------------------------- 1 | deb [arch=armhf] http://mirrordirector.raspbian.org/raspbian/ jessie main contrib non-free rpi 2 | -------------------------------------------------------------------------------- /scripts/rust-crosscompiler-arm/include/sources.list: -------------------------------------------------------------------------------- 1 | deb [arch=armhf] http://mirrordirector.raspbian.org/raspbian/ jessie main contrib non-free rpi 2 | deb http://httpredir.debian.org/debian jessie main 3 | deb http://httpredir.debian.org/debian jessie-updates main 4 | deb http://security.debian.org jessie/updates main 5 | -------------------------------------------------------------------------------- /src/chunk.rs: -------------------------------------------------------------------------------- 1 | pub use std::time::{SystemTime, Duration}; 2 | pub use ::output_value::OutputValue; 3 | pub use ::midi_time::MidiTime; 4 | use std::collections::HashSet; 5 | use ::serde::{Deserialize, Serialize}; 6 | 7 | pub trait Triggerable { 8 | // TODO: or should this be MidiTime?? 9 | fn trigger (&mut self, id: u32, value: OutputValue); 10 | fn on_tick (&mut self, _time: MidiTime) {} 11 | fn get_active (&self) -> Option> { None } 12 | fn latch_mode (&self) -> LatchMode { LatchMode::None } 13 | fn schedule_mode (&self) -> ScheduleMode { ScheduleMode::MostRecent } 14 | } 15 | 16 | #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Serialize, Deserialize)] 17 | pub struct Coords { 18 | pub row: u32, 19 | pub col: u32 20 | } 21 | 22 | impl Coords { 23 | pub fn new (row: u32, col: u32) -> Coords { 24 | Coords { row, col } 25 | } 26 | 27 | pub fn from (id: u32) -> Coords { 28 | Coords { 29 | row: id / 8, 30 | col: id % 8 31 | } 32 | } 33 | 34 | pub fn id_from (row: u32, col: u32) -> u32 { 35 | (row * 8) + col 36 | } 37 | 38 | // pub fn id (&self) -> u32 { 39 | // Coords::id_from(self.row, self.col) 40 | // } 41 | } 42 | 43 | #[derive(Serialize, Deserialize)] 44 | pub struct Shape { 45 | pub rows: u32, 46 | pub cols: u32 47 | } 48 | 49 | impl Shape { 50 | pub fn new (rows: u32, cols: u32) -> Shape { 51 | Shape { rows, cols } 52 | } 53 | } 54 | 55 | #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] 56 | pub struct MidiMap { 57 | pub chunk_index: usize, 58 | pub id: u32 59 | } 60 | 61 | pub struct ChunkMap { 62 | pub coords: Coords, 63 | pub shape: Shape, 64 | pub chunk: Box, 65 | pub channel: Option, 66 | pub color: u8, 67 | pub repeat_mode: RepeatMode 68 | } 69 | 70 | impl ChunkMap { 71 | pub fn new (chunk: Box, coords: Coords, shape: Shape, color: u8, channel: Option, repeat_mode: RepeatMode) -> Box { 72 | Box::new(ChunkMap { 73 | chunk, coords, shape, color, channel, repeat_mode 74 | }) 75 | } 76 | } 77 | 78 | #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Serialize, Deserialize)] 79 | pub enum RepeatMode { 80 | Global, 81 | OnlyQuant, 82 | NoCycle, 83 | None 84 | } 85 | 86 | #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] 87 | pub enum LatchMode { 88 | None, 89 | LatchSingle, 90 | LatchSuppress, 91 | NoSuppress 92 | } 93 | 94 | #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] 95 | pub enum ScheduleMode { 96 | MostRecent, 97 | Monophonic, 98 | Percussion 99 | } -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use chunk::{Coords, RepeatMode, Shape}; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_json::{json, to_writer_pretty}; 4 | use std::error::Error; 5 | use std::fs::File; 6 | use std::io::BufReader; 7 | 8 | impl Config { 9 | pub fn read(filepath: &str) -> Result> { 10 | let file = File::open(filepath)?; 11 | let reader = BufReader::new(file); 12 | 13 | let config = serde_json::from_reader(reader)?; 14 | Ok(config) 15 | } 16 | 17 | pub fn write(&self, filepath: &str) -> std::io::Result<()> { 18 | let myjson = json!(self); 19 | // println!("{}", myjson.to_string()); 20 | to_writer_pretty(&File::create(filepath)?, &myjson)?; 21 | Ok(()) 22 | } 23 | 24 | pub fn default() -> Self { 25 | let micromonsta_port_name = "MicroMonsta 2"; // synth 26 | 27 | let blackbox_output_name = "RK006 PORT 2"; // output 1 28 | let bluebox_output_name = "RK006 PORT 3"; // output 2 29 | let typhon_a_output_name = "RK006 PORT 4"; // output 3 30 | let typhon_b_output_name = "RK006 PORT 5"; // output 4 31 | let zero_one_four_output_name = "RK006 PORT 6"; // output 5 32 | let cv1_output_name = "RK006 PORT 7"; // output 6 33 | let cv2_output_name = "RK006 PORT 9"; // output 8 34 | let cv3_output_name = "RK006 PORT 10"; // output 9 35 | 36 | let clock_pulse_output_name = "RK006 PORT 8"; // output 7 37 | 38 | Config { 39 | chunks: vec![ 40 | // EXT SYNTH 41 | ChunkConfig { 42 | coords: Coords::new(0 + 8, 0), 43 | shape: Shape::new(3, 8), 44 | color: 125, // gross 45 | channel: Some(6), 46 | repeat_mode: RepeatMode::Global, 47 | device: DeviceConfig::multi(vec![ 48 | DeviceConfig::MidiKeys { 49 | output: MidiPortConfig::new(micromonsta_port_name, 1), 50 | velocity_map: None, 51 | offset_id: String::from("ext"), 52 | note_offset: -4, 53 | octave_offset: -1, 54 | }, 55 | DeviceConfig::MidiKeys { 56 | output: MidiPortConfig::new(blackbox_output_name, 3), 57 | velocity_map: None, 58 | offset_id: String::from("ext"), 59 | note_offset: -4, 60 | octave_offset: -1, 61 | }, 62 | ]), 63 | }, 64 | // EXT SYNTH OFFSET 65 | // (also sends pitch mod on channel 2 for slicer) 66 | ChunkConfig { 67 | coords: Coords::new(3 + 8, 0), 68 | shape: Shape::new(1, 8), 69 | color: 12, // soft yellow 70 | channel: None, 71 | repeat_mode: RepeatMode::OnlyQuant, 72 | device: DeviceConfig::multi(vec![ 73 | DeviceConfig::offset("ext"), 74 | DeviceConfig::PitchOffsetChunk { 75 | output: MidiPortConfig::new(blackbox_output_name, 3), 76 | }, 77 | ]), 78 | }, 79 | // BASS OFFSET 80 | ChunkConfig { 81 | device: DeviceConfig::offset("bass"), 82 | coords: Coords::new(4 + 8, 0), 83 | shape: Shape::new(1, 8), 84 | color: 43, // blue 85 | channel: None, 86 | repeat_mode: RepeatMode::OnlyQuant, 87 | }, 88 | // SYNTH OFFSET 89 | ChunkConfig { 90 | device: DeviceConfig::offset("keys"), 91 | coords: Coords::new(5 + 8, 0), 92 | shape: Shape::new(1, 8), 93 | color: 55, // pink 94 | channel: None, 95 | repeat_mode: RepeatMode::OnlyQuant, 96 | }, 97 | // ROOT NOTE SELECTOR 98 | ChunkConfig { 99 | device: DeviceConfig::RootSelect { 100 | output_modulators: vec![ 101 | // based default sample pitch off root note on blackbox 102 | ModulatorConfig::rx(blackbox_output_name, 1, Modulator::PitchBend(0.0)), 103 | ], 104 | }, 105 | coords: Coords::new(6 + 8, 0), 106 | shape: Shape::new(2, 8), 107 | color: 35, // soft green 108 | channel: None, 109 | repeat_mode: RepeatMode::OnlyQuant, 110 | }, 111 | // SCALE MODE SELECTOR 112 | ChunkConfig { 113 | device: DeviceConfig::ScaleSelect, 114 | coords: Coords::new(16, 0), 115 | shape: Shape::new(1, 8), 116 | color: 0, // black 117 | channel: None, 118 | repeat_mode: RepeatMode::OnlyQuant, 119 | }, 120 | // DRUMS 121 | ChunkConfig { 122 | device: DeviceConfig::MidiTriggers { 123 | output: MidiPortConfig::new(zero_one_four_output_name, 1), 124 | velocity_map: Some(vec![80, 80, 127]), 125 | trigger_ids: vec![36, 38, 40, 41], 126 | sidechain_output: Some(SidechainOutput { id: 0 }), 127 | }, 128 | coords: Coords::new(0, 0), 129 | shape: Shape::new(1, 4), 130 | color: 8, // warm white 131 | channel: Some(0), 132 | repeat_mode: RepeatMode::NoCycle, 133 | }, 134 | ChunkConfig { 135 | device: DeviceConfig::MidiTriggers { 136 | output: MidiPortConfig::new(blackbox_output_name, 10), 137 | velocity_map: Some(vec![100, 127]), 138 | trigger_ids: vec![36, 37, 38, 39], 139 | sidechain_output: None, 140 | }, 141 | coords: Coords::new(1, 0), 142 | shape: Shape::new(1, 4), 143 | color: 15, // yellow 144 | channel: Some(3), 145 | repeat_mode: RepeatMode::NoCycle, 146 | }, 147 | // SAMPLER 148 | ChunkConfig { 149 | device: DeviceConfig::MidiTriggers { 150 | output: MidiPortConfig::new(blackbox_output_name, 10), 151 | velocity_map: Some(vec![100, 127]), 152 | trigger_ids: vec![48, 49, 50, 51, 44, 45, 46, 47], 153 | sidechain_output: None, 154 | }, 155 | coords: Coords::new(0, 4), 156 | shape: Shape::new(2, 4), 157 | color: 9, // orange 158 | channel: Some(2), 159 | repeat_mode: RepeatMode::OnlyQuant, 160 | }, 161 | // BASS 162 | ChunkConfig { 163 | device: DeviceConfig::multi(vec![ 164 | DeviceConfig::MidiKeys { 165 | output: MidiPortConfig::new(typhon_a_output_name, 1), 166 | velocity_map: None, 167 | offset_id: String::from("bass"), 168 | note_offset: -4, 169 | octave_offset: -2, 170 | }, 171 | DeviceConfig::MidiKeys { 172 | output: MidiPortConfig::new(cv3_output_name, 1), 173 | velocity_map: None, 174 | offset_id: String::from("bass"), 175 | note_offset: -4, 176 | octave_offset: 0, 177 | }, 178 | ]), 179 | 180 | coords: Coords::new(2, 0), 181 | shape: Shape::new(6, 4), 182 | color: 43, // blue 183 | channel: Some(4), 184 | repeat_mode: RepeatMode::Global, 185 | }, 186 | // SYNTH 187 | ChunkConfig { 188 | device: DeviceConfig::multi(vec![ 189 | DeviceConfig::MidiKeys { 190 | output: MidiPortConfig::new(typhon_b_output_name, 1), 191 | velocity_map: None, 192 | offset_id: String::from("keys"), 193 | note_offset: -4, 194 | octave_offset: -1, 195 | }, 196 | DeviceConfig::MidiKeys { 197 | output: MidiPortConfig::new(blackbox_output_name, 2), 198 | offset_id: String::from("keys"), 199 | velocity_map: None, 200 | note_offset: -4, 201 | octave_offset: -1, 202 | }, 203 | ]), 204 | coords: Coords::new(2, 4), 205 | shape: Shape::new(6, 4), 206 | color: 59, // pink 207 | channel: Some(5), 208 | repeat_mode: RepeatMode::Global, 209 | }, 210 | ], 211 | clock_input_port_name: String::from("RK006"), 212 | clock_output_port_names: vec![String::from(micromonsta_port_name)], 213 | resync_port_names: vec![ 214 | String::from(blackbox_output_name), 215 | String::from(micromonsta_port_name), 216 | ], 217 | keep_alive_port_names: vec![], 218 | controllers: vec![ 219 | ControllerConfig::Twister { 220 | port_name: String::from("Midi Fighter Twister"), 221 | mixer_port: MidiPortConfig::new(bluebox_output_name, 1), 222 | modulators: vec![ 223 | ModulatorConfig::rx( 224 | zero_one_four_output_name, 225 | 1, 226 | Modulator::MaxCc(66, 24, 12), 227 | ), // LT tune 228 | ModulatorConfig::rx(zero_one_four_output_name, 1, Modulator::Cc(54, 0)), // CH decay 229 | ModulatorConfig::new(cv1_output_name, 1, Modulator::Cc(1, 64)), 230 | ModulatorConfig::new(cv2_output_name, 1, Modulator::Cc(1, 64)), 231 | ModulatorConfig::new(blackbox_output_name, 1, Modulator::Cc(1, 64)), 232 | ModulatorConfig::new(blackbox_output_name, 1, Modulator::Cc(2, 64)), 233 | ModulatorConfig::new(blackbox_output_name, 1, Modulator::Cc(3, 64)), 234 | ModulatorConfig::new(blackbox_output_name, 1, Modulator::Cc(4, 64)), 235 | ModulatorConfig::new(typhon_a_output_name, 1, Modulator::PitchBend(0.0)), 236 | ModulatorConfig::new(typhon_a_output_name, 1, Modulator::Cc(4, 0)), 237 | ModulatorConfig::new(typhon_b_output_name, 1, Modulator::PitchBend(0.0)), 238 | ModulatorConfig::new(typhon_b_output_name, 1, Modulator::Cc(4, 0)), // filter envelope 239 | ModulatorConfig::rx(micromonsta_port_name, 1, Modulator::PitchBend(0.0)), 240 | ModulatorConfig::rx(micromonsta_port_name, 1, Modulator::Cc(74, 64)), 241 | ModulatorConfig::rx( 242 | bluebox_output_name, 243 | 1, 244 | Modulator::PolarCcSwitch { 245 | cc_low: Some(1), 246 | cc_high: Some(2), 247 | cc_switch: Some(3), 248 | default: 96, 249 | }, 250 | ), 251 | ModulatorConfig::rx(bluebox_output_name, 1, Modulator::Cc(4, 64)), 252 | ], 253 | }, 254 | ControllerConfig::ClockPulse { 255 | output: MidiPortConfig::new(clock_pulse_output_name, 1), 256 | divider: 6, 257 | }, 258 | ControllerConfig::VT4Key { 259 | output: MidiPortConfig::new("VT-4", 1), 260 | }, 261 | ControllerConfig::Umi3 { 262 | port_name: String::from("Logidy UMI3"), 263 | }, 264 | ], 265 | } 266 | } 267 | } 268 | 269 | #[derive(Serialize, Deserialize)] 270 | pub struct Config { 271 | pub chunks: Vec, 272 | pub clock_input_port_name: String, 273 | pub clock_output_port_names: Vec, 274 | pub keep_alive_port_names: Vec, 275 | pub resync_port_names: Vec, 276 | pub controllers: Vec, 277 | } 278 | 279 | #[derive(Serialize, Deserialize)] 280 | pub struct ChunkConfig { 281 | pub coords: Coords, 282 | pub shape: Shape, 283 | pub color: u8, 284 | pub channel: Option, 285 | pub repeat_mode: RepeatMode, 286 | pub device: DeviceConfig, 287 | } 288 | 289 | #[derive(Serialize, Deserialize, Clone)] 290 | pub struct MidiPortConfig { 291 | pub name: String, 292 | pub channel: u8, 293 | } 294 | 295 | #[derive(Serialize, Deserialize, Clone)] 296 | pub struct SidechainOutput { 297 | pub id: u32, 298 | } 299 | 300 | impl MidiPortConfig { 301 | pub fn new(name: &str, channel: u8) -> Self { 302 | MidiPortConfig { 303 | name: String::from(name), 304 | channel, 305 | } 306 | } 307 | } 308 | 309 | #[derive(Serialize, Deserialize, Clone)] 310 | pub enum DeviceConfig { 311 | Multi { 312 | devices: Vec, 313 | }, 314 | MidiKeys { 315 | output: MidiPortConfig, 316 | offset_id: String, 317 | note_offset: i32, 318 | velocity_map: Option>, 319 | octave_offset: i32, 320 | }, 321 | OffsetChunk { 322 | id: String, 323 | }, 324 | PitchOffsetChunk { 325 | output: MidiPortConfig, 326 | }, 327 | RootSelect { 328 | output_modulators: Vec>, 329 | }, 330 | ScaleSelect, 331 | MidiTriggers { 332 | output: MidiPortConfig, 333 | trigger_ids: Vec, 334 | velocity_map: Option>, 335 | sidechain_output: Option, 336 | }, 337 | } 338 | 339 | impl DeviceConfig { 340 | pub fn offset(id: &str) -> Self { 341 | DeviceConfig::OffsetChunk { 342 | id: String::from(id), 343 | } 344 | } 345 | 346 | pub fn multi(devices: Vec) -> Self { 347 | DeviceConfig::Multi { devices } 348 | } 349 | } 350 | 351 | #[derive(Serialize, Deserialize, Clone)] 352 | pub enum ControllerConfig { 353 | Twister { 354 | port_name: String, 355 | mixer_port: MidiPortConfig, 356 | modulators: Vec>, 357 | }, 358 | Umi3 { 359 | port_name: String, 360 | }, 361 | VT4Key { 362 | output: MidiPortConfig, 363 | }, 364 | ClockPulse { 365 | output: MidiPortConfig, 366 | divider: i32, 367 | }, 368 | Init { 369 | modulators: Vec>, 370 | }, 371 | } 372 | 373 | #[derive(Serialize, Deserialize, Clone)] 374 | pub struct ModulatorConfig { 375 | pub port: MidiPortConfig, 376 | pub rx_port: Option, 377 | pub modulator: Modulator, 378 | } 379 | 380 | #[derive(Serialize, Deserialize, Clone)] 381 | pub enum Modulator { 382 | Cc(u8, u8), 383 | MaxCc(u8, u8, u8), 384 | PolarCcSwitch { 385 | cc_low: Option, 386 | cc_high: Option, 387 | cc_switch: Option, 388 | default: u8, 389 | }, 390 | PitchBend(f64), 391 | } 392 | 393 | impl ModulatorConfig { 394 | pub fn new(port_name: &str, port_number: u8, modulator: Modulator) -> Option { 395 | Some(ModulatorConfig { 396 | port: MidiPortConfig::new(port_name, port_number), 397 | rx_port: None, 398 | modulator, 399 | }) 400 | } 401 | pub fn rx(port_name: &str, port_number: u8, modulator: Modulator) -> Option { 402 | Some(ModulatorConfig { 403 | port: MidiPortConfig::new(port_name, port_number), 404 | rx_port: Some(MidiPortConfig::new(port_name, port_number)), 405 | modulator, 406 | }) 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /src/controllers/clock_pulse.rs: -------------------------------------------------------------------------------- 1 | use midi_connection; 2 | use scheduler::MidiTime; 3 | 4 | pub struct ClockPulse { 5 | midi_output: midi_connection::SharedMidiOutputConnection, 6 | channel: u8, 7 | divider: i32, 8 | } 9 | 10 | impl ClockPulse { 11 | pub fn new( 12 | midi_output: midi_connection::SharedMidiOutputConnection, 13 | channel: u8, 14 | divider: i32, 15 | ) -> Self { 16 | ClockPulse { 17 | midi_output, 18 | channel, 19 | divider, 20 | } 21 | } 22 | } 23 | 24 | impl ::controllers::Schedulable for ClockPulse { 25 | fn schedule(&mut self, pos: MidiTime, _length: MidiTime) { 26 | if (pos.ticks() - 1) % self.divider == 0 { 27 | self.midi_output 28 | .send(&[144 - 1 + self.channel, 36, 127]) 29 | .unwrap(); 30 | self.midi_output 31 | .send(&[144 - 1 + self.channel, 36, 0]) 32 | .unwrap(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/controllers/init.rs: -------------------------------------------------------------------------------- 1 | use ::midi_time::MidiTime; 2 | 3 | pub struct Init { 4 | modulators: Vec>, 5 | scheduled: bool 6 | } 7 | 8 | impl Init { 9 | pub fn new (modulators: Vec>) -> Self { 10 | Init { 11 | modulators, 12 | scheduled: false 13 | } 14 | } 15 | } 16 | 17 | impl ::controllers::Schedulable for Init { 18 | fn schedule (&mut self, _pos: MidiTime, _length: MidiTime) { 19 | if !self.scheduled { 20 | for modulator in &mut self.modulators { 21 | if let Some(modulator) = modulator { 22 | modulator.send_default(); 23 | } 24 | } 25 | self.scheduled = true 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/controllers/mod.rs: -------------------------------------------------------------------------------- 1 | mod clock_pulse; 2 | mod init; 3 | mod twister; 4 | mod umi3; 5 | mod vt4_key; 6 | 7 | use midi_time::MidiTime; 8 | 9 | pub use self::clock_pulse::ClockPulse; 10 | pub use self::init::Init; 11 | pub use self::twister::Twister; 12 | pub use self::umi3::Umi3; 13 | pub use self::vt4_key::VT4Key; 14 | 15 | pub struct Modulator { 16 | pub port: ::midi_connection::SharedMidiOutputConnection, 17 | pub channel: u8, 18 | pub modulator: ::config::Modulator, 19 | pub rx_port: Option<::config::MidiPortConfig>, 20 | } 21 | 22 | impl Modulator { 23 | pub fn send_polar(&mut self, value: f64) { 24 | if let ::config::Modulator::PitchBend(..) = self.modulator { 25 | let value = polar_to_msb_lsb(value); 26 | self.port 27 | .send(&[224 - 1 + self.channel, value.0, value.1]) 28 | .unwrap(); 29 | } else { 30 | self.send(polar_to_midi(value)); 31 | } 32 | } 33 | 34 | pub fn send(&mut self, value: u8) { 35 | match self.modulator { 36 | ::config::Modulator::Cc(id, ..) => { 37 | self.port 38 | .send(&[176 - 1 + self.channel, id, value]) 39 | .unwrap(); 40 | } 41 | ::config::Modulator::PolarCcSwitch { 42 | cc_low, 43 | cc_high, 44 | cc_switch, 45 | .. 46 | } => { 47 | let polar_value = midi_to_polar(value); 48 | if polar_value < 0.0 { 49 | if let Some(cc) = cc_low { 50 | let abs = polar_value * -1.0; 51 | let value = float_to_midi(abs * abs); 52 | self.port 53 | .send(&[176 - 1 + self.channel, cc, value]) 54 | .unwrap(); 55 | } 56 | } else { 57 | if let Some(cc) = cc_high { 58 | let value = float_to_midi(polar_value); 59 | self.port 60 | .send(&[176 - 1 + self.channel, cc, value]) 61 | .unwrap(); 62 | } 63 | } 64 | 65 | if let Some(cc) = cc_switch { 66 | let value = if polar_value < 0.0 { 0 } else { 127 }; 67 | 68 | self.port 69 | .send(&[176 - 1 + self.channel, cc, value]) 70 | .unwrap(); 71 | } 72 | } 73 | ::config::Modulator::MaxCc(id, max, ..) => { 74 | let f_value = value as f64 / 127.0 as f64; 75 | let u_value = (f_value * max as f64).min(127.0) as u8; 76 | self.port 77 | .send(&[176 - 1 + self.channel, id, u_value]) 78 | .unwrap(); 79 | } 80 | ::config::Modulator::PitchBend(..) => { 81 | let value = polar_to_msb_lsb(midi_to_polar(value)); 82 | self.port 83 | .send(&[224 - 1 + self.channel, value.0, value.1]) 84 | .unwrap(); 85 | } 86 | } 87 | } 88 | 89 | pub fn send_default(&mut self) { 90 | match self.modulator { 91 | ::config::Modulator::Cc(id, value) => { 92 | self.port 93 | .send(&[176 - 1 + self.channel, id, value]) 94 | .unwrap(); 95 | } 96 | ::config::Modulator::PolarCcSwitch { default, .. } => { 97 | self.send(default); 98 | } 99 | ::config::Modulator::MaxCc(id, max, value) => { 100 | self.port 101 | .send(&[176 - 1 + self.channel, id, value.min(max)]) 102 | .unwrap(); 103 | } 104 | ::config::Modulator::PitchBend(value) => { 105 | let value = ::controllers::polar_to_msb_lsb(value); 106 | self.port 107 | .send(&[224 - 1 + self.channel, value.0, value.1]) 108 | .unwrap(); 109 | } 110 | } 111 | } 112 | } 113 | 114 | pub trait Schedulable { 115 | fn schedule(&mut self, _pos: MidiTime, _length: MidiTime) {} 116 | } 117 | 118 | pub fn polar_to_msb_lsb(input: f64) -> (u8, u8) { 119 | let max = (2.0f64).powf(14.0) / 2.0; 120 | let input_14bit = (input.max(-1.0).min(0.99999999999) * max + max) as u16; 121 | 122 | let lsb = mask7(input_14bit as u8); 123 | let msb = mask7((input_14bit >> 7) as u8); 124 | 125 | (lsb, msb) 126 | } 127 | 128 | /// 7 bit mask 129 | #[inline(always)] 130 | pub fn mask7(input: u8) -> u8 { 131 | input & 0b01111111 132 | } 133 | 134 | pub fn midi_to_polar(value: u8) -> f64 { 135 | if value < 63 { 136 | (value as f64 - 63.0) / 63.0 137 | } else if value > 64 { 138 | (value as f64 - 64.0) / 63.0 139 | } else { 140 | 0.0 141 | } 142 | } 143 | 144 | pub fn midi_to_float(value: u8) -> f64 { 145 | value as f64 / 127.0 146 | } 147 | 148 | pub fn float_to_midi(value: f64) -> u8 { 149 | (value * 127.0).max(0.0).min(127.0) as u8 150 | } 151 | 152 | pub fn polar_to_midi(value: f64) -> u8 { 153 | let midi = (value + 1.0) / 2.0 * 127.0; 154 | midi.max(0.0).min(127.0) as u8 155 | } 156 | 157 | // pub fn random_range(from: u8, to: u8) -> u8 { 158 | // rand::thread_rng().gen_range(from, to) 159 | // } 160 | 161 | pub fn midi_ease_out(value: u8) -> u8 { 162 | let f = midi_to_float(value); 163 | float_to_midi(f * (2.0 - f)) 164 | } 165 | -------------------------------------------------------------------------------- /src/controllers/twister.rs: -------------------------------------------------------------------------------- 1 | use lfo::Lfo; 2 | use loop_grid_launchpad::LoopGridParams; 3 | use loop_recorder::{LoopEvent, LoopRecorder}; 4 | use midi_connection; 5 | use output_value::OutputValue; 6 | use std::sync::mpsc; 7 | use throttled_output::ThrottledOutput; 8 | use trigger_envelope::TriggerEnvelope; 9 | use MidiTime; 10 | 11 | use controllers::{float_to_midi, midi_ease_out, midi_to_polar, polar_to_midi, Modulator}; 12 | use std::collections::{HashMap, HashSet}; 13 | use std::sync::{Arc, Mutex}; 14 | use std::thread; 15 | 16 | use super::midi_to_float; 17 | 18 | pub struct Twister { 19 | _midi_input: midi_connection::ThreadReference, 20 | tx: mpsc::Sender, 21 | } 22 | 23 | #[derive(Debug, Clone, Copy, PartialEq)] 24 | enum EventSource { 25 | User, 26 | Loop, 27 | } 28 | 29 | impl Twister { 30 | pub fn new( 31 | port_name: &str, 32 | main_output: midi_connection::SharedMidiOutputConnection, 33 | main_channel: u8, 34 | modulators: Vec>, 35 | params: Arc>, 36 | ) -> Self { 37 | let (tx, rx) = mpsc::channel(); 38 | // let clock_sender = clock.sender.clone(); 39 | let control_ids = get_control_ids(); 40 | 41 | let channel_offsets = [10, 20, 30, 40, 50, 60, 70, 80]; 42 | 43 | let tx_input = tx.clone(); 44 | let tx_feedback = tx.clone(); 45 | let tx_clock = tx.clone(); 46 | 47 | let mut output = midi_connection::get_shared_output(port_name); 48 | 49 | let input = midi_connection::get_input(port_name, move |_stamp, message| { 50 | let control = Control::from_id(message[1] as u32); 51 | if message[0] == 176 { 52 | tx_input 53 | .send(TwisterMessage::ControlChange( 54 | control, 55 | OutputValue::On(message[2]), 56 | EventSource::User, 57 | )) 58 | .unwrap(); 59 | } else if message[0] == 177 { 60 | tx_input 61 | .send(TwisterMessage::Recording(control, message[2] > 0)) 62 | .unwrap(); 63 | } else if message[0] == 179 && message[1] < 4 && message[2] == 127 { 64 | tx_input 65 | .send(TwisterMessage::BankChange(message[1])) 66 | .unwrap(); 67 | } else if message[0] == 179 68 | && (message[1] == 10 || message[1] == 16 || message[1] == 22 || message[1] == 28) 69 | { 70 | tx_input 71 | .send(TwisterMessage::LeftButton(message[2] > 0)) 72 | .unwrap(); 73 | } else if message[0] == 179 74 | && (message[1] == 13 || message[1] == 19 || message[1] == 25 || message[1] == 31) 75 | { 76 | tx_input 77 | .send(TwisterMessage::RightButton(message[2] > 0)) 78 | .unwrap(); 79 | } 80 | }); 81 | 82 | thread::spawn(move || { 83 | let mut recorder = LoopRecorder::new(); 84 | let mut last_pos = MidiTime::zero(); 85 | let mut last_values: HashMap = HashMap::new(); 86 | let mut record_start_times = HashMap::new(); 87 | let mut loops: HashMap = HashMap::new(); 88 | let mut modulators = modulators; 89 | let mut throttled_main_output = ThrottledOutput::new(main_output); 90 | 91 | let mut current_bank = 0; 92 | 93 | let mut frozen = false; 94 | let mut cueing = false; 95 | let mut frozen_values = None; 96 | let mut frozen_loops: Option> = None; 97 | let mut cued_values: Option> = None; 98 | let mut triggering_channels: HashSet = HashSet::new(); 99 | 100 | let mut lfo_amounts = HashMap::new(); 101 | let mut duck_amounts = HashMap::new(); 102 | 103 | let mut lfo = Lfo::new(); 104 | let mut trigger_envelope = TriggerEnvelope::new(0.9, 0.5); 105 | 106 | for channel in 0..8 { 107 | last_values.insert(Control::ChannelVolume(channel), 80); 108 | last_values.insert(Control::ChannelReverb(channel), 0); 109 | last_values.insert(Control::ChannelDelay(channel), 0); 110 | last_values.insert(Control::ChannelFilterLfoAmount(channel), 64); 111 | last_values.insert(Control::ChannelFilter(channel), 64); 112 | last_values.insert(Control::ChannelDuck(channel), 64); 113 | } 114 | 115 | last_values.insert(Control::Swing, 64); 116 | last_values.insert(Control::DuckRelease, 64); 117 | last_values.insert(Control::LfoRate, 64); 118 | last_values.insert(Control::LfoSkew, 64); 119 | 120 | // default values for modulators 121 | for (index, modulator) in modulators.iter().enumerate() { 122 | if let Some(modulator) = modulator { 123 | last_values.insert( 124 | Control::Modulator(index), 125 | match modulator.modulator { 126 | ::config::Modulator::Cc(_id, value) => value, 127 | ::config::Modulator::PolarCcSwitch { default, .. } => default, 128 | ::config::Modulator::MaxCc(_id, max, value) => { 129 | float_to_midi(value.min(max) as f64 / max as f64) 130 | } 131 | ::config::Modulator::PitchBend(value) => polar_to_midi(value), 132 | }, 133 | ); 134 | } 135 | } 136 | 137 | // update display and send all of the start values on load 138 | for control in control_ids.keys() { 139 | tx.send(TwisterMessage::Send(*control)).unwrap(); 140 | tx.send(TwisterMessage::Refresh(*control)).unwrap(); 141 | if let Some(control_id) = control_ids.get(control) { 142 | recorder.allocate(*control_id, 50000); 143 | } 144 | } 145 | 146 | // enable nemesis pedal! 147 | // throttled_main_output.send(&[176 + digit_channel - 1, 38, 127]); 148 | 149 | for received in rx { 150 | match received { 151 | TwisterMessage::LeftButton(pressed) | TwisterMessage::RightButton(pressed) => { 152 | let mut params = params.lock().unwrap(); 153 | if pressed { 154 | // if already frozen, go into cueing mode 155 | // if already in cueing mode, revert back to normal frozen mode 156 | if params.cueing { 157 | params.cueing = false 158 | } else if params.frozen { 159 | params.cueing = true 160 | } else { 161 | params.frozen = true 162 | } 163 | } else if !cued_values.is_some() { 164 | // only leave frozen on button up if not cueing 165 | params.frozen = false 166 | } 167 | } 168 | TwisterMessage::BankChange(bank) => { 169 | let mut params = params.lock().unwrap(); 170 | params.bank = bank; 171 | } 172 | TwisterMessage::ControlChange(control, value, source) => { 173 | if let Some(id) = control_ids.get(&control) { 174 | let allow = if loops.contains_key(&control) { 175 | let item = loops.get(&control).unwrap(); 176 | (item.offset + item.length) < (last_pos - MidiTime::from_ticks(8)) 177 | } else { 178 | true 179 | }; 180 | 181 | if allow { 182 | let event = LoopEvent { 183 | id: id.clone(), 184 | value, 185 | pos: last_pos, 186 | }; 187 | 188 | tx_feedback 189 | .send(TwisterMessage::Event(event, source)) 190 | .unwrap(); 191 | } 192 | } 193 | } 194 | TwisterMessage::Send(control) => { 195 | let last_value = last_values.get(&control).unwrap_or(&0); 196 | let value = if let Some(lfo_amount) = lfo_amounts.get(&control) { 197 | let lfo_value = lfo.get_value_at(last_pos); 198 | if *lfo_amount > 0.0 { 199 | // bipolar modulation (CV style) 200 | let polar = ((lfo_value * 2.0) - 1.0) * lfo_amount; 201 | (*last_value as f64 + (polar * 64.0)).min(127.0).max(0.0) as u8 202 | } else { 203 | // treat current value as max and multiplier (subtract / sidechain style) 204 | let offset: f64 = lfo_value * (*last_value as f64) * lfo_amount; 205 | (*last_value as f64 + offset).min(127.0).max(0.0) as u8 206 | } 207 | } else if let Some(duck_amount) = duck_amounts.get(&control) { 208 | let multiplier = 1.0 - trigger_envelope.value() as f64 * duck_amount; 209 | (*last_value as f64 * multiplier).min(127.0).max(0.0) as u8 210 | } else { 211 | *last_value 212 | }; 213 | 214 | match control { 215 | Control::ChannelVolume(channel) => { 216 | let cc = 217 | channel_offsets[channel as usize % channel_offsets.len()] + 0; 218 | throttled_main_output.send(&[ 219 | 176 - 1 + main_channel, 220 | cc as u8, 221 | midi_ease_out(value), 222 | ]); 223 | } 224 | 225 | Control::ChannelReverb(channel) => { 226 | let cc = 227 | channel_offsets[channel as usize % channel_offsets.len()] + 1; 228 | throttled_main_output.send(&[ 229 | 176 - 1 + main_channel, 230 | cc as u8, 231 | midi_ease_out(value), 232 | ]); 233 | } 234 | Control::ChannelDelay(channel) => { 235 | let cc = 236 | channel_offsets[channel as usize % channel_offsets.len()] + 2; 237 | throttled_main_output.send(&[ 238 | 176 - 1 + main_channel, 239 | cc as u8, 240 | midi_ease_out(value), 241 | ]); 242 | } 243 | 244 | Control::ChannelFilter(channel) => { 245 | let hp_cc = 246 | channel_offsets[channel as usize % channel_offsets.len()] + 3; 247 | let lp_cc = 248 | channel_offsets[channel as usize % channel_offsets.len()] + 4; 249 | 250 | if value > 60 { 251 | throttled_main_output.send(&[ 252 | 176 - 1 + main_channel, 253 | hp_cc as u8, 254 | (value.max(64) - 64) * 2, 255 | ]); 256 | } 257 | 258 | if value < 70 { 259 | throttled_main_output.send(&[ 260 | 176 - 1 + main_channel, 261 | lp_cc as u8, 262 | value.min(63) * 2, 263 | ]); 264 | } 265 | } 266 | 267 | Control::ChannelDuck(channel) => { 268 | duck_amounts 269 | .insert(Control::ChannelVolume(channel), midi_to_float(value)); 270 | } 271 | 272 | Control::DuckRelease => { 273 | let multiplier = ((midi_to_float(value) / 2.0) * 0.95) + 0.5; 274 | trigger_envelope.tick_multiplier = multiplier as f32; 275 | } 276 | 277 | Control::Swing => { 278 | let mut params = params.lock().unwrap(); 279 | let linear_swing = (value as f64 - 64.0) / 64.0; 280 | params.swing = if value == 63 || value == 64 { 281 | 0.0 282 | } else if linear_swing < 0.0 { 283 | -linear_swing.abs().powf(2.0) 284 | } else { 285 | linear_swing.powf(2.0) 286 | }; 287 | } 288 | Control::LfoRate => { 289 | lfo.speed = value; 290 | } 291 | Control::LfoSkew => { 292 | lfo.skew = value; 293 | } 294 | Control::Modulator(index) => { 295 | if let Some(Some(modulator)) = modulators.get_mut(index) { 296 | modulator.send(value); 297 | } 298 | } 299 | Control::ChannelFilterLfoAmount(channel) => { 300 | lfo_amounts 301 | .insert(Control::ChannelFilter(channel), midi_to_polar(value)); 302 | } 303 | 304 | Control::None => (), 305 | } 306 | } 307 | TwisterMessage::Event(event, source) => { 308 | let control = Control::from_id(event.id); 309 | let value = event.value.value(); 310 | 311 | if source != EventSource::Loop && !cueing { 312 | loops.remove(&control); 313 | } 314 | 315 | if let Some(loops) = &mut cued_values { 316 | if source != EventSource::Loop && cueing { 317 | loops.insert(control, value); 318 | } 319 | } 320 | 321 | if source == EventSource::Loop || !cueing { 322 | last_values.insert(control, value); 323 | } 324 | 325 | // suppress updating device with cued values 326 | if source == EventSource::Loop || (source == EventSource::User && !cueing) { 327 | tx_feedback.send(TwisterMessage::Send(control)).unwrap(); 328 | } 329 | 330 | tx_feedback.send(TwisterMessage::Refresh(control)).unwrap(); 331 | 332 | recorder.add(event); 333 | } 334 | 335 | TwisterMessage::Recording(control, recording) => { 336 | if recording { 337 | record_start_times.insert(control, last_pos); 338 | } else { 339 | if let Some(pos) = record_start_times.remove(&control) { 340 | let loop_length = MidiTime::quantize_length(last_pos - pos); 341 | if loop_length < MidiTime::from_ticks(16) { 342 | loops.remove(&control); 343 | } else { 344 | loops.insert( 345 | control, 346 | Loop { 347 | offset: last_pos - loop_length, 348 | length: loop_length, 349 | }, 350 | ); 351 | } 352 | } 353 | } 354 | } 355 | 356 | TwisterMessage::Refresh(control) => { 357 | let cued_value = if let Some(cued_values) = &cued_values { 358 | if cueing { 359 | cued_values.get(&control) 360 | } else { 361 | None 362 | } 363 | } else { 364 | None 365 | }; 366 | 367 | let value = *last_values.get(&control).unwrap_or(&0); 368 | if let Some(id) = control_ids.get(&control) { 369 | output 370 | .send(&[176, id.clone() as u8, *cued_value.unwrap_or(&value)]) 371 | .unwrap(); 372 | 373 | let channel = id / 2 % 8; 374 | 375 | // MFT animation for currently looping (Channel 6) 376 | if cued_value.is_some() { 377 | output.send(&[181, id.clone() as u8, 61]).unwrap(); 378 | // Fast Indicator Pulse 379 | } else if cueing { 380 | output.send(&[181, id.clone() as u8, 15]).unwrap(); 381 | // Fast RGB Pulse 382 | } else if frozen { 383 | output.send(&[181, id.clone() as u8, 59]).unwrap(); 384 | // Slow Indicator Pulse 385 | } else if triggering_channels.contains(&channel) { 386 | output.send(&[181, id.clone() as u8, 17]).unwrap(); 387 | // Turn off indicator (flash) 388 | } else if loops.contains_key(&control) { 389 | // control has loop 390 | output.send(&[181, id.clone() as u8, 13]).unwrap(); 391 | // Slow RGB Pulse 392 | } else { 393 | output.send(&[181, id.clone() as u8, 0]).unwrap(); 394 | } 395 | } 396 | } 397 | 398 | TwisterMessage::Schedule { pos, length } => { 399 | let mut params = params.lock().unwrap(); 400 | if params.reset_automation { 401 | // HACK: ack reset message from clear all 402 | params.reset_automation = false; 403 | loops.clear(); 404 | 405 | for control in control_ids.keys() { 406 | tx.send(TwisterMessage::Refresh(*control)).unwrap(); 407 | } 408 | } 409 | 410 | let mut to_refresh = triggering_channels.clone(); 411 | triggering_channels.clear(); 412 | 413 | for channel in ¶ms.channel_triggered { 414 | triggering_channels.insert(*channel); 415 | to_refresh.insert(*channel); 416 | } 417 | 418 | params.channel_triggered.clear(); 419 | 420 | trigger_envelope.tick(params.duck_triggered); 421 | params.duck_triggered = false; 422 | 423 | for (control, id) in control_ids.iter() { 424 | let channel = id / 2 % 8; 425 | if to_refresh.contains(&channel) { 426 | tx.send(TwisterMessage::Refresh(*control)).unwrap(); 427 | } 428 | } 429 | 430 | if current_bank != params.bank { 431 | output.send(&[179, params.bank, 127]).unwrap(); 432 | current_bank = params.bank; 433 | } 434 | 435 | if params.frozen != frozen { 436 | frozen = params.frozen; 437 | 438 | if frozen { 439 | frozen_values = Some(last_values.clone()); 440 | frozen_loops = Some(loops.clone()); 441 | for control in control_ids.keys() { 442 | tx.send(TwisterMessage::Refresh(*control)).unwrap(); 443 | } 444 | } else { 445 | if let Some(frozen_loops) = frozen_loops.take() { 446 | loops = frozen_loops; 447 | } 448 | if let Some(frozen_values) = frozen_values.take() { 449 | for (control, _) in &control_ids { 450 | if !loops.contains_key(control) 451 | && frozen_values.get(control) 452 | != last_values.get(control) 453 | { 454 | // queue a value send for changed values on next message loop 455 | tx.send(TwisterMessage::Send(*control)).unwrap(); 456 | } 457 | } 458 | 459 | last_values = frozen_values; 460 | } 461 | 462 | if let Some(values) = cued_values { 463 | for (key, value) in values { 464 | last_values.insert(key, value); 465 | tx.send(TwisterMessage::Send(key)).unwrap(); 466 | } 467 | } 468 | 469 | for control in control_ids.keys() { 470 | tx.send(TwisterMessage::Refresh(*control)).unwrap(); 471 | } 472 | 473 | cued_values = None; 474 | } 475 | } 476 | 477 | if params.cueing != cueing { 478 | cueing = params.cueing; 479 | if cueing { 480 | if !cued_values.is_some() { 481 | cued_values = Some(HashMap::new()); 482 | } 483 | } else { 484 | // force refresh to clear out stalled animations by swapping pages 485 | output.send(&[179, (params.bank + 1) % 4, 127]).unwrap(); 486 | output.send(&[179, params.bank, 127]).unwrap(); 487 | } 488 | 489 | for control in control_ids.keys() { 490 | tx.send(TwisterMessage::Refresh(*control)).unwrap(); 491 | } 492 | } 493 | 494 | let mut scheduled = HashSet::new(); 495 | for (control, value) in &loops { 496 | let offset = value.offset % value.length; 497 | let playback_pos = value.offset + ((pos - offset) % value.length); 498 | 499 | if let Some(id) = control_ids.get(control) { 500 | if let Some(range) = recorder.get_range_for( 501 | id.clone(), 502 | playback_pos, 503 | playback_pos + length, 504 | ) { 505 | for event in range { 506 | tx_feedback 507 | .send(TwisterMessage::Event( 508 | event.clone(), 509 | EventSource::Loop, 510 | )) 511 | .unwrap(); 512 | scheduled.insert(control); 513 | } 514 | } 515 | } 516 | } 517 | 518 | let mut to_refresh = HashSet::new(); 519 | 520 | for (control, value) in &lfo_amounts { 521 | if value != &0.0 { 522 | to_refresh.insert(control); 523 | } 524 | } 525 | 526 | for (control, value) in &duck_amounts { 527 | // only schedule ducks if enabled and trigger has value in midi resolution 528 | if value > &0.0 && float_to_midi(trigger_envelope.value() as f64) > 0 { 529 | to_refresh.insert(control); 530 | } 531 | } 532 | 533 | for control in to_refresh { 534 | tx_feedback.send(TwisterMessage::Send(*control)).unwrap(); 535 | } 536 | 537 | last_pos = pos; 538 | 539 | throttled_main_output.flush(); 540 | } 541 | } 542 | } 543 | }); 544 | 545 | Twister { 546 | _midi_input: input, 547 | tx: tx_clock, 548 | } 549 | } 550 | } 551 | 552 | impl ::controllers::Schedulable for Twister { 553 | fn schedule(&mut self, pos: MidiTime, length: MidiTime) { 554 | self.tx 555 | .send(TwisterMessage::Schedule { pos, length }) 556 | .unwrap(); 557 | } 558 | } 559 | 560 | #[derive(Debug, Clone)] 561 | enum TwisterMessage { 562 | ControlChange(Control, OutputValue, EventSource), 563 | BankChange(u8), 564 | Event(LoopEvent, EventSource), 565 | Send(Control), 566 | Refresh(Control), 567 | Recording(Control, bool), 568 | LeftButton(bool), 569 | RightButton(bool), 570 | Schedule { pos: MidiTime, length: MidiTime }, 571 | } 572 | 573 | #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)] 574 | enum Control { 575 | ChannelVolume(u32), 576 | ChannelFilter(u32), 577 | 578 | ChannelReverb(u32), 579 | ChannelDelay(u32), 580 | 581 | ChannelFilterLfoAmount(u32), 582 | ChannelDuck(u32), 583 | 584 | Modulator(usize), 585 | 586 | DuckRelease, 587 | Swing, 588 | 589 | LfoRate, 590 | LfoSkew, 591 | 592 | None, 593 | } 594 | 595 | #[derive(Debug, Clone)] 596 | struct Loop { 597 | offset: MidiTime, 598 | length: MidiTime, 599 | } 600 | 601 | impl Control { 602 | fn from_id(id: u32) -> Control { 603 | let page = id / 16; 604 | let control = id % 2; 605 | let channel = (id % 16) / 2; 606 | 607 | match (page, channel, control) { 608 | // Bank A 609 | (0, channel, 0) => Control::ChannelVolume(channel), 610 | (0, channel, 1) => Control::ChannelFilter(channel), 611 | 612 | // Bank B 613 | (1, 7, control) => Control::Modulator((7 * 2 + control) as usize), 614 | (1, channel, 0) => Control::ChannelReverb(channel), 615 | (1, channel, 1) => Control::ChannelDelay(channel), 616 | 617 | // Bank C 618 | (2, 0, 0) => Control::DuckRelease, 619 | (2, 0, 1) => Control::LfoRate, 620 | (2, channel, 0) => Control::ChannelDuck(channel), 621 | (2, channel, 1) => Control::ChannelFilterLfoAmount(channel), 622 | 623 | // PARAMS 624 | (3, 7, 0) => Control::Swing, 625 | (3, 7, 1) => Control::LfoSkew, 626 | (3, channel, control) => Control::Modulator((channel * 2 + control) as usize), 627 | 628 | _ => Control::None, 629 | } 630 | } 631 | } 632 | 633 | fn get_control_ids() -> HashMap { 634 | let mut result = HashMap::new(); 635 | for id in 0..64 { 636 | let control = Control::from_id(id); 637 | if control != Control::None { 638 | result.insert(control, id); 639 | } 640 | } 641 | result 642 | } 643 | -------------------------------------------------------------------------------- /src/controllers/umi3.rs: -------------------------------------------------------------------------------- 1 | use ::loop_grid_launchpad::LoopGridRemoteEvent; 2 | use ::midi_connection; 3 | 4 | use std::sync::mpsc; 5 | 6 | pub struct Umi3 { 7 | _midi_input: midi_connection::ThreadReference 8 | } 9 | 10 | impl Umi3 { 11 | pub fn new (port_name: &str, remote_tx: mpsc::Sender) -> Self { 12 | let input = midi_connection::get_input(port_name, move |_stamp, message| { 13 | match message { 14 | [144, 60, velocity] => { 15 | remote_tx.send(LoopGridRemoteEvent::LoopButton(velocity > &0)).unwrap(); 16 | }, 17 | [144, 62, velocity] => { 18 | remote_tx.send(LoopGridRemoteEvent::DoubleButton(velocity > &0)).unwrap(); 19 | }, 20 | [144, 64, velocity] => { 21 | remote_tx.send(LoopGridRemoteEvent::SustainButton(velocity > &0)).unwrap(); 22 | }, 23 | _ => () 24 | } 25 | }); 26 | 27 | Umi3 { 28 | _midi_input: input 29 | } 30 | } 31 | } 32 | 33 | impl ::controllers::Schedulable for Umi3 {} -------------------------------------------------------------------------------- /src/controllers/vt4_key.rs: -------------------------------------------------------------------------------- 1 | use ::midi_connection; 2 | use std::sync::{Arc, Mutex}; 3 | use ::scale::{Scale}; 4 | use ::scheduler::MidiTime; 5 | 6 | pub struct VT4Key { 7 | midi_output: midi_connection::SharedMidiOutputConnection, 8 | channel: u8, 9 | scale: Arc>, 10 | last_key: Option 11 | } 12 | 13 | impl VT4Key { 14 | pub fn new (midi_output: midi_connection::SharedMidiOutputConnection, channel: u8, scale: Arc>) -> Self { 15 | VT4Key { 16 | midi_output, 17 | channel, 18 | scale, 19 | last_key: None 20 | } 21 | } 22 | } 23 | 24 | impl ::controllers::Schedulable for VT4Key { 25 | fn schedule (&mut self, _pos: MidiTime, _length: MidiTime) { 26 | let key; 27 | let scale = self.scale.lock().unwrap(); 28 | 29 | { // immutable borrow 30 | let from_c = scale.root - 60; 31 | let base_key = modulo(from_c, 12); 32 | let offset = get_mode_offset(modulo(scale.scale, 7)); 33 | key = modulo(base_key - offset, 12) as u8; 34 | } 35 | 36 | if Some(key) != self.last_key { 37 | self.midi_output.send(&[176 - 1 + self.channel, 48, key]).unwrap(); 38 | self.last_key = Some(key); 39 | } 40 | } 41 | } 42 | 43 | fn modulo (n: i32, m: i32) -> i32 { 44 | ((n % m) + m) % m 45 | } 46 | 47 | fn get_mode_offset (mode: i32) -> i32 { 48 | let mut offset = 0; 49 | let intervals = [2, 2, 1, 2, 2, 2, 1]; 50 | 51 | for i in 0..6 { 52 | if (i as i32) >= mode { 53 | break 54 | } 55 | offset += intervals[i]; 56 | } 57 | 58 | offset 59 | } -------------------------------------------------------------------------------- /src/devices/midi_keys.rs: -------------------------------------------------------------------------------- 1 | use chunk::{MidiTime, OutputValue, Triggerable}; 2 | use midi_connection; 3 | use std::collections::HashMap; 4 | use std::sync::{Arc, Mutex}; 5 | 6 | pub use midi_connection::SharedMidiOutputConnection; 7 | pub use scale::{Offset, Scale}; 8 | 9 | pub struct MidiKeys { 10 | pub midi_port: midi_connection::SharedMidiOutputConnection, 11 | midi_channel: u8, 12 | output_values: HashMap, 13 | scale: Arc>, 14 | offset: Arc>, 15 | velocity_map: Option>, 16 | octave_offset: i32, 17 | } 18 | 19 | impl MidiKeys { 20 | pub fn new( 21 | midi_port: midi_connection::SharedMidiOutputConnection, 22 | midi_channel: u8, 23 | scale: Arc>, 24 | offset: Arc>, 25 | octave_offset: i32, 26 | velocity_map: Option>, 27 | ) -> Self { 28 | MidiKeys { 29 | midi_port, 30 | midi_channel, 31 | velocity_map, 32 | output_values: HashMap::new(), 33 | offset, 34 | octave_offset, 35 | scale, 36 | } 37 | } 38 | 39 | pub fn scale(&self) -> std::sync::MutexGuard<'_, Scale> { 40 | self.scale.lock().unwrap() 41 | } 42 | } 43 | 44 | fn get_note_id( 45 | id: u32, 46 | scale: &Arc>, 47 | offset: &Arc>, 48 | octave_offset: i32, 49 | ) -> u8 { 50 | let scale = scale.lock().unwrap(); 51 | let offset = offset.lock().unwrap(); 52 | let mut scale_offset = offset.base + offset.offset; 53 | 54 | let col = (id % 8) as i32; 55 | 56 | // hacky chord inversions 57 | if offset.offset > -7 && offset.offset < 7 { 58 | if col + offset.offset > 8 { 59 | scale_offset -= 7 60 | } else if col + offset.offset < 0 { 61 | scale_offset += 7 62 | } 63 | } 64 | 65 | (scale.get_note_at((id as i32) + scale_offset) + offset.pitch + (octave_offset * 12)) as u8 66 | } 67 | 68 | impl Triggerable for MidiKeys { 69 | fn trigger(&mut self, id: u32, value: OutputValue) { 70 | match value { 71 | OutputValue::Off => { 72 | if self.output_values.contains_key(&id) { 73 | let (note_id, _) = *self.output_values.get(&id).unwrap(); 74 | 75 | self.midi_port 76 | .send(&[144 + self.midi_channel - 1, note_id, 0]) 77 | .unwrap(); 78 | self.output_values.remove(&id); 79 | } 80 | } 81 | OutputValue::On(velocity) => { 82 | let note_id = get_note_id(id, &self.scale, &self.offset, self.octave_offset); 83 | let velocity = ::devices::map_velocity(&self.velocity_map, velocity); 84 | 85 | self.midi_port 86 | .send(&[144 + self.midi_channel - 1, note_id, velocity]) 87 | .unwrap(); 88 | self.output_values.insert(id, (note_id, velocity)); 89 | } 90 | } 91 | } 92 | 93 | fn on_tick(&mut self, _: MidiTime) { 94 | let mut to_update = HashMap::new(); 95 | 96 | for (id, (note_id, velocity)) in &self.output_values { 97 | let new_note_id = get_note_id(*id, &self.scale, &self.offset, self.octave_offset); 98 | if note_id != &new_note_id { 99 | self.midi_port 100 | .send(&[144 + self.midi_channel - 1, *note_id, 0]) 101 | .unwrap(); 102 | to_update.insert(id.clone(), (new_note_id, velocity.clone())); 103 | } 104 | } 105 | 106 | for (id, item) in to_update { 107 | self.midi_port 108 | .send(&[144 + self.midi_channel - 1, item.0, item.1]) 109 | .unwrap(); 110 | self.output_values.insert(id, item); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/devices/midi_triggers.rs: -------------------------------------------------------------------------------- 1 | use chunk::{MidiTime, OutputValue, Triggerable}; 2 | use midi_connection; 3 | 4 | use std::{ 5 | collections::HashMap, 6 | sync::{Arc, Mutex}, 7 | }; 8 | 9 | use crate::loop_grid_launchpad::LoopGridParams; 10 | 11 | pub struct MidiTriggers { 12 | midi_port: midi_connection::SharedMidiOutputConnection, 13 | midi_channel: u8, 14 | sidechain_output: Option, 15 | last_pos: MidiTime, 16 | velocity_map: Option>, 17 | output_values: HashMap, 18 | trigger_ids: Vec, 19 | } 20 | 21 | pub struct SidechainOutput { 22 | pub params: Arc>, 23 | pub id: u32, 24 | } 25 | 26 | impl MidiTriggers { 27 | pub fn new( 28 | midi_port: midi_connection::SharedMidiOutputConnection, 29 | channel: u8, 30 | sidechain_output: Option, 31 | trigger_ids: Vec, 32 | velocity_map: Option>, 33 | ) -> Self { 34 | MidiTriggers { 35 | midi_port, 36 | last_pos: MidiTime::zero(), 37 | sidechain_output, 38 | midi_channel: channel, 39 | output_values: HashMap::new(), 40 | velocity_map, 41 | trigger_ids, 42 | } 43 | } 44 | } 45 | 46 | impl Triggerable for MidiTriggers { 47 | fn on_tick(&mut self, time: MidiTime) { 48 | self.last_pos = time; 49 | } 50 | 51 | fn trigger(&mut self, id: u32, value: OutputValue) { 52 | match value { 53 | OutputValue::Off => { 54 | if self.output_values.contains_key(&id) { 55 | let (channel, note_id, _) = *self.output_values.get(&id).unwrap(); 56 | self.midi_port 57 | .send(&[144 - 1 + channel, note_id, 0]) 58 | .unwrap(); 59 | self.output_values.remove(&id); 60 | } 61 | } 62 | OutputValue::On(velocity) => { 63 | let channel = self.midi_channel; 64 | let note_id = self.trigger_ids[id as usize % self.trigger_ids.len()]; 65 | let velocity = ::devices::map_velocity(&self.velocity_map, velocity); 66 | 67 | // send note 68 | self.midi_port 69 | .send(&[144 - 1 + channel, note_id, velocity]) 70 | .unwrap(); 71 | 72 | // send sync if kick 73 | if let Some(sidechain_output) = &mut self.sidechain_output { 74 | if id == sidechain_output.id { 75 | let mut params = sidechain_output.params.lock().unwrap(); 76 | params.duck_triggered = true; 77 | } 78 | } 79 | 80 | self.output_values.insert(id, (channel, note_id, velocity)); 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/devices/mod.rs: -------------------------------------------------------------------------------- 1 | mod midi_triggers; 2 | mod midi_keys; 3 | mod offset; 4 | mod pitch_offset_chunk; 5 | mod root_select; 6 | mod root_offset_chunk; 7 | mod scale_offset_chunk; 8 | mod scale_select; 9 | mod multi; 10 | 11 | pub use self::midi_triggers::MidiTriggers; 12 | pub use self::midi_triggers::SidechainOutput; 13 | pub use self::multi::MultiChunk; 14 | 15 | pub use self::midi_keys::MidiKeys; 16 | pub use self::offset::OffsetChunk; 17 | pub use self::pitch_offset_chunk::PitchOffsetChunk; 18 | pub use self::root_select::RootSelect; 19 | pub use self::root_offset_chunk::RootOffsetChunk; 20 | pub use self::scale_offset_chunk::ScaleOffsetChunk; 21 | pub use self::scale_select::ScaleSelect; 22 | 23 | pub fn map_velocity (velocity_map: &Option>, velocity: u8) -> u8 { 24 | if let Some(velocity_map) = velocity_map { 25 | if velocity_map.len() > 0 { 26 | let group_size = 128 / velocity_map.len(); 27 | let index = (velocity as usize / group_size).min(velocity_map.len() - 1); 28 | return velocity_map[index] 29 | } 30 | } 31 | 32 | velocity 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use super::*; 38 | 39 | #[test] 40 | fn test_map_velocity () { 41 | assert_eq!(map_velocity(&Some(vec![0, 100]), 0), 0); 42 | assert_eq!(map_velocity(&Some(vec![0, 100]), 63), 0); 43 | assert_eq!(map_velocity(&Some(vec![0, 100]), 64), 100); 44 | assert_eq!(map_velocity(&Some(vec![0, 100]), 127), 100); 45 | 46 | assert_eq!(map_velocity(&Some(vec![0, 100, 127]), 0), 0); 47 | assert_eq!(map_velocity(&Some(vec![0, 100, 127]), 41), 0); 48 | assert_eq!(map_velocity(&Some(vec![0, 100, 127]), 42), 100); 49 | assert_eq!(map_velocity(&Some(vec![0, 100, 127]), 83), 100); 50 | assert_eq!(map_velocity(&Some(vec![0, 100, 127]), 84), 127); 51 | assert_eq!(map_velocity(&Some(vec![0, 100, 127]), 127), 127); 52 | } 53 | } -------------------------------------------------------------------------------- /src/devices/multi.rs: -------------------------------------------------------------------------------- 1 | use ::chunk::{Triggerable, OutputValue, MidiTime, LatchMode, ScheduleMode}; 2 | use std::collections::HashSet; 3 | 4 | pub use ::scale::{Scale, Offset}; 5 | 6 | pub struct MultiChunk { 7 | chunks: Vec> 8 | } 9 | 10 | impl MultiChunk { 11 | pub fn new (chunks: Vec>) -> Self { 12 | MultiChunk { 13 | chunks 14 | } 15 | } 16 | } 17 | 18 | impl Triggerable for MultiChunk { 19 | fn trigger (&mut self, id: u32, value: OutputValue) { 20 | for chunk in self.chunks.iter_mut() { 21 | chunk.trigger(id, value); 22 | } 23 | } 24 | 25 | fn on_tick (&mut self, time: MidiTime) { 26 | for chunk in self.chunks.iter_mut() { 27 | chunk.on_tick(time); 28 | } 29 | } 30 | 31 | // pass thru to first chunk 32 | fn get_active (&self) -> Option> { 33 | self.chunks[0].get_active() 34 | } 35 | fn latch_mode (&self) -> LatchMode { 36 | self.chunks[0].latch_mode() 37 | } 38 | fn schedule_mode (&self) -> ScheduleMode { 39 | self.chunks[0].schedule_mode() 40 | } 41 | } -------------------------------------------------------------------------------- /src/devices/offset.rs: -------------------------------------------------------------------------------- 1 | use chunk::{OutputValue, Triggerable}; 2 | use devices::midi_keys::Offset; 3 | use std::sync::{Arc, Mutex}; 4 | 5 | use std::collections::HashMap; 6 | 7 | pub struct OffsetChunk { 8 | offset: Arc>, 9 | output_values: HashMap, 10 | } 11 | 12 | const OFFSETS: [i32; 8] = [-4, -3, -2, -1, 1, 2, 3, 4]; 13 | 14 | impl OffsetChunk { 15 | pub fn new(offset: Arc>) -> Self { 16 | OffsetChunk { 17 | offset, 18 | output_values: HashMap::new(), 19 | } 20 | } 21 | } 22 | 23 | impl Triggerable for OffsetChunk { 24 | fn trigger(&mut self, id: u32, value: OutputValue) { 25 | match value { 26 | OutputValue::Off => { 27 | self.output_values.remove(&id); 28 | } 29 | OutputValue::On(_velocity) => { 30 | let offset = OFFSETS[id as usize % OFFSETS.len()]; 31 | self.output_values.insert(id, offset); 32 | } 33 | } 34 | 35 | let mut current = self.offset.lock().unwrap(); 36 | current.offset = self.output_values.values().sum(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/devices/pitch_offset_chunk.rs: -------------------------------------------------------------------------------- 1 | use chunk::{OutputValue, Triggerable}; 2 | pub use midi_connection::SharedMidiOutputConnection; 3 | 4 | use std::collections::HashMap; 5 | 6 | pub struct PitchOffsetChunk { 7 | midi_output: SharedMidiOutputConnection, 8 | channel: u8, 9 | output_values: HashMap, 10 | } 11 | 12 | const OFFSETS: [i32; 8] = [-4, -3, -2, -1, 1, 2, 3, 4]; 13 | 14 | impl PitchOffsetChunk { 15 | pub fn new(midi_output: SharedMidiOutputConnection, channel: u8) -> Self { 16 | PitchOffsetChunk { 17 | midi_output, 18 | channel, 19 | output_values: HashMap::new(), 20 | } 21 | } 22 | } 23 | 24 | impl Triggerable for PitchOffsetChunk { 25 | fn trigger(&mut self, id: u32, value: OutputValue) { 26 | match value { 27 | OutputValue::Off => { 28 | self.output_values.remove(&id); 29 | } 30 | OutputValue::On(_velocity) => { 31 | let offset = OFFSETS[id as usize % OFFSETS.len()]; 32 | self.output_values.insert(id, offset); 33 | } 34 | } 35 | 36 | let result: i32 = self.output_values.values().sum(); 37 | let msb_lsb = polar_to_msb_lsb(result as f32 / 12.0); 38 | self.midi_output 39 | .send(&[224 + self.channel - 1, msb_lsb.0, msb_lsb.1]) 40 | .unwrap(); 41 | } 42 | } 43 | 44 | pub fn polar_to_msb_lsb(input: f32) -> (u8, u8) { 45 | let max = (2.0f32).powf(14.0) / 2.0; 46 | let input_14bit = (input.max(-1.0).min(1.0) * max + max) as u16; 47 | 48 | let lsb = mask7(input_14bit as u8); 49 | let msb = mask7((input_14bit >> 7) as u8); 50 | (lsb, msb) 51 | } 52 | 53 | /// 7 bit mask 54 | #[inline(always)] 55 | pub fn mask7(input: u8) -> u8 { 56 | input & 0b01111111 57 | } 58 | -------------------------------------------------------------------------------- /src/devices/root_offset_chunk.rs: -------------------------------------------------------------------------------- 1 | use chunk::{OutputValue, Triggerable}; 2 | use scale::Scale; 3 | use std::sync::{Arc, Mutex}; 4 | 5 | use std::collections::HashMap; 6 | 7 | pub struct RootOffsetChunk { 8 | scale: Arc>, 9 | output_values: HashMap, 10 | } 11 | 12 | const OFFSETS: [i32; 8] = [-4, -3, -2, -1, 1, 2, 3, 4]; 13 | 14 | impl RootOffsetChunk { 15 | pub fn new(scale: Arc>) -> Self { 16 | RootOffsetChunk { 17 | scale, 18 | output_values: HashMap::new(), 19 | } 20 | } 21 | } 22 | 23 | impl Triggerable for RootOffsetChunk { 24 | fn trigger(&mut self, id: u32, value: OutputValue) { 25 | match value { 26 | OutputValue::Off => { 27 | self.output_values.remove(&id); 28 | } 29 | OutputValue::On(_velocity) => { 30 | let offset = OFFSETS[id as usize % OFFSETS.len()]; 31 | self.output_values.insert(id, offset); 32 | } 33 | } 34 | 35 | let mut scale = self.scale.lock().unwrap(); 36 | scale.offset = self.output_values.values().sum(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/devices/root_select.rs: -------------------------------------------------------------------------------- 1 | 2 | use ::indexmap::IndexSet; 3 | 4 | use std::sync::{Arc, Mutex}; 5 | use ::chunk::{Triggerable, OutputValue, ScheduleMode, LatchMode, MidiTime}; 6 | use std::collections::HashSet; 7 | use ::controllers::Modulator; 8 | 9 | pub use ::scale::{Scale, Offset}; 10 | 11 | pub struct RootSelect { 12 | stack: IndexSet, 13 | scale: Arc>, 14 | modulators: Vec> 15 | } 16 | 17 | impl RootSelect { 18 | pub fn new (scale: Arc>, modulators: Vec>) -> Self { 19 | RootSelect { 20 | scale, 21 | modulators, 22 | stack: IndexSet::new() 23 | } 24 | } 25 | 26 | fn refresh_output (&mut self) { 27 | if let Some(id) = self.stack.last().cloned() { 28 | let mut current_scale = self.scale.lock().unwrap(); 29 | current_scale.root = 52 + (id as i32); 30 | 31 | for modulator in &mut self.modulators { 32 | let pitch_mod = (id as f64 - 8.0) / 12.0; 33 | if let Some(modulator) = modulator { 34 | modulator.send_polar(pitch_mod); 35 | } 36 | } 37 | } 38 | } 39 | } 40 | 41 | impl Triggerable for RootSelect { 42 | fn trigger (&mut self, id: u32, value: OutputValue) { 43 | match value { 44 | OutputValue::Off => { 45 | self.stack.shift_remove(&id); 46 | self.refresh_output(); 47 | }, 48 | OutputValue::On(_velocity) => { 49 | self.stack.insert(id); 50 | self.refresh_output(); 51 | } 52 | } 53 | } 54 | 55 | fn on_tick (&mut self, time: MidiTime) { 56 | if time.is_whole_beat() { 57 | self.refresh_output(); 58 | } 59 | } 60 | 61 | fn get_active (&self) -> Option> { 62 | let current_scale = self.scale.lock().unwrap(); 63 | 64 | let mut result = HashSet::new(); 65 | if current_scale.root >= 52 { 66 | result.insert(current_scale.root as u32 - 52); 67 | } 68 | 69 | Some(result) 70 | } 71 | 72 | fn latch_mode (&self) -> LatchMode { LatchMode::NoSuppress } 73 | fn schedule_mode (&self) -> ScheduleMode { ScheduleMode::Monophonic } 74 | } -------------------------------------------------------------------------------- /src/devices/scale_offset_chunk.rs: -------------------------------------------------------------------------------- 1 | use chunk::{OutputValue, Triggerable}; 2 | use scale::Scale; 3 | use std::sync::{Arc, Mutex}; 4 | 5 | use std::collections::HashMap; 6 | 7 | pub struct ScaleOffsetChunk { 8 | scale: Arc>, 9 | output_values: HashMap, 10 | } 11 | 12 | const OFFSETS: [i32; 8] = [-4, -3, -2, -1, 1, 2, 3, 4]; 13 | 14 | impl ScaleOffsetChunk { 15 | pub fn new(scale: Arc>) -> Self { 16 | ScaleOffsetChunk { 17 | scale, 18 | output_values: HashMap::new(), 19 | } 20 | } 21 | } 22 | 23 | impl Triggerable for ScaleOffsetChunk { 24 | fn trigger(&mut self, id: u32, value: OutputValue) { 25 | match value { 26 | OutputValue::Off => { 27 | self.output_values.remove(&id); 28 | } 29 | OutputValue::On(_velocity) => { 30 | let offset = OFFSETS[id as usize % OFFSETS.len()]; 31 | self.output_values.insert(id, offset); 32 | } 33 | } 34 | 35 | let mut scale = self.scale.lock().unwrap(); 36 | scale.scale = modulo(self.output_values.values().sum(), 7); 37 | } 38 | } 39 | 40 | fn modulo(n: i32, m: i32) -> i32 { 41 | ((n % m) + m) % m 42 | } 43 | -------------------------------------------------------------------------------- /src/devices/scale_select.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexSet; 2 | 3 | use chunk::{LatchMode, OutputValue, ScheduleMode, Triggerable}; 4 | use std::collections::HashSet; 5 | use std::sync::{Arc, Mutex}; 6 | 7 | pub use scale::{Offset, Scale}; 8 | 9 | pub struct ScaleSelect { 10 | scale: Arc>, 11 | stack: IndexSet, 12 | } 13 | 14 | impl ScaleSelect { 15 | pub fn new(scale: Arc>) -> Self { 16 | ScaleSelect { 17 | scale, 18 | stack: IndexSet::new(), 19 | } 20 | } 21 | 22 | fn refresh_output(&mut self) { 23 | if let Some(id) = self.stack.last().cloned() { 24 | let mut current_scale = self.scale.lock().unwrap(); 25 | current_scale.scale = id as i32; 26 | } 27 | } 28 | } 29 | 30 | impl Triggerable for ScaleSelect { 31 | fn trigger(&mut self, id: u32, value: OutputValue) { 32 | match value { 33 | OutputValue::Off => { 34 | self.stack.shift_remove(&id); 35 | self.refresh_output(); 36 | } 37 | OutputValue::On(_velocity) => { 38 | self.stack.insert(id); 39 | self.refresh_output(); 40 | } 41 | } 42 | } 43 | 44 | fn get_active(&self) -> Option> { 45 | let current_scale = self.scale.lock().unwrap(); 46 | 47 | let mut result = HashSet::new(); 48 | if current_scale.scale >= 0 { 49 | result.insert(current_scale.scale as u32); 50 | } 51 | Some(result) 52 | } 53 | 54 | fn latch_mode(&self) -> LatchMode { 55 | LatchMode::NoSuppress 56 | } 57 | fn schedule_mode(&self) -> ScheduleMode { 58 | ScheduleMode::Monophonic 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/lfo.rs: -------------------------------------------------------------------------------- 1 | use ::MidiTime; 2 | 3 | lazy_static! { 4 | static ref RATES: [MidiTime; 10] = [ 5 | MidiTime::from_measure(3, 1), 6 | MidiTime::from_measure(2, 1), 7 | MidiTime::from_measure(3, 2), 8 | MidiTime::from_measure(1, 1), 9 | MidiTime::from_measure(2, 3), 10 | MidiTime::from_measure(1, 2), 11 | MidiTime::from_measure(1, 3), 12 | MidiTime::from_measure(1, 4), 13 | MidiTime::from_measure(1, 6), 14 | MidiTime::from_measure(1, 8) 15 | ]; 16 | } 17 | 18 | // midi 0-127 for all values 19 | pub struct Lfo { 20 | pub skew: u8, 21 | pub hold: u8, 22 | pub speed: u8, 23 | pub offset: u8 24 | } 25 | 26 | impl Lfo { 27 | // Returns a value between 0 and 1 28 | pub fn new () -> Self { 29 | Lfo { 30 | skew: 0, 31 | hold: 0, 32 | speed: 50, 33 | offset: 64 34 | } 35 | } 36 | pub fn get_value_at (&self, pos: MidiTime) -> f64 { 37 | let rate_index = (self.speed as f64 * (RATES.len() as f64 / 128.0)) as usize; 38 | let cycle_duration = RATES[rate_index]; 39 | let offset = MidiTime::from_float(cycle_duration.as_float() * ((self.offset as f64 - 64.0) / 64.0) / 2.0); 40 | let phase = ((pos + offset) % cycle_duration).as_float() / cycle_duration.as_float(); 41 | let mid = self.skew as f64 / 127.0; 42 | let hold = self.hold as f64 / 127.0; 43 | if mid <= 0.0 { 44 | 1.0 - get_held_pos(phase, hold) 45 | } else if phase < mid { 46 | get_held_pos(phase / mid, hold) 47 | } else { 48 | 1.0 - get_held_pos((phase - mid) / (1.0 - mid), hold) 49 | } 50 | } 51 | } 52 | 53 | fn get_held_pos (pos: f64, hold: f64) -> f64 { 54 | if pos < (1.0 - hold) { 55 | pos / (1.0 - hold) 56 | } else { 57 | 1.0 58 | } 59 | } -------------------------------------------------------------------------------- /src/loop_event.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | use ::output_value::OutputValue; 3 | use ::midi_time::MidiTime; 4 | 5 | #[derive(Eq, Debug, Copy, Clone)] 6 | pub struct LoopEvent { 7 | pub value: OutputValue, 8 | pub pos: MidiTime, 9 | pub id: u32 10 | } 11 | 12 | impl LoopEvent { 13 | pub fn is_on (&self) -> bool { 14 | self.value.is_on() 15 | } 16 | 17 | pub fn with_pos (&self, new_pos: MidiTime) -> LoopEvent { 18 | LoopEvent { 19 | id: self.id, 20 | value: self.value.clone(), 21 | pos: new_pos 22 | } 23 | } 24 | 25 | pub fn insert_into (self, target: &mut Vec) { 26 | match target.binary_search_by(|v| v.cmp(&self)) { 27 | Ok(index) => { 28 | target.push(self); 29 | // swap_remove removes at index and puts last item in its place 30 | target.swap_remove(index); 31 | }, 32 | Err(index) => target.insert(index, self) 33 | }; 34 | } 35 | 36 | pub fn range<'a> (collection: &'a [LoopEvent], start_pos: MidiTime, end_pos: MidiTime) -> &'a [LoopEvent] { 37 | let start_index = match collection.binary_search_by(|v| { 38 | if v.pos < start_pos { 39 | Ordering::Less 40 | } else { 41 | Ordering::Greater 42 | } 43 | }) { 44 | Ok(index) | Err(index) => index, 45 | }; 46 | 47 | let end_index = match collection.binary_search_by(|v| { 48 | if v.pos < end_pos { 49 | Ordering::Less 50 | } else { 51 | Ordering::Greater 52 | } 53 | }) { 54 | Ok(index) | Err(index) => index, 55 | }; 56 | 57 | &collection[start_index..start_index.max(end_index)] 58 | } 59 | 60 | pub fn at<'a> (collection: &'a [LoopEvent], pos: MidiTime) -> Option<&'a LoopEvent> { 61 | match collection.binary_search_by(|v| { 62 | v.pos.partial_cmp(&pos).unwrap() 63 | }) { 64 | Ok(index) => collection.get(index), 65 | Err(index) => if index > 0 { 66 | collection.get(index - 1) 67 | } else { 68 | None 69 | } 70 | } 71 | } 72 | } 73 | 74 | impl Ord for LoopEvent { 75 | fn cmp(&self, other: &LoopEvent) -> Ordering { 76 | // Some(self.cmp(other)) 77 | let value = self.pos.cmp(&other.pos); 78 | if self.eq(other) { 79 | // replace the item if same type, 80 | Ordering::Equal 81 | } else if value == Ordering::Equal { 82 | // or insert after if different (but same position) 83 | 84 | // insert offs after ons (by defining off after on in OutputValue) 85 | let cmp = self.value.cmp(&other.value); 86 | match cmp { 87 | Ordering::Equal => self.id.cmp(&other.id), 88 | _ => cmp 89 | } 90 | } else { 91 | value 92 | } 93 | } 94 | } 95 | 96 | impl PartialOrd for LoopEvent { 97 | fn partial_cmp(&self, other: &LoopEvent) -> Option { 98 | Some(self.cmp(other)) 99 | } 100 | } 101 | 102 | impl PartialEq for LoopEvent { 103 | fn eq(&self, other: &LoopEvent) -> bool { 104 | self.pos == other.pos && self.value == other.value && self.id == other.id 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/loop_recorder.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap}; 2 | use ::midi_time::MidiTime; 3 | pub use ::loop_event::LoopEvent; 4 | use std::collections::hash_map::Entry::{Occupied, Vacant}; 5 | 6 | pub struct LoopRecorder { 7 | per_id: HashMap> 8 | } 9 | 10 | impl LoopRecorder { 11 | pub fn new () -> Self { 12 | Self { 13 | per_id: HashMap::new() 14 | } 15 | } 16 | 17 | pub fn allocate (&mut self, id: u32, capacity: usize) { 18 | match self.per_id.entry(id) { 19 | Vacant(entry) => { 20 | entry.insert(Vec::with_capacity(capacity)); 21 | }, 22 | Occupied(entry) => { 23 | entry.into_mut().reserve(capacity); 24 | } 25 | } 26 | } 27 | 28 | pub fn add (&mut self, event: LoopEvent) { 29 | // record events per slot 30 | let collection = self.per_id.entry(event.id).or_insert(Vec::new()); 31 | event.insert_into(collection); 32 | } 33 | 34 | pub fn has_events (&self, id: u32, start_pos: MidiTime, end_pos: MidiTime) -> bool { 35 | if let Some(events) = self.get_range_for(id, start_pos, end_pos) { 36 | events.iter().any(|item| item.is_on()) 37 | } else { 38 | false 39 | } 40 | } 41 | 42 | pub fn get_range_for (&self, id: u32, start_pos: MidiTime, end_pos: MidiTime) -> Option<&[LoopEvent]> { 43 | if let Some(collection) = self.per_id.get(&id) { 44 | Some(LoopEvent::range(collection, start_pos, end_pos)) 45 | } else { 46 | None 47 | } 48 | } 49 | 50 | pub fn get_event_at (&self, id: u32, pos: MidiTime) -> Option<&LoopEvent> { 51 | if let Some(collection) = self.per_id.get(&id) { 52 | LoopEvent::at(collection, pos) 53 | } else { 54 | None 55 | } 56 | } 57 | 58 | pub fn get_next_event_at (&self, id: u32, pos: MidiTime) -> Option<&LoopEvent> { 59 | if let Some(collection) = self.per_id.get(&id) { 60 | match collection.binary_search_by(|v| { 61 | v.pos.cmp(&pos) 62 | }) { 63 | Ok(index) => { 64 | collection.get(index + 1) 65 | }, 66 | Err(index) => { 67 | collection.get(index) 68 | } 69 | } 70 | } else { 71 | None 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/loop_state.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::collections::HashSet; 3 | use std::sync::mpsc; 4 | 5 | use ::midi_time::MidiTime; 6 | pub use ::loop_transform::LoopTransform; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct LoopCollection { 10 | pub length: MidiTime, 11 | pub transforms: HashMap 12 | } 13 | 14 | #[derive(Eq, PartialEq)] 15 | pub enum LoopStateChange { 16 | Undo, Redo, Set 17 | } 18 | 19 | impl LoopCollection { 20 | pub fn new (length: MidiTime) -> LoopCollection { 21 | LoopCollection { 22 | length, 23 | transforms: HashMap::new() 24 | } 25 | } 26 | } 27 | 28 | pub struct LoopState { 29 | pub change_queue: mpsc::Receiver, 30 | change_queue_tx: mpsc::Sender, 31 | 32 | undos: Vec, 33 | redos: Vec 34 | } 35 | 36 | impl LoopState { 37 | pub fn new (default_length: MidiTime) -> LoopState { 38 | let default_loop = LoopCollection::new(default_length); 39 | let (change_queue_tx, change_queue) = mpsc::channel(); 40 | LoopState { 41 | undos: vec![default_loop], 42 | redos: Vec::new(), 43 | change_queue_tx, 44 | change_queue 45 | } 46 | } 47 | 48 | pub fn get (&self) -> &LoopCollection { 49 | &self.undos.last().unwrap() 50 | } 51 | 52 | pub fn retrieve (&self, offset: isize) -> Option<&LoopCollection> { 53 | if offset < 0 { 54 | let resolved_offset = self.undos.len() as isize - 1 + offset; 55 | if resolved_offset > 0 { 56 | self.undos.get(resolved_offset as usize) 57 | } else { 58 | None 59 | } 60 | } else if offset > 0 { 61 | let resolved_offset = self.redos.len() as isize - 1 - offset; 62 | if resolved_offset > 0 { 63 | self.redos.get(resolved_offset as usize) 64 | } else { 65 | None 66 | } 67 | } else { 68 | Some(&self.get()) 69 | } 70 | } 71 | 72 | pub fn next_index_for (&self, current_offset: isize, selection: &HashSet) -> Option { 73 | self.index_from(current_offset, 1, selection) 74 | } 75 | 76 | pub fn previous_index_for (&self, current_offset: isize, selection: &HashSet) -> Option { 77 | self.index_from(current_offset, -1, selection) 78 | } 79 | 80 | pub fn set (&mut self, value: LoopCollection) { 81 | self.undos.push(value); 82 | self.on_change(LoopStateChange::Set); 83 | } 84 | 85 | pub fn undo (&mut self) { 86 | if self.undos.len() > 1 { 87 | match self.undos.pop() { 88 | Some(value) => { 89 | self.redos.push(value); 90 | self.on_change(LoopStateChange::Undo); 91 | }, 92 | None => () 93 | }; 94 | } 95 | } 96 | 97 | pub fn redo (&mut self) { 98 | match self.redos.pop() { 99 | Some(value) => { 100 | self.undos.push(value); 101 | self.on_change(LoopStateChange::Redo); 102 | }, 103 | None => () 104 | }; 105 | } 106 | 107 | fn index_from (&self, current_offset: isize, request_offset: isize, selection: &HashSet) -> Option { 108 | if let Some(start_item) = self.retrieve(current_offset) { 109 | let mut item = Some(start_item); 110 | let mut offset = current_offset; 111 | 112 | // keep going until we run out or the transforms are different for given range 113 | while item.is_some() { 114 | offset = offset + request_offset; 115 | item = self.retrieve(offset); 116 | 117 | if let Some(item) = item { 118 | if selection.iter().any(|id| start_item.transforms.get(id) != item.transforms.get(id)) { 119 | return Some(offset) 120 | } 121 | } 122 | } 123 | } 124 | 125 | None 126 | } 127 | 128 | fn on_change (&self, change: LoopStateChange) { 129 | self.change_queue_tx.send(change).unwrap(); 130 | } 131 | } -------------------------------------------------------------------------------- /src/loop_transform.rs: -------------------------------------------------------------------------------- 1 | use ::output_value::OutputValue; 2 | use ::midi_time::MidiTime; 3 | 4 | 5 | #[derive(PartialEq, Debug, Clone)] 6 | pub enum LoopTransform { 7 | Value(OutputValue), 8 | Repeat { rate: MidiTime, offset: MidiTime, value: OutputValue }, 9 | Cycle { rate: MidiTime, offset: MidiTime, value: OutputValue }, 10 | Range { pos: MidiTime, length: MidiTime }, 11 | None 12 | } 13 | 14 | impl LoopTransform { 15 | pub fn apply (&self, previous: &LoopTransform) -> LoopTransform { 16 | match self { 17 | &LoopTransform::Range {pos, length} => { 18 | match previous { 19 | &LoopTransform::Repeat {rate, offset, value} => { 20 | LoopTransform::Repeat { 21 | rate: rate.min(length), offset, value 22 | } 23 | }, 24 | &LoopTransform::Cycle {rate, offset, value} => { 25 | LoopTransform::Cycle { 26 | rate: rate.min(length), offset, value 27 | } 28 | }, 29 | &LoopTransform::Range {pos: previous_pos, length: previous_length} => { 30 | let playback_offset = previous_pos % previous_length; 31 | let playback_pos = previous_pos + ((pos - playback_offset) % previous_length); 32 | LoopTransform::Range { 33 | pos: playback_pos, 34 | length: length.min(previous_length) 35 | } 36 | }, 37 | _ => self.clone() 38 | } 39 | }, 40 | &LoopTransform::None => previous.clone(), 41 | _ => self.clone() 42 | } 43 | } 44 | 45 | pub fn is_active (&self) -> bool { 46 | match self { 47 | &LoopTransform::Value(OutputValue::Off) | &LoopTransform::None => false, 48 | _ => true 49 | } 50 | } 51 | 52 | pub fn unwrap_or<'a> (&'a self, or_value: &'a LoopTransform) -> &'a LoopTransform { 53 | if self == &LoopTransform::None { 54 | or_value 55 | } else { 56 | self 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate lazy_static; 3 | extern crate indexmap; 4 | extern crate rand; 5 | extern crate serde; 6 | extern crate serde_json; 7 | 8 | use std::collections::{HashMap, HashSet}; 9 | use std::path::Path; 10 | use std::sync::{Arc, Mutex}; 11 | use std::time::{Duration, Instant}; 12 | 13 | mod chunk; 14 | mod config; 15 | mod controllers; 16 | mod devices; 17 | mod lfo; 18 | mod loop_event; 19 | mod loop_grid_launchpad; 20 | mod loop_recorder; 21 | mod loop_state; 22 | mod loop_transform; 23 | mod midi_connection; 24 | mod midi_time; 25 | mod output_value; 26 | mod scale; 27 | mod scheduler; 28 | mod throttled_output; 29 | mod trigger_envelope; 30 | 31 | use chunk::{ChunkMap, Triggerable}; 32 | use loop_grid_launchpad::{LoopGridLaunchpad, LoopGridParams}; 33 | use midi_time::MidiTime; 34 | use scale::{Offset, Scale}; 35 | use scheduler::Scheduler; 36 | 37 | const APP_NAME: &str = "Loop Drop"; 38 | const CONFIG_FILEPATH: &str = "./loopdrop-config.json"; 39 | 40 | type PortLookup = HashMap; 41 | type OffsetLookup = HashMap>>; 42 | 43 | fn main() { 44 | let mut chunks = Vec::new(); 45 | let mut myconfig = config::Config::default(); 46 | 47 | // TODO: enable config persistence when loaded with filepath 48 | // if Path::new(CONFIG_FILEPATH).exists() { 49 | // myconfig = config::Config::read(CONFIG_FILEPATH).unwrap(); 50 | // println!("Read config from {}", CONFIG_FILEPATH); 51 | // } else { 52 | // myconfig.write(CONFIG_FILEPATH).unwrap(); 53 | // println!("Wrote config to {}", CONFIG_FILEPATH); 54 | // } 55 | 56 | let output = midi_connection::MidiOutput::new(APP_NAME).unwrap(); 57 | let input = midi_connection::MidiInput::new(APP_NAME).unwrap(); 58 | 59 | println!("Midi Outputs: {:?}", midi_connection::get_outputs(&output)); 60 | println!("Midi Inputs: {:?}", midi_connection::get_inputs(&input)); 61 | 62 | let clock_input_name = &myconfig.clock_input_port_name; 63 | 64 | let scale = Scale::new(60, 0); 65 | 66 | let params = Arc::new(Mutex::new(LoopGridParams { 67 | swing: 0.0, 68 | bank: 0, 69 | frozen: false, 70 | cueing: false, 71 | duck_triggered: false, 72 | channel_triggered: HashSet::new(), 73 | reset_automation: false, 74 | })); 75 | 76 | let launchpad_io_name = if cfg!(target_os = "linux") { 77 | "Launchpad Pro MK3" 78 | } else { 79 | "Launchpad Pro MK3 LPProMK3 MIDI" 80 | }; 81 | 82 | let mut output_ports = HashMap::new(); 83 | let mut offset_lookup = HashMap::new(); 84 | 85 | for chunk in myconfig.chunks { 86 | chunks.push(ChunkMap::new( 87 | make_device( 88 | chunk.device, 89 | &mut output_ports, 90 | &mut offset_lookup, 91 | &scale, 92 | ¶ms, 93 | ), 94 | chunk.coords, 95 | chunk.shape, 96 | chunk.color, 97 | chunk.channel, 98 | chunk.repeat_mode, 99 | )) 100 | } 101 | 102 | let mut launchpad = LoopGridLaunchpad::new(launchpad_io_name, chunks, Arc::clone(¶ms)); 103 | 104 | let mut controller_references: Vec> = Vec::new(); 105 | 106 | for controller in myconfig.controllers { 107 | controller_references.push(match controller { 108 | config::ControllerConfig::Twister { 109 | port_name, 110 | mixer_port, 111 | modulators, 112 | } => Box::new(controllers::Twister::new( 113 | &port_name, 114 | get_port(&mut output_ports, &mixer_port.name), 115 | mixer_port.channel, 116 | resolve_modulators(&mut output_ports, &modulators), 117 | Arc::clone(¶ms), 118 | )), 119 | config::ControllerConfig::Umi3 { port_name } => Box::new(controllers::Umi3::new( 120 | &port_name, 121 | launchpad.remote_tx.clone(), 122 | )), 123 | config::ControllerConfig::VT4Key { output } => { 124 | let device_port = get_port(&mut output_ports, &output.name); 125 | Box::new(controllers::VT4Key::new( 126 | device_port, 127 | output.channel, 128 | scale.clone(), 129 | )) 130 | } 131 | config::ControllerConfig::ClockPulse { output, divider } => { 132 | let device_port = get_port(&mut output_ports, &output.name); 133 | Box::new(controllers::ClockPulse::new( 134 | device_port, 135 | output.channel, 136 | divider, 137 | )) 138 | } 139 | config::ControllerConfig::Init { modulators } => Box::new(controllers::Init::new( 140 | resolve_modulators(&mut output_ports, &modulators), 141 | )), 142 | }) 143 | } 144 | 145 | let mut clock_outputs: Vec = Vec::new(); 146 | for name in myconfig.clock_output_port_names { 147 | clock_outputs.push(get_port(&mut output_ports, &name)) 148 | } 149 | 150 | let mut keep_alive_outputs: Vec = Vec::new(); 151 | for name in myconfig.keep_alive_port_names { 152 | keep_alive_outputs.push(get_port(&mut output_ports, &name)) 153 | } 154 | 155 | let mut resync_outputs: Vec = Vec::new(); 156 | for name in myconfig.resync_port_names { 157 | resync_outputs.push(get_port(&mut output_ports, &name)) 158 | } 159 | 160 | for range in Scheduler::start(clock_input_name) { 161 | // sending clock is the highest priority, so lets do these first 162 | if range.ticked { 163 | if (range.tick_pos - MidiTime::tick()) % MidiTime::from_beats(32) == MidiTime::zero() { 164 | for output in &mut resync_outputs { 165 | output.send(&[250]).unwrap(); 166 | } 167 | } 168 | 169 | for output in &mut clock_outputs { 170 | output.send(&[248]).unwrap(); 171 | } 172 | } 173 | 174 | // if range.ticked && range.from.ticks() != range.to.ticks() { 175 | // // HACK: straighten out missing sub ticks into separate schedules 176 | // let mut a = range.clone(); 177 | // a.to = MidiTime::new(a.to.ticks(), 0); 178 | // a.tick_pos = MidiTime::new(a.from.ticks(), 0); 179 | // a.ticked = false; 180 | // let mut b = range.clone(); 181 | // b.from = MidiTime::new(b.to.ticks(), 0); 182 | // launchpad.schedule(a); 183 | // launchpad.schedule(b); 184 | // } else { 185 | let start = Instant::now(); 186 | launchpad.schedule(range); 187 | if start.elapsed() > Duration::from_millis(15) { 188 | println!("[WARN] SCHEDULE TIME {:?}", start.elapsed()); 189 | } 190 | // } 191 | 192 | // now for the lower priority stuff 193 | if range.ticked { 194 | let length = MidiTime::tick(); 195 | for controller in &mut controller_references { 196 | controller.schedule(range.tick_pos, length) 197 | } 198 | 199 | for output in &mut keep_alive_outputs { 200 | output.send(&[254]).unwrap(); 201 | } 202 | } 203 | } 204 | } 205 | 206 | // Helper functions 207 | fn resolve_modulators( 208 | output_ports: &mut PortLookup, 209 | modulators: &Vec>, 210 | ) -> Vec> { 211 | modulators 212 | .iter() 213 | .map(|modulator| { 214 | if let Some(modulator) = modulator { 215 | Some(controllers::Modulator { 216 | port: get_port(output_ports, &modulator.port.name), 217 | channel: modulator.port.channel, 218 | rx_port: modulator.rx_port.clone(), 219 | modulator: modulator.modulator.clone(), 220 | }) 221 | } else { 222 | None 223 | } 224 | }) 225 | .collect() 226 | } 227 | 228 | fn get_port( 229 | ports_lookup: &mut PortLookup, 230 | port_name: &str, 231 | ) -> midi_connection::SharedMidiOutputConnection { 232 | if !ports_lookup.contains_key(port_name) { 233 | ports_lookup.insert( 234 | String::from(port_name), 235 | midi_connection::get_shared_output(port_name), 236 | ); 237 | } 238 | 239 | ports_lookup.get(port_name).unwrap().clone() 240 | } 241 | 242 | fn get_offset(offset_lookup: &mut OffsetLookup, id: &str) -> Arc> { 243 | if !offset_lookup.contains_key(id) { 244 | offset_lookup.insert(String::from(id), Offset::new(0)); 245 | } 246 | 247 | offset_lookup.get(id).unwrap().clone() 248 | } 249 | 250 | fn set_offset(offset: Arc>, note_offset: &i32) { 251 | let mut value = offset.lock().unwrap(); 252 | 253 | value.base = *note_offset; 254 | } 255 | 256 | fn make_device( 257 | device: config::DeviceConfig, 258 | output_ports: &mut PortLookup, 259 | offset_lookup: &mut OffsetLookup, 260 | scale: &Arc>, 261 | params: &Arc>, 262 | ) -> Box { 263 | let mut output_ports = output_ports; 264 | let mut offset_lookup = offset_lookup; 265 | 266 | match device { 267 | config::DeviceConfig::Multi { devices } => { 268 | let instances = devices 269 | .iter() 270 | .map(|device| { 271 | make_device(device.clone(), output_ports, offset_lookup, scale, params) 272 | }) 273 | .collect(); 274 | Box::new(devices::MultiChunk::new(instances)) 275 | } 276 | config::DeviceConfig::MidiKeys { 277 | output, 278 | offset_id, 279 | note_offset, 280 | octave_offset, 281 | velocity_map, 282 | } => { 283 | let device_port = get_port(&mut output_ports, &output.name); 284 | let offset = get_offset(&mut offset_lookup, &offset_id); 285 | set_offset(offset.clone(), ¬e_offset); 286 | 287 | Box::new(devices::MidiKeys::new( 288 | device_port, 289 | output.channel, 290 | scale.clone(), 291 | offset, 292 | octave_offset, 293 | velocity_map, 294 | )) 295 | } 296 | config::DeviceConfig::OffsetChunk { id } => Box::new(devices::OffsetChunk::new( 297 | get_offset(&mut offset_lookup, &id), 298 | )), 299 | config::DeviceConfig::RootSelect { output_modulators } => { 300 | Box::new(devices::RootSelect::new( 301 | scale.clone(), 302 | resolve_modulators(&mut output_ports, &output_modulators), 303 | )) 304 | } 305 | config::DeviceConfig::ScaleSelect => Box::new(devices::ScaleSelect::new(scale.clone())), 306 | config::DeviceConfig::PitchOffsetChunk { output } => { 307 | Box::new(devices::PitchOffsetChunk::new( 308 | get_port(&mut output_ports, &output.name), 309 | output.channel, 310 | )) 311 | } 312 | config::DeviceConfig::MidiTriggers { 313 | output, 314 | sidechain_output, 315 | trigger_ids, 316 | velocity_map, 317 | } => { 318 | let device_port = get_port(&mut output_ports, &output.name); 319 | 320 | let sidechain_output = if let Some(sidechain_output) = sidechain_output { 321 | Some(devices::SidechainOutput { 322 | params: Arc::clone(params), 323 | id: sidechain_output.id, 324 | }) 325 | } else { 326 | None 327 | }; 328 | 329 | Box::new(devices::MidiTriggers::new( 330 | device_port, 331 | output.channel, 332 | sidechain_output, 333 | trigger_ids, 334 | velocity_map, 335 | )) 336 | } 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /src/midi_connection.rs: -------------------------------------------------------------------------------- 1 | extern crate midir; 2 | extern crate regex; 3 | 4 | pub use self::midir::{ 5 | ConnectError, ConnectErrorKind, MidiInput, MidiInputConnection, MidiOutput, 6 | MidiOutputConnection, PortInfoError, SendError, 7 | }; 8 | use self::regex::Regex; 9 | use std::collections::HashMap; 10 | use std::sync::{mpsc, Arc, Mutex}; 11 | 12 | use std::thread; 13 | use std::time::Duration; 14 | pub use std::time::SystemTime; 15 | type Listener = Box; 16 | 17 | const APP_NAME: &str = "Loop Drop"; 18 | 19 | struct OutputState { 20 | port: Option, 21 | listeners: Vec, 22 | current_values: HashMap<(u8, u8), u8>, 23 | } 24 | 25 | impl OutputState { 26 | fn notify_listeners(&mut self) { 27 | if let Some(ref mut port) = self.port { 28 | for listener in &self.listeners { 29 | listener(port) 30 | } 31 | } 32 | } 33 | 34 | fn resend(&mut self) { 35 | if let Some(ref mut port) = self.port { 36 | for ((msg, id), value) in self.current_values.clone() { 37 | // resend 0 for CCs, but not for anything else 38 | if (msg >= 176 && msg < 192) || value > 0 { 39 | port.send(&[msg, id, value]).unwrap(); 40 | } 41 | } 42 | } 43 | } 44 | } 45 | 46 | pub fn get_shared_output(port_name: &str) -> SharedMidiOutputConnection { 47 | let state = Arc::new(Mutex::new(OutputState { 48 | port: None, 49 | listeners: Vec::new(), 50 | current_values: HashMap::new(), 51 | })); 52 | 53 | let state_l = state.clone(); 54 | let port_name_notify = String::from(port_name); 55 | let port_name_msg = String::from(port_name); 56 | 57 | let rx_state = state.clone(); 58 | 59 | // midi send queue 60 | let (tx, rx) = mpsc::sync_channel::>(256); 61 | thread::spawn(move || { 62 | for message in rx { 63 | let mut state = rx_state.lock().unwrap(); 64 | if message.len() == 3 { 65 | state 66 | .current_values 67 | .insert((message[0], message[1]), message[2]); 68 | } 69 | 70 | if let Some(ref mut port) = state.port { 71 | port.send(&message).unwrap(); 72 | } 73 | } 74 | }); 75 | 76 | // reconnect loop 77 | thread::spawn(move || { 78 | let mut has_port = false; 79 | loop { 80 | let output = MidiOutput::new(APP_NAME).unwrap(); 81 | let current_port_id = get_outputs(&output) 82 | .iter() 83 | .position(|item| item == &port_name_notify); 84 | if current_port_id.is_some() != has_port { 85 | let mut state = state_l.lock().unwrap(); 86 | state.port = get_output(&port_name_msg); 87 | state.notify_listeners(); 88 | state.resend(); 89 | has_port = state.port.is_some(); 90 | } 91 | thread::sleep(Duration::from_secs(1)); 92 | } 93 | }); 94 | 95 | SharedMidiOutputConnection { state, tx } 96 | } 97 | 98 | pub fn get_input(port_name: &str, callback: F) -> ThreadReference 99 | where 100 | F: FnMut(u64, &[u8]) + Send + 'static, 101 | { 102 | let port_name_notify = String::from(port_name); 103 | let (tx, rx) = mpsc::channel::(); 104 | 105 | thread::spawn(move || { 106 | let mut callback = callback; 107 | for msg in rx { 108 | callback(msg.stamp, &msg.data) 109 | } 110 | }); 111 | 112 | thread::spawn(move || { 113 | let mut last_port = None; 114 | let mut current_input: Option> = None; 115 | loop { 116 | let input = MidiInput::new(APP_NAME).unwrap(); 117 | let current_port = get_inputs(&input) 118 | .iter() 119 | .position(|item| item == &port_name_notify); 120 | if last_port.is_some() != current_port.is_some() { 121 | if let Some(current_input) = current_input { 122 | current_input.close(); 123 | } 124 | current_input = match current_port { 125 | Some(current_port) => { 126 | let tx_input = tx.clone(); 127 | 128 | input 129 | .connect( 130 | current_port, 131 | &port_name_notify, 132 | move |stamp, msg, _| { 133 | tx_input 134 | .send(MidiInputMessage { 135 | stamp, 136 | data: Vec::from(msg), 137 | }) 138 | .unwrap(); 139 | }, 140 | (), 141 | ) 142 | .ok() 143 | } 144 | None => None, 145 | }; 146 | last_port = current_port; 147 | } 148 | thread::sleep(Duration::from_secs(1)); 149 | } 150 | }); 151 | 152 | ThreadReference {} 153 | } 154 | 155 | pub fn get_output(port_name: &str) -> Option { 156 | let output = MidiOutput::new(APP_NAME).unwrap(); 157 | let port_number = match get_outputs(&output) 158 | .iter() 159 | .position(|item| item == port_name) 160 | { 161 | None => return None, 162 | Some(value) => value, 163 | }; 164 | output.connect(port_number, port_name).ok() 165 | } 166 | 167 | pub fn get_outputs(output: &MidiOutput) -> Vec { 168 | let mut result = Vec::new(); 169 | 170 | for i in 0..output.port_count() { 171 | // for some reason, sometimes the port doesn't exist -- use empty string 172 | result.push(output.port_name(i).unwrap_or(String::from(""))); 173 | } 174 | 175 | normalize_port_names(&result) 176 | } 177 | 178 | pub fn get_inputs(input: &MidiInput) -> Vec { 179 | let mut result = Vec::new(); 180 | 181 | for i in 0..input.port_count() { 182 | // for some reason, sometimes the port doesn't exist -- use empty string 183 | result.push(input.port_name(i).unwrap_or(String::from(""))); 184 | } 185 | 186 | normalize_port_names(&result) 187 | } 188 | 189 | fn normalize_port_names(names: &Vec) -> Vec { 190 | lazy_static! { 191 | static ref RE: Regex = Regex::new(r"^([0-9]- )?(.+?)( [0-9]+:([0-9]+))?$").unwrap(); 192 | } 193 | 194 | let mut result = Vec::new(); 195 | 196 | for name in names { 197 | let base_device_name = RE.replace(name, "${2}").into_owned(); 198 | let device_port_index = RE.replace(name, "${4}").parse::().unwrap_or(0); 199 | let mut device_index = 0; 200 | let mut device_name = build_name(&base_device_name, device_index, device_port_index); 201 | 202 | // find an available device name (deal with multiple devices with the same name) 203 | while result.contains(&device_name) { 204 | device_index += 1; 205 | device_name = build_name(&base_device_name, device_index, device_port_index); 206 | } 207 | 208 | result.push(device_name); 209 | } 210 | 211 | result 212 | } 213 | 214 | fn build_name(base: &str, device_id: u32, port_id: u32) -> String { 215 | let mut result = String::from(base); 216 | if device_id > 0 { 217 | result.push_str(&format!(" {}", device_id + 1)) 218 | } 219 | if port_id > 0 { 220 | result.push_str(&format!(" PORT {}", port_id + 1)) 221 | } 222 | result 223 | } 224 | 225 | #[derive(Clone)] 226 | pub struct SharedMidiOutputConnection { 227 | state: Arc>, 228 | tx: mpsc::SyncSender>, 229 | } 230 | 231 | impl SharedMidiOutputConnection { 232 | pub fn send_sync(&mut self, message: &[u8]) -> Result<(), SendError> { 233 | let mut state = self.state.lock().unwrap(); 234 | 235 | if message.len() == 3 { 236 | state 237 | .current_values 238 | .insert((message[0], message[1]), message[2]); 239 | } 240 | 241 | if let Some(ref mut port) = state.port { 242 | port.send(message) 243 | } else { 244 | Ok(()) 245 | } 246 | } 247 | 248 | // async send 249 | pub fn send(&mut self, message: &[u8]) -> Result<(), mpsc::TrySendError>> { 250 | self.tx.try_send(message.to_vec()) 251 | } 252 | 253 | pub fn on_connect(&mut self, callback: F) 254 | where 255 | F: Fn(&mut MidiOutputConnection) + Send + 'static, 256 | { 257 | let mut state = self.state.lock().unwrap(); 258 | state.listeners.push(Box::new(callback)); 259 | } 260 | } 261 | 262 | #[derive(Debug, Clone)] 263 | struct MidiInputMessage { 264 | stamp: u64, 265 | data: Vec, 266 | } 267 | 268 | pub struct ThreadReference { 269 | //tx: mpsc::Sender<()> 270 | } 271 | 272 | impl Drop for ThreadReference { 273 | fn drop(&mut self) { 274 | println!("DROP NOT IMPLEMENTED") 275 | //self.tx.send(()).unwrap(); 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/midi_time.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Add, Div, Mul, Rem, Sub}; 2 | 3 | pub const SUB_TICKS: u8 = 8; 4 | 5 | #[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)] 6 | pub struct MidiTime { 7 | ticks: i32, 8 | sub_ticks: u8, 9 | } 10 | 11 | impl MidiTime { 12 | pub fn new(ticks: i32, sub_ticks: u8) -> MidiTime { 13 | if sub_ticks >= SUB_TICKS { 14 | MidiTime { 15 | ticks, 16 | sub_ticks: 0, 17 | } + MidiTime::from_sub_ticks(sub_ticks) 18 | } else { 19 | MidiTime { ticks, sub_ticks } 20 | } 21 | } 22 | pub fn from_ticks(ticks: i32) -> MidiTime { 23 | MidiTime { 24 | ticks, 25 | sub_ticks: 0, 26 | } 27 | } 28 | 29 | pub fn from_sub_ticks(sub_ticks: u8) -> MidiTime { 30 | MidiTime { 31 | ticks: (sub_ticks / SUB_TICKS) as i32, 32 | sub_ticks: sub_ticks % SUB_TICKS, 33 | } 34 | } 35 | 36 | pub fn zero() -> MidiTime { 37 | MidiTime::from_ticks(0) 38 | } 39 | 40 | pub fn tick() -> MidiTime { 41 | MidiTime::from_ticks(1) 42 | } 43 | 44 | pub fn half_tick() -> MidiTime { 45 | MidiTime::from_sub_ticks(SUB_TICKS / 2) 46 | } 47 | 48 | pub fn from_beats(beats: i32) -> MidiTime { 49 | MidiTime::from_ticks(beats * 24) 50 | } 51 | 52 | pub fn from_measure(beats: i32, divider: i32) -> MidiTime { 53 | MidiTime::from_ticks(beats * 24 / divider) 54 | } 55 | 56 | pub fn quantize_length(length: MidiTime) -> MidiTime { 57 | let grid = get_quantize_grid(length.ticks); 58 | let result = MidiTime::from_ticks(((length.ticks as f64 / grid).round() * grid) as i32); 59 | result 60 | } 61 | 62 | pub fn half(&self) -> MidiTime { 63 | if self.ticks % 2 == 0 { 64 | MidiTime { 65 | ticks: self.ticks / 2, 66 | sub_ticks: self.sub_ticks / 2, 67 | } 68 | } else { 69 | let sub_ticks = ((self.sub_ticks / 2) as i32) + (SUB_TICKS as i32 / 2); 70 | let mut ticks = self.ticks; 71 | ticks += sub_ticks / SUB_TICKS as i32; 72 | MidiTime { 73 | ticks: ticks / 2, 74 | sub_ticks: sub_ticks as u8, 75 | } 76 | } 77 | } 78 | 79 | pub fn is_zero(&self) -> bool { 80 | self.ticks == 0 && self.sub_ticks == 0 81 | } 82 | 83 | pub fn is_whole_beat(&self) -> bool { 84 | self.sub_ticks == 0 && self.ticks % 24 == 0 85 | } 86 | 87 | pub fn beat_tick(&self) -> i32 { 88 | self.ticks % 24 89 | } 90 | 91 | pub fn ticks(&self) -> i32 { 92 | self.ticks 93 | } 94 | 95 | pub fn sub_ticks(&self) -> u8 { 96 | self.sub_ticks 97 | } 98 | 99 | pub fn as_float(&self) -> f64 { 100 | (self.ticks as f64) + self.sub_ticks_float() 101 | } 102 | 103 | pub fn sub_ticks_float(&self) -> f64 { 104 | (self.sub_ticks as f64) / SUB_TICKS as f64 105 | } 106 | 107 | pub fn from_float(float: f64) -> MidiTime { 108 | let ticks = float as i32; 109 | let sub_ticks = ((float - ticks as f64) * SUB_TICKS as f64) as u8; 110 | let result = MidiTime { ticks, sub_ticks }; 111 | result 112 | } 113 | 114 | pub fn round(&self) -> MidiTime { 115 | if self.sub_ticks < (SUB_TICKS / 2) { 116 | MidiTime { 117 | ticks: self.ticks, 118 | sub_ticks: 0, 119 | } 120 | } else { 121 | MidiTime { 122 | ticks: self.ticks + 1, 123 | sub_ticks: 0, 124 | } 125 | } 126 | } 127 | 128 | pub fn floor(&self) -> MidiTime { 129 | MidiTime::from_ticks(self.ticks) 130 | } 131 | 132 | pub fn quantize(&self, block_align: MidiTime) -> MidiTime { 133 | MidiTime::from_ticks((self.ticks() / block_align.ticks()) * block_align.ticks()) 134 | } 135 | 136 | pub fn swing(&self, amount: f64) -> MidiTime { 137 | let sixteenth = MidiTime::from_ticks(6); 138 | let root = MidiTime::from_ticks((self.ticks() / 12) * 12); 139 | let offset = *self - root; 140 | 141 | let (up, down) = swing_multipliers(amount); 142 | 143 | if offset < sixteenth { 144 | root + MidiTime::from_float(offset.as_float() * up) 145 | } else { 146 | let sixteenth_offset = offset - sixteenth; 147 | let peak = sixteenth.as_float() * up; 148 | root + MidiTime::from_float(peak + sixteenth_offset.as_float() * down) 149 | } 150 | } 151 | } 152 | 153 | fn swing_multipliers(amount: f64) -> (f64, f64) { 154 | if amount > 0.0 { 155 | (1.0 - amount, 1.0 + amount) 156 | } else { 157 | (1.0 + (amount * -1.0), 1.0 - (amount * -1.0)) 158 | } 159 | } 160 | 161 | impl Sub for MidiTime { 162 | type Output = MidiTime; 163 | 164 | fn sub(self, other: MidiTime) -> MidiTime { 165 | let ticks = if self.sub_ticks < other.sub_ticks { 166 | self.ticks - other.ticks - 1 167 | } else { 168 | self.ticks - other.ticks 169 | }; 170 | MidiTime { 171 | ticks, 172 | sub_ticks: modulo( 173 | self.sub_ticks as i32 - other.sub_ticks as i32, 174 | SUB_TICKS as i32, 175 | ) as u8, 176 | } 177 | } 178 | } 179 | 180 | impl Add for MidiTime { 181 | type Output = MidiTime; 182 | 183 | fn add(self, other: MidiTime) -> MidiTime { 184 | let ticks = if (self.sub_ticks as u32) + (other.sub_ticks as u32) >= SUB_TICKS as u32 { 185 | self.ticks + other.ticks + 1 186 | } else { 187 | self.ticks + other.ticks 188 | }; 189 | MidiTime { 190 | ticks, 191 | sub_ticks: (self.sub_ticks + other.sub_ticks) % SUB_TICKS, 192 | } 193 | } 194 | } 195 | 196 | impl Mul for MidiTime { 197 | type Output = Self; 198 | 199 | fn mul(self, rhs: i32) -> Self { 200 | MidiTime::from_ticks(self.ticks * rhs) 201 | } 202 | } 203 | 204 | impl Div for MidiTime { 205 | type Output = Self; 206 | 207 | fn div(self, rhs: i32) -> Self { 208 | MidiTime::from_ticks(self.ticks / rhs) 209 | } 210 | } 211 | 212 | impl Rem for MidiTime { 213 | type Output = MidiTime; 214 | 215 | fn rem(self, modulus: MidiTime) -> Self { 216 | // ignore sub_ticks on modulus 217 | MidiTime { 218 | ticks: modulo(self.ticks, modulus.ticks), 219 | sub_ticks: self.sub_ticks, 220 | } 221 | } 222 | } 223 | 224 | fn modulo(n: i32, m: i32) -> i32 { 225 | ((n % m) + m) % m 226 | } 227 | 228 | fn get_quantize_grid(length: i32) -> f64 { 229 | if length < 24 - 8 { 230 | 24.0 / 2.0 231 | } else if length < 24 + 16 { 232 | 24.0 233 | } else { 234 | 24.0 * 2.0 235 | } 236 | } 237 | 238 | #[cfg(test)] 239 | mod tests { 240 | use super::*; 241 | 242 | #[test] 243 | fn subtract() { 244 | let a = MidiTime { 245 | ticks: 100, 246 | sub_ticks: 5, 247 | }; 248 | let b = MidiTime { 249 | ticks: 90, 250 | sub_ticks: 4, 251 | }; 252 | let c = MidiTime { 253 | ticks: 90, 254 | sub_ticks: 6, 255 | }; 256 | assert_eq!( 257 | a - b, 258 | MidiTime { 259 | ticks: 10, 260 | sub_ticks: 1 261 | } 262 | ); 263 | assert_eq!( 264 | a - c, 265 | MidiTime { 266 | ticks: 9, 267 | sub_ticks: 7 268 | } 269 | ); 270 | assert_eq!( 271 | MidiTime::new(1, 0) - MidiTime::new(0, 1), 272 | MidiTime { 273 | ticks: 0, 274 | sub_ticks: SUB_TICKS - 1 275 | } 276 | ); 277 | } 278 | 279 | #[test] 280 | fn add() { 281 | let a = MidiTime { 282 | ticks: 100, 283 | sub_ticks: 4, 284 | }; 285 | let b = MidiTime { 286 | ticks: 50, 287 | sub_ticks: 3, 288 | }; 289 | let c = MidiTime { 290 | ticks: 50, 291 | sub_ticks: 6, 292 | }; 293 | assert_eq!( 294 | a + b, 295 | MidiTime { 296 | ticks: 150, 297 | sub_ticks: 7 298 | } 299 | ); 300 | assert_eq!( 301 | a + c, 302 | MidiTime { 303 | ticks: 151, 304 | sub_ticks: 2 305 | } 306 | ); 307 | assert_eq!( 308 | MidiTime::new(0, SUB_TICKS - 1) + MidiTime::new(0, 1), 309 | MidiTime { 310 | ticks: 1, 311 | sub_ticks: 0 312 | } 313 | ); 314 | } 315 | 316 | #[test] 317 | fn sub_tick_wrap_around() { 318 | assert_eq!( 319 | MidiTime::from_sub_ticks(SUB_TICKS), 320 | MidiTime { 321 | ticks: 1, 322 | sub_ticks: 0 323 | } 324 | ); 325 | } 326 | 327 | #[test] 328 | fn half() { 329 | // TODO: test fractions, etc 330 | let a = MidiTime::from_beats(4); 331 | assert_eq!( 332 | a.half(), 333 | MidiTime { 334 | ticks: 4 * 24 / 2, 335 | sub_ticks: 0 336 | } 337 | ); 338 | } 339 | 340 | #[test] 341 | fn swing() { 342 | assert_eq!( 343 | MidiTime::from_ticks(24).swing(0.5), 344 | MidiTime::from_ticks(24) 345 | ); 346 | assert_eq!( 347 | MidiTime::from_ticks(24 * 2).swing(0.5), 348 | MidiTime::from_ticks(24 * 2) 349 | ); 350 | assert_eq!( 351 | MidiTime::from_ticks(24 * 3).swing(0.5), 352 | MidiTime::from_ticks(24 * 3) 353 | ); 354 | 355 | // TODO: I DON'T THINK THIS IS WORKING!??? 356 | assert_eq!( 357 | MidiTime::from_ticks(24 + 6).swing(0.5), 358 | MidiTime::from_ticks(27) 359 | ); 360 | // assert_eq!(MidiTime::from_ticks(24 * 2 + 6).swing(0.5), MidiTime::from_ticks(24 * 2 + 6)); 361 | // assert_eq!(MidiTime::from_ticks(24 * 3 + 6).swing(0.5), MidiTime::from_ticks(24 * 3 + 6)); 362 | } 363 | 364 | #[test] 365 | fn float_conversion() { 366 | assert_eq!(MidiTime::new(0, 0).as_float(), 0.0); 367 | assert_eq!(MidiTime::new(0, SUB_TICKS / 2).as_float(), 0.5); 368 | assert_eq!(MidiTime::new(1, SUB_TICKS / 2).as_float(), 1.5); 369 | assert_eq!(MidiTime::new(2, 0).as_float(), 2.0); 370 | assert_eq!(MidiTime::from_float(0.0), MidiTime::new(0, 0)); 371 | assert_eq!(MidiTime::from_float(0.5), MidiTime::new(0, SUB_TICKS / 2)); 372 | assert_eq!(MidiTime::from_float(1.5), MidiTime::new(1, SUB_TICKS / 2)); 373 | assert_eq!(MidiTime::from_float(2.0), MidiTime::new(2, 0)); 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /src/output_value.rs: -------------------------------------------------------------------------------- 1 | #[derive(Ord, PartialOrd, Debug, Eq, PartialEq, Copy, Clone)] 2 | pub enum OutputValue { 3 | // Insert offs after ons when sorting 4 | On(u8), Off 5 | } 6 | 7 | impl OutputValue { 8 | pub fn is_on (&self) -> bool { 9 | match self { 10 | &OutputValue::Off => false, 11 | &OutputValue::On(_) => true 12 | } 13 | } 14 | 15 | pub fn value (&self) -> u8 { 16 | match self { 17 | &OutputValue::Off => 0, 18 | &OutputValue::On(value) => value 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/scale.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::sync::{Arc, Mutex}; 3 | 4 | #[derive(Clone, Eq, PartialEq)] 5 | pub struct Scale { 6 | pub root: i32, 7 | pub scale: i32, 8 | pub offset: i32, 9 | } 10 | 11 | impl Scale { 12 | pub fn new(root: i32, scale: i32) -> Arc> { 13 | Arc::new(Mutex::new(Scale { 14 | root, 15 | scale, 16 | offset: 0, 17 | })) 18 | } 19 | 20 | pub fn get_notes(&self) -> HashSet { 21 | let mut result = HashSet::new(); 22 | for i in -100..100 { 23 | result.insert(self.get_note_at(i)); 24 | } 25 | result 26 | } 27 | 28 | pub fn get_note_at(&self, value: i32) -> i32 { 29 | let intervals = [2, 2, 1, 2, 2, 2, 1]; 30 | let mut scale_notes = vec![0]; 31 | let mut last_value = 0; 32 | for i in 0..6 { 33 | last_value += intervals[modulo(i + self.scale, 7) as usize]; 34 | scale_notes.push(last_value); 35 | } 36 | let length = scale_notes.len() as i32; 37 | let interval = scale_notes[modulo(value, length) as usize]; 38 | let octave = (value as f64 / length as f64).floor() as i32; 39 | self.root + self.offset + (octave * 12) + interval 40 | } 41 | } 42 | 43 | fn modulo(n: i32, m: i32) -> i32 { 44 | ((n % m) + m) % m 45 | } 46 | 47 | #[derive(Clone, Eq, PartialEq)] 48 | pub struct Offset { 49 | pub base: i32, 50 | pub offset: i32, 51 | pub pitch: i32, 52 | } 53 | 54 | impl Offset { 55 | pub fn new(base: i32) -> Arc> { 56 | Arc::new(Mutex::new(Offset { 57 | offset: 0, 58 | base, 59 | pitch: 0, 60 | })) 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use super::*; 67 | 68 | #[test] 69 | fn check_major() { 70 | let scale_arc = Scale::new(0, 0); 71 | let scale = scale_arc.lock().unwrap(); 72 | let result: Vec = (-2..9).map(|i| scale.get_note_at(i)).collect(); 73 | assert_eq!(result, vec![-3, -1, 0, 2, 4, 5, 7, 9, 11, 12, 14]); 74 | } 75 | 76 | #[test] 77 | fn check_natural_minor() { 78 | let scale_arc = Scale::new(0, 5); 79 | let scale = scale_arc.lock().unwrap(); 80 | let result: Vec = (-2..9).map(|i| scale.get_note_at(i)).collect(); 81 | assert_eq!(result, vec![-4, -2, 0, 2, 3, 5, 7, 8, 10, 12, 14]); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/scheduler.rs: -------------------------------------------------------------------------------- 1 | extern crate circular_queue; 2 | use self::circular_queue::CircularQueue; 3 | 4 | use midi_connection; 5 | use std::sync::{mpsc, Arc, Mutex}; 6 | use std::thread; 7 | use std::time::{Duration, Instant}; 8 | 9 | pub use midi_time::{MidiTime, SUB_TICKS}; 10 | 11 | pub struct Scheduler { 12 | next_pos: MidiTime, 13 | last_tick_at: Instant, 14 | ticks: i32, 15 | sub_ticks: u8, 16 | rx: mpsc::Receiver, 17 | _clock_source: Option, 18 | } 19 | 20 | struct RemoteSchedulerState { 21 | tick_durations: CircularQueue, 22 | last_tick_stamp: Option, 23 | tick_start_at: Instant, 24 | stamp_offset: u64, 25 | started: bool, 26 | last_tick_at: Option, 27 | } 28 | 29 | impl RemoteSchedulerState { 30 | fn restart(&mut self, offset: u64) { 31 | self.stamp_offset = offset; 32 | self.last_tick_stamp = None; 33 | self.tick_start_at = Instant::now(); 34 | } 35 | 36 | fn tick_duration(&self) -> Duration { 37 | let sum = self.tick_durations.iter().sum::(); 38 | let count = self.tick_durations.len() as u32; 39 | if count > 1 { 40 | let average = sum.as_secs_f64() / count as f64; 41 | Duration::from_secs_f64(average) 42 | } else { 43 | Duration::from_secs_f64(0.5 / 24.0) 44 | } 45 | } 46 | 47 | fn tick(&mut self, stamp: u64) { 48 | if let Some(last_tick_stamp) = self.last_tick_stamp { 49 | let duration = Duration::from_micros(stamp - last_tick_stamp); 50 | 51 | if duration < Duration::from_millis(500) { 52 | self.tick_durations 53 | .push(Duration::from_micros(stamp - last_tick_stamp)); 54 | self.last_tick_at = 55 | Some(self.tick_start_at + Duration::from_micros(stamp - self.stamp_offset)); 56 | } else { 57 | self.restart(stamp); 58 | } 59 | } 60 | 61 | self.last_tick_stamp = Some(stamp); 62 | } 63 | } 64 | 65 | impl Scheduler { 66 | pub fn start(clock_port_name: &str) -> Self { 67 | let remote_state = Arc::new(Mutex::new(RemoteSchedulerState { 68 | tick_durations: CircularQueue::with_capacity(3), 69 | last_tick_at: None, 70 | started: false, 71 | last_tick_stamp: None, 72 | tick_start_at: Instant::now(), 73 | stamp_offset: 0, 74 | })); 75 | 76 | let (tx, rx) = mpsc::sync_channel(8); 77 | let tx_clock = tx.clone(); 78 | 79 | // track external clock and tick durations (to calculate bpm) 80 | let state_m = remote_state.clone(); 81 | let _clock_source = Some(midi_connection::get_input( 82 | clock_port_name, 83 | move |stamp, message| { 84 | if message[0] == 248 { 85 | let mut state: std::sync::MutexGuard = 86 | state_m.lock().unwrap(); 87 | 88 | // if we get a tick before clock start, treat as clock start 89 | if !state.started { 90 | state.restart(stamp); 91 | } 92 | 93 | state.tick(stamp); 94 | tx_clock.send(ScheduleTick::MidiTick).unwrap(); 95 | } else if message[0] == 250 { 96 | // play 97 | let mut state: std::sync::MutexGuard = 98 | state_m.lock().unwrap(); 99 | state.restart(stamp); 100 | } 101 | }, 102 | )); 103 | 104 | let state_s = remote_state.clone(); 105 | let tx_sub_clock = tx.clone(); 106 | // thread::spawn(move || loop { 107 | // let state: std::sync::MutexGuard = state_s.lock().unwrap(); 108 | // let duration = state.tick_duration() / (SUB_TICKS as u32); 109 | // drop(state); 110 | // thread::sleep(duration); 111 | // tx_sub_clock.send(ScheduleTick::SubTick(duration)).unwrap(); 112 | // }); 113 | 114 | Scheduler { 115 | ticks: -1, 116 | sub_ticks: 0, 117 | rx, 118 | last_tick_at: Instant::now(), 119 | next_pos: MidiTime::zero(), 120 | _clock_source, 121 | } 122 | } 123 | 124 | fn await_next(&mut self) -> ScheduleRange { 125 | loop { 126 | let msg = self.rx.recv().unwrap(); 127 | let from = self.next_pos; 128 | 129 | match msg { 130 | ScheduleTick::MidiTick => { 131 | self.last_tick_at = Instant::now(); 132 | self.sub_ticks = 0; 133 | self.ticks += 1; 134 | self.next_pos = MidiTime::new(self.ticks, self.sub_ticks); 135 | 136 | return ScheduleRange { 137 | from, 138 | to: self.next_pos, 139 | tick_pos: MidiTime::from_ticks(self.ticks), 140 | ticked: true, 141 | jumped: false, 142 | }; 143 | } 144 | ScheduleTick::SubTick(duration) => { 145 | if from.sub_ticks() < (SUB_TICKS - 1) 146 | && self.last_tick_at.elapsed() > (duration / 2) 147 | { 148 | self.sub_ticks += 1; 149 | self.next_pos = MidiTime::new(self.ticks, self.sub_ticks); 150 | return ScheduleRange { 151 | from, 152 | to: self.next_pos, 153 | tick_pos: MidiTime::from_ticks(self.ticks), 154 | ticked: false, 155 | jumped: false, 156 | }; 157 | } 158 | } 159 | }; 160 | } 161 | } 162 | } 163 | 164 | impl Iterator for Scheduler { 165 | type Item = ScheduleRange; 166 | 167 | fn next(&mut self) -> Option { 168 | Some(self.await_next()) 169 | } 170 | } 171 | 172 | #[derive(Debug, Copy, Clone)] 173 | pub struct ScheduleRange { 174 | pub from: MidiTime, 175 | pub to: MidiTime, 176 | pub tick_pos: MidiTime, 177 | pub ticked: bool, 178 | pub jumped: bool, 179 | } 180 | 181 | enum ScheduleTick { 182 | MidiTick, 183 | SubTick(Duration), 184 | } 185 | -------------------------------------------------------------------------------- /src/throttled_output.rs: -------------------------------------------------------------------------------- 1 | use ::midi_connection::SharedMidiOutputConnection; 2 | use std::collections::{HashMap, HashSet}; 3 | 4 | pub struct ThrottledOutput { 5 | midi_connection: SharedMidiOutputConnection, 6 | unsent_values: HashMap<(u8, u8), u8>, 7 | sent_keys: HashSet<(u8, u8)> 8 | } 9 | 10 | impl ThrottledOutput { 11 | pub fn new (midi_connection: SharedMidiOutputConnection) -> Self { 12 | ThrottledOutput { 13 | midi_connection, 14 | unsent_values: HashMap::new(), 15 | sent_keys: HashSet::new() 16 | } 17 | } 18 | 19 | pub fn flush (&mut self) { 20 | for ((msg, cc), value) in &self.unsent_values { 21 | self.midi_connection.send(&[*msg, *cc, *value]).unwrap(); 22 | } 23 | self.unsent_values.clear(); 24 | self.sent_keys.clear(); 25 | } 26 | 27 | pub fn send (&mut self, message: &[u8]) { 28 | if message.len() == 3 { 29 | let key = (message[0], message[1]); 30 | if self.sent_keys.contains(&key) { 31 | self.unsent_values.insert(key, message[2]); 32 | } else { 33 | self.midi_connection.send(message).unwrap(); 34 | self.sent_keys.insert(key); 35 | } 36 | } else { 37 | self.midi_connection.send(message).unwrap(); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/trigger_envelope.rs: -------------------------------------------------------------------------------- 1 | pub struct TriggerEnvelope { 2 | pub tick_multiplier: f32, 3 | pub max_tick_change: f32, 4 | value: f32, 5 | out_value: f32, 6 | } 7 | 8 | impl TriggerEnvelope { 9 | pub fn new(tick_multiplier: f32, max_tick_change: f32) -> Self { 10 | TriggerEnvelope { 11 | tick_multiplier, 12 | max_tick_change, 13 | value: 0.0, 14 | out_value: 0.0, 15 | } 16 | } 17 | 18 | pub fn value(&self) -> f32 { 19 | self.out_value.max(0.0).min(1.0) 20 | } 21 | 22 | pub fn tick(&mut self, triggered: bool) { 23 | // decay 24 | self.value = if triggered { 25 | 1.0 26 | } else if self.value > 0.0 { 27 | self.value * self.tick_multiplier 28 | } else { 29 | 0.0 30 | }; 31 | 32 | // slew limit 33 | if self.value > self.out_value { 34 | self.out_value += (self.value - self.out_value).min(self.max_tick_change); 35 | } else if self.value < self.out_value { 36 | self.out_value -= (self.out_value - self.value).min(self.max_tick_change); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /zoia/060_zoia_drums_and_bass.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/rust-loop-drop/87d6cef47a9874ffdad4a2d91bccd48fec477811/zoia/060_zoia_drums_and_bass.bin -------------------------------------------------------------------------------- /zoia/061_zoia_smplr_and_synth.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmckegg/rust-loop-drop/87d6cef47a9874ffdad4a2d91bccd48fec477811/zoia/061_zoia_smplr_and_synth.bin --------------------------------------------------------------------------------