├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Guardfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin └── xi ├── lib ├── xi.rb └── xi │ ├── bjorklund.rb │ ├── clock.rb │ ├── core_ext.rb │ ├── core_ext │ ├── array.rb │ ├── enumerable.rb │ ├── enumerator.rb │ ├── integer.rb │ ├── numeric.rb │ ├── object.rb │ ├── scalar.rb │ └── string.rb │ ├── error_log.rb │ ├── logger.rb │ ├── osc.rb │ ├── pattern.rb │ ├── pattern │ ├── generators.rb │ └── transforms.rb │ ├── repl.rb │ ├── scale.rb │ ├── step_sequencer.rb │ ├── stream.rb │ ├── supercollider.rb │ ├── supercollider │ └── stream.rb │ ├── tidal_clock.rb │ └── version.rb ├── synthdefs ├── other.scd └── superdirt.scd ├── test ├── bjorklund_test.rb ├── core_ext_test.rb ├── pattern │ ├── generators_test.rb │ └── transforms_test.rb ├── pattern_test.rb ├── test_helper.rb └── xi_test.rb └── xi.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.swp 11 | *.swo 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.4 5 | before_install: gem install bundler -v 1.17.2 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at munshkr@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in xi.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard :minitest do 2 | watch(%r{^test/(.*)_test\.rb$}) 3 | watch(%r{^lib/xi/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" } 4 | watch(%r{^test/test_helper\.rb$}) { 'test' } 5 | end 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 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 General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xi [![Build Status](https://travis-ci.org/xi-livecode/xi.svg?branch=master)](https://travis-ci.org/xi-livecode/xi) 2 | 3 | Xi is a musical pattern language inspired in Tidal and SuperCollider for 4 | building higher-level musical constructs easily. It is implemented on the Ruby 5 | programming language and uses SuperCollider as a backend. 6 | 7 | Xi is only a patterns library, but can talk to 8 | [SuperCollider](https://github.com/supercollider/supercollider) synths or MIDI 9 | devices. 10 | 11 | *NOTE*: Be advised that this project is in very early alpha stages. There are a 12 | multiple known bugs, missing features, documentation and tests. 13 | 14 | ## Example 15 | 16 | ```ruby 17 | melody = [0,3,6,7,8] 18 | scale = [Scale.iwato, Scale.jiao] 19 | 20 | fm.set degree: melody.p(1/2,1/8,1/8,1/8,1/16,1/8,1/8).seq(2), 21 | gate: :degree, 22 | detune: [0.1, 0.4, 0.4], 23 | sustain: [1,2,3,10], 24 | accelerate: [0.5, 0, 0.1], 25 | octave: [3,4,5,6].p(1/3), 26 | scale: scale.p(1/2), 27 | amp: P.new { |y| y << rand * 0.2 + 0.8 }.p(1/2) 28 | 29 | kick.set freq: s("xi.x .ix. | xi.x xx.x", 70, 200), amp: 0.8, gate: :freq 30 | 31 | clap.set n: s("..x. xyz. .x.. .xyx", 60, 61, 60).p.slow(2), 32 | gate: :n, 33 | amp: 0.35, 34 | pan: P.sin(16, 2) * 0.6, 35 | sustain: 0.25 36 | ``` 37 | 38 | ## Installation 39 | 40 | ### Quickstart 41 | 42 | You will need Ruby 2.4+ installed on your system. Check by running `ruby -v`. 43 | To install Xi you must install the core libraries and REPL, and then one or 44 | more backends. 45 | 46 | $ gem install xi-lang 47 | 48 | Available backends: 49 | 50 | * xi-midi: MIDI devices support 51 | * xi-superdirt: [SuperDirt](https://github.com/musikinformatik/SuperDirt) backend 52 | 53 | For example: 54 | 55 | $ gem install xi-lang xi-superdirt 56 | 57 | Then run Xi REPL with: 58 | 59 | $ xi 60 | 61 | There is a configuration file that is written automatically for you when run 62 | for the first time at `~/.config/xi/init.rb`. You can add require lines and 63 | define all the function helpers you want. 64 | 65 | ### Repository 66 | 67 | Becase Xi is still in **alpha** stage, you might want to clone the repository 68 | using Git instead: 69 | 70 | $ git clone https://github.com/xi-livecode/xi 71 | 72 | After that, change into the new directory and install gem dependencies with 73 | Bundler. If you don't have Bundler installed, run `gem install bundler` first. 74 | Then: 75 | 76 | $ cd xi 77 | $ bundle install 78 | $ rake install 79 | 80 | You're done! Fire up the REPL using `xi`. 81 | 82 | ## Development 83 | 84 | After checking out the repo, run `bundle` to install dependencies. Then, run 85 | `rake test` to run the tests. 86 | 87 | To install this gem onto your local machine, run `bundle exec rake install`. To 88 | release a new version, update the version number in `version.rb`, and then run 89 | `bundle exec rake release`, which will create a git tag for the version, push 90 | git commits and tags, and push the `.gem` file to 91 | [rubygems.org](https://rubygems.org). 92 | 93 | You can also try things on the REPL without installing the gem on your machine 94 | by doing `bundle exec bin/xi`. 95 | 96 | ## Contributing 97 | 98 | Bug reports and pull requests are welcome on GitHub at 99 | https://github.com/xi-livecode/xi. This project is intended to be a safe, 100 | welcoming space for collaboration, and contributors are expected to adhere to 101 | the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 102 | 103 | ## License 104 | 105 | See [LICENSE](LICENSE.txt) 106 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList['test/**/*_test.rb'] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /bin/xi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "xi" 3 | require "xi/repl" 4 | 5 | include Xi 6 | 7 | if ARGV.index('--irb') 8 | ARGV.delete('--irb') 9 | REPL.start(irb: true) 10 | else 11 | REPL.start 12 | end 13 | -------------------------------------------------------------------------------- /lib/xi.rb: -------------------------------------------------------------------------------- 1 | require "xi/version" 2 | require 'xi/core_ext' 3 | require 'xi/pattern' 4 | require 'xi/stream' 5 | require 'xi/clock' 6 | require 'xi/bjorklund' 7 | require 'xi/step_sequencer' 8 | 9 | def inf 10 | Float::INFINITY 11 | end 12 | 13 | module Xi 14 | def self.default_backend 15 | @default_backend 16 | end 17 | 18 | def self.default_backend=(new_name) 19 | @default_backend = new_name && new_name.to_sym 20 | end 21 | 22 | def self.default_clock 23 | @default_clock ||= Clock.new 24 | end 25 | 26 | def self.default_clock=(new_clock) 27 | @default_clock = new_clock 28 | end 29 | 30 | module Init 31 | def stop_all 32 | @streams.each { |_, ss| ss.each { |_, s| s.stop } } 33 | end 34 | alias_method :hush, :stop_all 35 | 36 | def start_all 37 | @streams.each { |_, ss| ss.each { |_, s| s.start } } 38 | end 39 | 40 | def peek(pattern, *args) 41 | pattern.peek(*args) 42 | end 43 | 44 | def peek_events(pattern, *args) 45 | pattern.peek_events(*args) 46 | end 47 | 48 | def e(n, m, value=nil) 49 | Bjorklund.new([n, m].min, [n, m].max, value) 50 | end 51 | 52 | def s(str, *values) 53 | StepSequencer.new(str, *values) 54 | end 55 | 56 | def method_missing(method, backend=nil, **opts) 57 | backend ||= Xi.default_backend 58 | super if backend.nil? 59 | 60 | if !backend.is_a?(String) && !backend.is_a?(Symbol) 61 | fail ArgumentError, "invalid backend '#{backend}'" 62 | end 63 | 64 | @streams ||= {} 65 | @streams[backend] ||= {} 66 | 67 | stream = @streams[backend][method] ||= begin 68 | require "xi/#{backend}" 69 | 70 | cls = Class.const_get("#{backend.to_s.camelize}::Stream") 71 | cls.new(method, Xi.default_clock, **opts) 72 | end 73 | 74 | # Define (or overwrite) a local variable named +method+ with the stream 75 | Pry.binding_for(self).local_variable_set(method, stream) 76 | 77 | stream 78 | end 79 | end 80 | end 81 | 82 | singleton_class.include Xi::Init 83 | 84 | # Try to load Supercollider backend and set it as default if installed 85 | begin 86 | require "xi/supercollider" 87 | Xi.default_backend = :supercollider 88 | rescue LoadError 89 | Xi.default_backend = nil 90 | end 91 | -------------------------------------------------------------------------------- /lib/xi/bjorklund.rb: -------------------------------------------------------------------------------- 1 | # Implementation adapted from Nebs' (MIT licensed) 2 | # https://github.com/nebs/bjorklund-euclidean-rhythms 3 | # 4 | class Xi::Bjorklund 5 | attr_reader :pulses, :slots, :value 6 | 7 | def initialize(pulses, slots, value=nil) 8 | @pulses = pulses.to_i 9 | @slots = slots.to_i 10 | @value = value || 1 11 | end 12 | 13 | def p(*args, **metadata) 14 | ary = to_a 15 | ary.map { |v| v ? @value : nil }.p(1 / ary.size, **metadata) 16 | end 17 | 18 | def inspect 19 | "e(#{@pulses}, #{@slots}, #{@value.inspect})" 20 | end 21 | 22 | def to_s 23 | to_a.map { |i| i ? 'x' : '.' }.join 24 | end 25 | 26 | def to_a 27 | k = @pulses 28 | n = @slots 29 | 30 | return [] if n == 0 || k == 0 31 | 32 | bins = [] 33 | remainders = [] 34 | k.times { |i| bins[i] = [true] } 35 | (n-k).times { |i| remainders[i] = [false] } 36 | 37 | return bins.flatten if n == k 38 | 39 | loop do 40 | new_remainders = [] 41 | bins.each_with_index do |bin, i| 42 | if remainders.empty? 43 | new_remainders.push bin 44 | else 45 | bin += remainders.shift 46 | bins[i] = bin 47 | end 48 | end 49 | 50 | if new_remainders.any? 51 | bins.pop new_remainders.count 52 | remainders = new_remainders 53 | end 54 | 55 | break unless remainders.size > 1 56 | end 57 | 58 | return (bins + remainders).flatten 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/xi/clock.rb: -------------------------------------------------------------------------------- 1 | require 'thread' 2 | require 'set' 3 | 4 | Thread.abort_on_exception = true 5 | 6 | module Xi 7 | class Clock 8 | DEFAULT_CPS = 1.0 9 | INTERVAL_SEC = 10 / 1000.0 10 | 11 | attr_reader :init_ts, :latency 12 | 13 | def initialize(cps: DEFAULT_CPS) 14 | @mutex = Mutex.new 15 | @cps = cps 16 | @playing = true 17 | @streams = [].to_set 18 | @init_ts = Time.now.to_i.to_f 19 | @latency = 0.0 20 | @play_thread = Thread.new { thread_routine } 21 | end 22 | 23 | def subscribe(stream) 24 | @mutex.synchronize { @streams << stream } 25 | end 26 | 27 | def unsubscribe(stream) 28 | @mutex.synchronize { @streams.delete(stream) } 29 | end 30 | 31 | def cps 32 | @mutex.synchronize { @cps } 33 | end 34 | 35 | def cps=(new_cps) 36 | @mutex.synchronize { @cps = new_cps.to_f } 37 | end 38 | 39 | def bps 40 | @mutex.synchronize { @cps * 2 } 41 | end 42 | 43 | def bps=(new_bps) 44 | @mutex.synchronize { @cps = new_bps / 2.0 } 45 | end 46 | 47 | def bpm 48 | @mutex.synchronize { @cps * 120 } 49 | end 50 | 51 | def bpm=(new_bpm) 52 | @mutex.synchronize { @cps = new_bpm / 120.0 } 53 | end 54 | 55 | def latency=(new_latency) 56 | @latency = new_latency.to_f 57 | end 58 | 59 | def playing? 60 | @mutex.synchronize { @playing } 61 | end 62 | 63 | def stopped? 64 | !playing? 65 | end 66 | 67 | def play 68 | @mutex.synchronize { @playing = true } 69 | self 70 | end 71 | alias_method :start, :play 72 | 73 | def stop 74 | @mutex.synchronize { @playing = false } 75 | self 76 | end 77 | alias_method :pause, :play 78 | 79 | def seconds_per_cycle 80 | @mutex.synchronize { 1.0 / @cps } 81 | end 82 | 83 | def current_time 84 | Time.now.to_f - @init_ts + @latency 85 | end 86 | 87 | def current_cycle 88 | current_time * cps 89 | end 90 | 91 | def inspect 92 | "#<#{self.class.name}:#{"0x%014x" % object_id} " \ 93 | "cps=#{cps.inspect} #{playing? ? :playing : :stopped}>" 94 | end 95 | 96 | private 97 | 98 | def thread_routine 99 | loop do 100 | do_tick 101 | sleep INTERVAL_SEC 102 | end 103 | end 104 | 105 | def do_tick 106 | return unless playing? 107 | now = self.current_time 108 | cps = self.cps 109 | @streams.each { |s| s.notify(now, cps) } 110 | rescue => err 111 | error(err) 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/xi/core_ext.rb: -------------------------------------------------------------------------------- 1 | require 'xi/core_ext/array' 2 | require 'xi/core_ext/enumerable' 3 | require 'xi/core_ext/enumerator' 4 | require 'xi/core_ext/integer' 5 | require 'xi/core_ext/numeric' 6 | require 'xi/core_ext/object' 7 | require 'xi/core_ext/scalar' 8 | require 'xi/core_ext/string' 9 | -------------------------------------------------------------------------------- /lib/xi/core_ext/array.rb: -------------------------------------------------------------------------------- 1 | require 'xi/pattern' 2 | 3 | module Xi::CoreExt 4 | module Array 5 | def p(*delta, **metadata) 6 | Xi::Pattern.new(self, delta: delta, **metadata) 7 | end 8 | end 9 | end 10 | 11 | class Array 12 | include Xi::CoreExt::Array 13 | end 14 | -------------------------------------------------------------------------------- /lib/xi/core_ext/enumerable.rb: -------------------------------------------------------------------------------- 1 | require 'xi/pattern' 2 | 3 | module Xi::CoreExt 4 | module Enumerable 5 | def p(*delta, **metadata) 6 | Xi::Pattern.new(self.to_a, delta: delta, **metadata) 7 | end 8 | end 9 | end 10 | 11 | class Enumerator 12 | include Xi::CoreExt::Enumerable 13 | end 14 | 15 | class Range 16 | include Xi::CoreExt::Enumerable 17 | end 18 | -------------------------------------------------------------------------------- /lib/xi/core_ext/enumerator.rb: -------------------------------------------------------------------------------- 1 | module Xi::CoreExt 2 | module Enumerator 3 | def next? 4 | peek 5 | true 6 | rescue StopIteration 7 | false 8 | end 9 | end 10 | end 11 | 12 | class Enumerator 13 | include Xi::CoreExt::Enumerator 14 | end 15 | -------------------------------------------------------------------------------- /lib/xi/core_ext/integer.rb: -------------------------------------------------------------------------------- 1 | module Xi::CoreExt 2 | module Integer 3 | def /(o) 4 | super(o.to_r) 5 | end 6 | end 7 | end 8 | 9 | class Integer 10 | prepend Xi::CoreExt::Integer 11 | end 12 | -------------------------------------------------------------------------------- /lib/xi/core_ext/numeric.rb: -------------------------------------------------------------------------------- 1 | module Xi::CoreExt 2 | module Numeric 3 | def midi_to_cps 4 | 440 * (2 ** ((self - 69) / 12.0)) 5 | end 6 | 7 | def db_to_amp 8 | 10 ** (self / 20.0) 9 | end 10 | 11 | def degree_to_key(scale, steps_per_octave) 12 | accidental = (self - self.to_i) * 10.0 13 | inner_key = scale[self % scale.size] 14 | base_key = (self / scale.size).to_i * steps_per_octave + inner_key 15 | if accidental != 0 16 | base_key + accidental * (steps_per_octave / 12.0) 17 | else 18 | base_key 19 | end 20 | end 21 | end 22 | end 23 | 24 | class Integer 25 | include Xi::CoreExt::Numeric 26 | end 27 | 28 | class Float 29 | include Xi::CoreExt::Numeric 30 | end 31 | 32 | class Rational 33 | include Xi::CoreExt::Numeric 34 | end 35 | -------------------------------------------------------------------------------- /lib/xi/core_ext/object.rb: -------------------------------------------------------------------------------- 1 | require "xi/logger" 2 | 3 | class Object 4 | include Xi::Logger 5 | end 6 | -------------------------------------------------------------------------------- /lib/xi/core_ext/scalar.rb: -------------------------------------------------------------------------------- 1 | require 'xi/pattern' 2 | 3 | module Xi::CoreExt 4 | module Scalar 5 | def p(*delta, **metadata) 6 | [self].p(*delta, **metadata) 7 | end 8 | end 9 | end 10 | 11 | class Integer; include Xi::CoreExt::Scalar; end 12 | class Float; include Xi::CoreExt::Scalar; end 13 | class String; include Xi::CoreExt::Scalar; end 14 | class Symbol; include Xi::CoreExt::Scalar; end 15 | class Rational; include Xi::CoreExt::Scalar; end 16 | class Hash; include Xi::CoreExt::Scalar; end 17 | -------------------------------------------------------------------------------- /lib/xi/core_ext/string.rb: -------------------------------------------------------------------------------- 1 | module Xi::CoreExt 2 | # String Inflectors, taken from ActiveSupport 5.0 source code 3 | module String 4 | # Converts strings to UpperCamelCase. 5 | # If the +uppercase_first_letter+ parameter is set to false, then produces 6 | # lowerCamelCase. 7 | # 8 | # Also converts '/' to '::' which is useful for converting 9 | # paths to namespaces. 10 | # 11 | # camelize('active_model') # => "ActiveModel" 12 | # camelize('active_model', false) # => "activeModel" 13 | # camelize('active_model/errors') # => "ActiveModel::Errors" 14 | # camelize('active_model/errors', false) # => "activeModel::Errors" 15 | # 16 | # As a rule of thumb you can think of +camelize+ as the inverse of 17 | # #underscore, though there are cases where that does not hold: 18 | # 19 | # camelize(underscore('SSLError')) # => "SslError" 20 | def camelize 21 | string = self.sub(/^[a-z\d]*/) { |match| match.capitalize } 22 | string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" } 23 | string.gsub!('/'.freeze, '::'.freeze) 24 | string 25 | end 26 | 27 | # Makes an underscored, lowercase form from the expression in the string. 28 | # 29 | # Changes '::' to '/' to convert namespaces to paths. 30 | # 31 | # underscore('ActiveModel') # => "active_model" 32 | # underscore('ActiveModel::Errors') # => "active_model/errors" 33 | # 34 | # As a rule of thumb you can think of +underscore+ as the inverse of 35 | # #camelize, though there are cases where that does not hold: 36 | # 37 | # camelize(underscore('SSLError')) # => "SslError" 38 | def underscore 39 | return self unless self =~ /[A-Z-]|::/ 40 | word = self.to_s.gsub('::'.freeze, '/'.freeze) 41 | word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2'.freeze) 42 | word.gsub!(/([a-z\d])([A-Z])/, '\1_\2'.freeze) 43 | word.tr!("-".freeze, "_".freeze) 44 | word.downcase! 45 | word 46 | end 47 | end 48 | end 49 | 50 | class String 51 | include Xi::CoreExt::String 52 | end 53 | -------------------------------------------------------------------------------- /lib/xi/error_log.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | require 'thread' 3 | 4 | module Xi 5 | class ErrorLog 6 | include Singleton 7 | 8 | attr_accessor :max_msgs 9 | attr_reader :more_errors 10 | alias_method :more_errors?, :more_errors 11 | 12 | def initialize(max_msgs: 6) 13 | @max_msgs = max_msgs 14 | 15 | @mutex = Mutex.new 16 | @errors = [] 17 | @more_errors = false 18 | end 19 | 20 | def <<(msg) 21 | @mutex.synchronize do 22 | @errors.unshift(msg) unless @errors.include?(msg) 23 | if @errors.size >= @max_msgs 24 | @errors.slice!(@max_msgs) 25 | @more_errors = true 26 | end 27 | end 28 | end 29 | 30 | def each 31 | return enum_for(:each) unless block_given? 32 | 33 | msgs = @mutex.synchronize do 34 | res = @errors.dup 35 | @errors.clear 36 | @more_errors = false 37 | res 38 | end 39 | 40 | while !msgs.empty? 41 | yield msgs.shift 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/xi/logger.rb: -------------------------------------------------------------------------------- 1 | require 'tmpdir' 2 | require 'logger' 3 | require 'xi/error_log' 4 | 5 | module Xi 6 | module Logger 7 | LOG_FILE = File.join(Dir.tmpdir, 'xi.log') 8 | 9 | def logger 10 | @@logger ||= begin 11 | logger = ::Logger.new(LOG_FILE) 12 | logger.formatter = proc do |severity, datetime, progname, msg| 13 | "[#{datetime.strftime("%F %T %L")}] #{msg}\n" 14 | end 15 | logger 16 | end 17 | end 18 | 19 | def debug(*args) 20 | logger.debug(args.map(&:to_s).join(' '.freeze)) 21 | end 22 | 23 | def error(error) 24 | logger.error("#{error}:\n#{error.backtrace.join("\n".freeze)}") 25 | ErrorLog.instance << error.to_s 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/xi/osc.rb: -------------------------------------------------------------------------------- 1 | require 'osc-ruby' 2 | 3 | module Xi 4 | module OSC 5 | def initialize(name, clock, server: 'localhost', port:, **opts) 6 | super 7 | @osc = ::OSC::Client.new(server, port) 8 | end 9 | 10 | private 11 | 12 | def send_msg(address, *args) 13 | msg = message(address, *args) 14 | debug(__method__, msg.address, *msg.to_a) 15 | send_osc_msg(msg) 16 | end 17 | 18 | def send_bundle(address, *args, at: Time.now) 19 | msg = message(address, *args) 20 | bundle = ::OSC::Bundle.new(at, msg) 21 | debug(__method__, msg.address, at.to_i, at.usec, *msg.to_a) 22 | send_osc_msg(bundle) 23 | end 24 | 25 | def message(address, *args) 26 | ::OSC::Message.new(address, *args) 27 | end 28 | 29 | def send_osc_msg(msg) 30 | @osc.send(msg) 31 | rescue StandardError => err 32 | error(err) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/xi/pattern.rb: -------------------------------------------------------------------------------- 1 | require 'xi/pattern/transforms' 2 | require 'xi/pattern/generators' 3 | 4 | module Xi 5 | # A Pattern is a lazy, infinite enumeration of values in time. 6 | # 7 | # An event represents a value that occurs in a specific moment in time. It 8 | # is a value together with its onset (start position) in terms of cycles, and 9 | # its duration. It is usually represented by a tuple of (value, start, 10 | # duration, iteration). This tuple indicates when a value occurs in time 11 | # (start), its duration, and on which iteration of the pattern happens. 12 | # 13 | # P is an alias of Pattern, so you can build them using P instead. Note that 14 | # if the pattern was built from an array, the string representation can be 15 | # used to build the same pattern again (almost the same ignoring whitespace 16 | # between constructor arguments). 17 | # 18 | # P[1,2,3] #=> P[1, 2, 3] 19 | # 20 | class Pattern 21 | extend Generators 22 | include Transforms 23 | 24 | # Array or Proc that produces values or events 25 | attr_reader :source 26 | 27 | # Event delta in terms of cycles (default: 1) 28 | attr_reader :delta 29 | 30 | # Hash that contains metadata related to pattern usage 31 | attr_reader :metadata 32 | 33 | # Size of pattern 34 | attr_reader :size 35 | 36 | # Duration of pattern 37 | attr_reader :duration 38 | 39 | # Creates a new Pattern given either a +source+ or a +block+ that yields 40 | # events. 41 | # 42 | # If a block is given, +yielder+ parameter must yield +value+ and +start+ 43 | # (optional) for each event. 44 | # 45 | # @example Pattern from an Array 46 | # Pattern.new(['a', 'b', 'c']).take(5) 47 | # # => [['a', 0, 1, 0], 48 | # # ['b', 1, 1, 0], 49 | # # ['c', 2, 1, 0], 50 | # # ['a', 3, 1, 1], # starts cycling... 51 | # # ['b', 4, 1, 1]] 52 | # 53 | # @example Pattern from a block that yields only values. 54 | # Pattern.new { |y| y << rand(100) }.take(5) 55 | # # => [[52, 0, 1, 0], 56 | # # [8, 1, 1, 0], 57 | # # [83, 2, 1, 0], 58 | # # [25, 3, 1, 0], 59 | # # [3, 4, 1, 0]] 60 | # 61 | # @param source [Array] 62 | # @param size [Integer] number of events per iteration 63 | # @param delta [Numeric, Array, Pattern] event delta 64 | # @param metadata [Hash] 65 | # @yield [yielder, delta] yielder and event delta 66 | # @yieldreturn [value, start, duration] 67 | # @return [Pattern] 68 | # 69 | def initialize(source=nil, size: nil, delta: nil, **metadata, &block) 70 | if source.nil? && block.nil? 71 | fail ArgumentError, 'must provide source or block' 72 | end 73 | 74 | if delta && delta.respond_to?(:size) && !(delta.size < Float::INFINITY) 75 | fail ArgumentError, 'delta cannot be infinite' 76 | end 77 | 78 | # If delta is an array of 1 or 0 values, flatten array 79 | delta = delta.first if delta.is_a?(Array) && delta.size <= 1 80 | 81 | # Block takes precedence as source, even though +source+ can be used to 82 | # infer attributes 83 | @source = block || source 84 | 85 | # Infer attributes from +source+ if it is a pattern 86 | if source.is_a?(Pattern) 87 | @delta = source.delta 88 | @size = source.size 89 | @metadata = source.metadata 90 | else 91 | @delta = 1 92 | @size = (source.respond_to?(:size) ? source.size : nil) || 93 | Float::INFINITY 94 | @metadata = {} 95 | end 96 | 97 | # Flatten source if it is a pattern 98 | @source = @source.source if @source.is_a?(Pattern) 99 | 100 | # Override or merge custom attributes if they were specified 101 | @size = size if size 102 | @delta = delta if delta 103 | @metadata.merge!(metadata) 104 | 105 | # Flatten delta values to an array, if it is an enumerable or pattern 106 | @delta = @delta.to_a if @delta.respond_to?(:to_a) 107 | 108 | # Set duration based on delta values 109 | @duration = delta_values.reduce(:+) || 0 110 | end 111 | 112 | # Create a new Pattern given an array of +args+ 113 | # 114 | # @see Pattern#initialize 115 | # 116 | # @param args [Array] 117 | # @param kwargs [Hash] 118 | # @return [Pattern] 119 | # 120 | def self.[](*args, **kwargs) 121 | new(args, **kwargs) 122 | end 123 | 124 | # Returns a new Pattern with the same +source+, but with +delta+ overriden 125 | # and +metadata+ merged. 126 | # 127 | # @param delta [Array, Pattern, Numeric] 128 | # @param metadata [Hash] 129 | # @return [Pattern] 130 | # 131 | def p(*delta, **metadata) 132 | delta = delta.compact.empty? ? @delta : delta 133 | Pattern.new(@source, delta: delta, size: @size, **@metadata.merge(metadata)) 134 | end 135 | 136 | # Returns true if pattern is infinite 137 | # 138 | # A Pattern is infinite if it was created from a Proc or another infinite 139 | # pattern, and size was not specified. 140 | # 141 | # @return [Boolean] 142 | # @see #finite? 143 | # 144 | def infinite? 145 | @size == Float::INFINITY 146 | end 147 | 148 | # Returns true if pattern is finite 149 | # 150 | # A pattern is finite if it has a finite size. 151 | # 152 | # @return [Boolean] 153 | # @see #infinite? 154 | # 155 | def finite? 156 | !infinite? 157 | end 158 | 159 | # Calls the given block once for each event, passing its value, start 160 | # position, duration and iteration as parameters. 161 | # 162 | # +cycle+ can be any number, even if there is no event that starts exactly 163 | # at that moment. It will start from the next event. 164 | # 165 | # If no block is given, an enumerator is returned instead. 166 | # 167 | # Enumeration loops forever, and starts yielding events based on pattern's 168 | # delta and from the +cycle+ position, which is by default 0. 169 | # 170 | # @example block yields value, start, duration and iteration 171 | # Pattern.new([1, 2], delta: 0.25).each_event.take(4) 172 | # # => [[1, 0.0, 0.25, 0], 173 | # # [2, 0.25, 0.25, 0], 174 | # # [1, 0.5, 0.25, 1], 175 | # # [2, 0.75, 0.25, 1]] 176 | # 177 | # @example +cycle+ is used to start iterating from that moment in time 178 | # Pattern.new([:a, :b, :c], delta: 1/2).each_event(42).take(4) 179 | # # => [[:a, (42/1), (1/2), 28], 180 | # # [:b, (85/2), (1/2), 28], 181 | # # [:c, (43/1), (1/2), 28], 182 | # # [:a, (87/2), (1/2), 29]] 183 | # 184 | # @example +cycle+ can also be a fractional number 185 | # Pattern.new([:a, :b, :c]).each_event(0.97).take(3) 186 | # # => [[:b, 1, 1, 0], 187 | # # [:c, 2, 1, 0], 188 | # # [:a, 3, 1, 1]] 189 | # 190 | # @param cycle [Numeric] 191 | # @yield [v, s, d, i] value, start, duration and iteration 192 | # @return [Enumerator] 193 | # 194 | def each_event(cycle=0) 195 | return enum_for(__method__, cycle) unless block_given? 196 | EventEnumerator.new(self, cycle).each { |v, s, d, i| yield v, s, d, i } 197 | end 198 | 199 | # Calls the given block passing the delta of each value in pattern 200 | # 201 | # This method is used internally by {#each_event} to calculate when each 202 | # event in pattern occurs in time. If no block is given, an Enumerator is 203 | # returned instead. 204 | # 205 | # @param index [Numeric] 206 | # @yield [d] duration 207 | # @return [Enumerator] 208 | # 209 | def each_delta(index=0) 210 | return enum_for(__method__, index) unless block_given? 211 | 212 | delta = @delta 213 | 214 | if delta.is_a?(Array) 215 | size = delta.size 216 | return if size == 0 217 | 218 | start = index.floor 219 | i = start % size 220 | loop do 221 | yield delta[i] 222 | i = (i + 1) % size 223 | start += 1 224 | end 225 | elsif delta.is_a?(Pattern) 226 | delta.each_event(index) { |v, _| yield v } 227 | else 228 | loop { yield delta } 229 | end 230 | end 231 | 232 | # Calls the given block once for each value in source 233 | # 234 | # @example 235 | # Pattern.new([1, 2, 3]).each.to_a 236 | # # => [1, 2, 3] 237 | # 238 | # @return [Enumerator] 239 | # @yield [Object] value 240 | # 241 | def each 242 | return enum_for(__method__) unless block_given? 243 | 244 | each_event { |v, _, _, i| 245 | break if i > 0 246 | yield v 247 | } 248 | end 249 | 250 | # Same as {#each} but in reverse order 251 | # 252 | # @example 253 | # Pattern.new([1, 2, 3]).reverse_each.to_a 254 | # # => [3, 2, 1] 255 | # 256 | # @return [Enumerator] 257 | # @yield [Object] value 258 | # 259 | def reverse_each 260 | return enum_for(__method__) unless block_given? 261 | each.to_a.reverse.each { |v| yield v } 262 | end 263 | 264 | # Returns an array of values from a single iteration of pattern 265 | # 266 | # @return [Array] values 267 | # @see #to_events 268 | # 269 | def to_a 270 | fail StandardError, 'pattern is infinite' if infinite? 271 | each.to_a 272 | end 273 | 274 | # Returns an array of events (i.e. a tuple [value, start, duration, 275 | # iteration]) from the first iteration. 276 | # 277 | # Only applies to finite patterns. 278 | # 279 | # @return [Array] events 280 | # @see #to_a 281 | # 282 | def to_events 283 | fail StandardError, 'pattern is infinite' if infinite? 284 | each_event.take(size) 285 | end 286 | 287 | # Returns a new Pattern with the results of running +block+ once for every 288 | # value in +self+ 289 | # 290 | # If no block is given, an Enumerator is returned. 291 | # 292 | # @yield [v, s, d, i] value, start, duration and iteration 293 | # @yieldreturn [v, s, d] value, start (optional) and duration (optional) 294 | # @return [Pattern] 295 | # 296 | def map 297 | return enum_for(__method__) unless block_given? 298 | 299 | Pattern.new(self) do |y, d| 300 | each_event do |v, s, ed, i| 301 | y << yield(v, s, ed, i) 302 | end 303 | end 304 | end 305 | alias_method :collect, :map 306 | 307 | # Returns a Pattern containing all events of +self+ for which +block+ is 308 | # true. 309 | # 310 | # If no block is given, an Enumerator is returned. 311 | # 312 | # @see Pattern#reject 313 | # 314 | # @yield [v, s, d, i] value, start, duration and iteration 315 | # @yieldreturn [Boolean] whether value is selected 316 | # @return [Pattern] 317 | # 318 | def select 319 | return enum_for(__method__) unless block_given? 320 | 321 | Pattern.new(self) do |y, d| 322 | each_event do |v, s, ed, i| 323 | y << v if yield(v, s, ed, i) 324 | end 325 | end 326 | end 327 | alias_method :find_all, :select 328 | 329 | # Returns a Pattern containing all events of +self+ for which +block+ 330 | # is false. 331 | # 332 | # If no block is given, an Enumerator is returned. 333 | # 334 | # @see Pattern#select 335 | # 336 | # @yield [v, s, d, i] value, start, duration and iteration 337 | # @yieldreturn [Boolean] whether event is rejected 338 | # @return [Pattern] 339 | # 340 | def reject 341 | return enum_for(__method__) unless block_given? 342 | 343 | select { |v, s, d, i| !yield(v, s, d, i) } 344 | end 345 | 346 | # Returns the first +n+ events from the pattern, starting from +cycle+ 347 | # 348 | # @param n [Integer] 349 | # @param cycle [Numeric] 350 | # @return [Array] values 351 | # 352 | def take(n, cycle=0) 353 | each_event(cycle).take(n) 354 | end 355 | 356 | # Returns the first +n+ values from +self+, starting from +cycle+. 357 | # 358 | # Only values are returned, start position and duration are ignored. 359 | # 360 | # @see #take 361 | # 362 | def take_values(*args) 363 | take(*args).map(&:first) 364 | end 365 | 366 | # @see #take_values 367 | def peek(n=10, *args) 368 | take_values(n, *args) 369 | end 370 | 371 | # @see #take 372 | def peek_events(n=10, cycle=0) 373 | take(n, cycle) 374 | end 375 | 376 | # Returns the first element, or the first +n+ elements, of the pattern. 377 | # 378 | # If the pattern is empty, the first form returns nil, and the second form 379 | # returns an empty array. 380 | # 381 | # @see #take 382 | # 383 | # @param n [Integer] 384 | # @param args same arguments as {#take} 385 | # @return [Object, Array] 386 | # 387 | def first(n=nil, *args) 388 | res = take(n || 1, *args) 389 | n.nil? ? res.first : res 390 | end 391 | 392 | # Returns a string containing a human-readable representation 393 | # 394 | # When source is not a Proc, this string can be evaluated to construct the 395 | # same instance. 396 | # 397 | # @return [String] 398 | # 399 | def inspect 400 | ss = if @source.respond_to?(:join) 401 | @source.map(&:inspect).join(', ') 402 | elsif @source.is_a?(Proc) 403 | "?proc" 404 | else 405 | @source.inspect 406 | end 407 | 408 | ms = @metadata.reject { |_, v| v.nil? } 409 | ms.merge!(delta: delta) if delta != 1 410 | ms = ms.map { |k, v| "#{k}: #{v.inspect}" }.join(', ') 411 | 412 | "P[#{ss}#{", #{ms}" unless ms.empty?}]" 413 | end 414 | alias_method :to_s, :inspect 415 | 416 | # Returns pattern interation size or length 417 | # 418 | # This is usually calculated from the least-common multiple between the sum 419 | # of delta values and the size of the pattern. If pattern is infinite, 420 | # pattern size is assumed to be 1, so iteration size depends on delta 421 | # values. 422 | # 423 | # @return [Integer] 424 | # 425 | def iteration_size 426 | finite? ? delta_size.lcm(@size) : delta_size 427 | end 428 | 429 | # @private 430 | def ==(o) 431 | self.class == o.class && 432 | delta == o.delta && 433 | size == o.size && 434 | duration == o.duration && 435 | metadata == o.metadata && 436 | (finite? && to_a == o.to_a) 437 | end 438 | 439 | private 440 | 441 | class EventEnumerator 442 | def initialize(pattern, cycle) 443 | @cycle = cycle 444 | 445 | @source = pattern.source 446 | @size = pattern.size 447 | @iter_size = pattern.iteration_size 448 | 449 | @iter = pattern.duration > 0 ? (cycle / pattern.duration).floor : 0 450 | @delta_enum = pattern.each_delta(@iter * @iter_size) 451 | @start = @iter * pattern.duration 452 | @prev_ev = nil 453 | @i = 0 454 | end 455 | 456 | def each(&block) 457 | return enum_for(__method__, @cycle) unless block_given? 458 | 459 | return if @size == 0 460 | 461 | if @source.respond_to?(:call) 462 | loop do 463 | yielder = ::Enumerator::Yielder.new do |value| 464 | each_block(value, &block) 465 | end 466 | @source.call(yielder, @delta_enum.peek) 467 | end 468 | elsif @source.respond_to?(:each_event) 469 | @source.each_event(@start) do |value, _| 470 | each_block(value, &block) 471 | end 472 | elsif @source.respond_to?(:[]) 473 | loop do 474 | each_block(@source[@i % @size], &block) 475 | end 476 | else 477 | fail StandardError, 'invalid source' 478 | end 479 | end 480 | 481 | private 482 | 483 | def each_block(value) 484 | delta = @delta_enum.peek 485 | 486 | if @start >= @cycle 487 | if @prev_ev 488 | yield @prev_ev if @start > @cycle 489 | @prev_ev = nil 490 | end 491 | yield value, @start, delta, @iter 492 | else 493 | @prev_ev = [value, @start, delta, @iter] 494 | end 495 | 496 | @iter += 1 if @i + 1 == @iter_size 497 | @i = (@i + 1) % @iter_size 498 | @start += delta 499 | @delta_enum.next 500 | end 501 | end 502 | 503 | def delta_values 504 | each_delta.take(iteration_size) 505 | end 506 | 507 | def delta_size 508 | @delta.respond_to?(:each) && @delta.respond_to?(:size) ? @delta.size : 1 509 | end 510 | end 511 | end 512 | 513 | P = Xi::Pattern 514 | -------------------------------------------------------------------------------- /lib/xi/pattern/generators.rb: -------------------------------------------------------------------------------- 1 | module Xi 2 | class Pattern 3 | module Generators 4 | # Create an arithmetic series pattern of +length+ values, being +start+ 5 | # the starting value and +step+ the addition factor. 6 | # 7 | # @example 8 | # peek P.series #=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 9 | # peek P.series(3) #=> [3, 4, 5, 6, 7, 8, 9, 10, 11, 12] 10 | # peek P.series(0, 2) #=> [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] 11 | # peek P.series(0, 0.25, 8) #=> [0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75] 12 | # 13 | # @param start [Numeric] (default: 0) 14 | # @param step [Numeric] (default: 1) 15 | # @param length [Numeric, Symbol] number or inf (default: inf) 16 | # @return [Pattern] 17 | # 18 | def series(start=0, step=1, length=inf) 19 | Pattern.new(size: length) do |y| 20 | i = start 21 | loop_n(length) do 22 | y << i 23 | i += step 24 | end 25 | end 26 | end 27 | 28 | # Create a geometric series pattern of +length+ values, being +start+ the 29 | # starting value and +step+ the multiplication factor. 30 | # 31 | # @example 32 | # peek P.geom #=> [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 33 | # peek P.geom(3) #=> [3, 3, 3, 3, 3, 3, 3, 3, 3, 3] 34 | # peek P.geom(1, 2) #=> [1, 2, 4, 8, 16, 32, 64, 128, 256, 512] 35 | # peek P.geom(1, 1/2, 6) #=> [1, (1/2), (1/4), (1/8), (1/16), (1/32)] 36 | # peek P.geom(1, -1, 8) #=> [1, -1, 1, -1, 1, -1, 1, -1] 37 | # 38 | # @param start [Numeric] (default: 0) 39 | # @param grow [Numeric] (default: 1) 40 | # @param length [Numeric, Symbol] number or inf (default: inf) 41 | # @return [Pattern] 42 | # 43 | def geom(start=0, grow=1, length=inf) 44 | Pattern.new(size: length) do |y| 45 | i = start 46 | loop_n(length) do 47 | y << i 48 | i *= grow 49 | end 50 | end 51 | end 52 | 53 | # Choose items from the +list+ randomly, +repeats+ number of times 54 | # 55 | # +list+ can be a *finite* enumerable or Pattern. 56 | # 57 | # @see Pattern::Transforms#rand 58 | # 59 | # @example 60 | # peek P.rand([1, 2, 3]) #=> [2] 61 | # peek P.rand([1, 2, 3, 4], 6) #=> [1, 3, 2, 2, 4, 3] 62 | # 63 | # @param list [#each] list of values 64 | # @param repeats [Integer, Symbol] number or inf (default: 1) 65 | # @return [Pattern] 66 | # 67 | def rand(list, repeats=1) 68 | Pattern.new(list, size: repeats) do |y| 69 | ls = list.to_a 70 | loop_n(repeats) { y << ls.sample } 71 | end 72 | end 73 | 74 | # Choose randomly, but only allow repeating the same item after yielding 75 | # all items from the list. 76 | # 77 | # +list+ can be a *finite* enumerable or Pattern. 78 | # 79 | # @see Pattern::Transforms#xrand 80 | # 81 | # @example 82 | # peek P.xrand([1, 2, 3, 4, 5]) #=> [4] 83 | # peek P.xrand([1, 2, 3], 8) #=> [1, 3, 2, 3, 1, 2, 3, 2] 84 | # 85 | # @param list [#each] list of values 86 | # @param repeats [Integer, Symbol] number or inf (default: 1) 87 | # @return [Pattern] 88 | # 89 | def xrand(list, repeats=1) 90 | Pattern.new(list, size: repeats) do |y| 91 | ls = list.to_a 92 | xs = nil 93 | loop_n(repeats) do |i| 94 | xs = ls.shuffle if i % ls.size == 0 95 | y << xs[i % ls.size] 96 | end 97 | end 98 | end 99 | 100 | # Shuffle the list in random order, and use the same random order 101 | # +repeats+ times 102 | # 103 | # +list+ can be a *finite* enumerable or Pattern. 104 | # 105 | # @see Pattern::Transforms#shuf 106 | # 107 | # @example 108 | # peek P.shuf([1, 2, 3, 4, 5]) #=> [5, 3, 4, 1, 2] 109 | # peek P.shuf([1, 2, 3], 3) #=> [2, 3, 1, 2, 3, 1, 2, 3, 1] 110 | # 111 | # @param list [#each] list of values 112 | # @param repeats [Integer, Symbol] number or inf (default: 1) 113 | # @return [Pattern] 114 | # 115 | def shuf(list, repeats=1) 116 | Pattern.new(list, size: list.size * repeats) do |y| 117 | xs = list.to_a.shuffle 118 | loop_n(repeats) do |i| 119 | xs.each { |x| y << x } 120 | end 121 | end 122 | end 123 | 124 | # Generates values from a sinewave discretized to +quant+ events 125 | # for the duration of +delta+ cycles. 126 | # 127 | # Values range from 0 to 1 128 | # 129 | # @example 130 | # peek P.sin(8).map { |i| i.round(2) } 131 | # #=> [0.5, 0.85, 1.0, 0.85, 0.5, 0.15, 0.0, 0.15, 0.5, 0.85] 132 | # 133 | # @example +quant+ determines the size, +delta+ the total duration 134 | # P.sin(8).size #=> 8 135 | # P.sin(22).duration #=> (1/1) 136 | # P.sin(19, 2).duration #=> (2/1) 137 | # 138 | # @param quant [Integer] 139 | # @param delta [Integer] (default: 1) 140 | # @return [Pattern] 141 | # 142 | def sin(quant, delta=1) 143 | Pattern.new(size: quant, delta: delta / quant) do |y| 144 | quant.times do |i| 145 | y << (Math.sin(i / quant * 2 * Math::PI) + 1) / 2 146 | end 147 | end 148 | end 149 | 150 | # Generates values from a sawtooth waveform, discretized to +quant+ events 151 | # for the duration of +delta+ cycles 152 | # 153 | # Values range from 0 to 1 154 | # 155 | # @example 156 | # peek P.saw(8) 157 | # #=> [(0/1), (1/8), (1/4), (3/8), (1/2), (5/8), (3/4), (7/8), (0/1), (1/8)] 158 | # 159 | # @example +quant+ determines the size, +delta+ the total duration 160 | # P.saw(8).size #=> 8 161 | # P.saw(22).duration #=> (1/1) 162 | # P.saw(19, 2).duration #=> (2/1) 163 | # 164 | # @param quant [Integer] 165 | # @param delta [Integer] (default: 1) 166 | # @return [Pattern] 167 | # 168 | def saw(quant, delta=1) 169 | Pattern.new(size: quant, delta: delta / quant) do |y| 170 | quant.times do |i| 171 | y << i / quant 172 | end 173 | end 174 | end 175 | 176 | # Generates an inverse sawtooth waveform, discretized to +quant+ events 177 | # for the duration of +delta+ cycles 178 | # 179 | # Values range from 0 to 1 180 | # 181 | # @see P.saw 182 | # 183 | # @example 184 | # peek P.isaw(8) 185 | # #=> [(1/1), (7/8), (3/4), (5/8), (1/2), (3/8), (1/4), (1/8), (1/1), (7/8)] 186 | # 187 | # @param quant [Integer] 188 | # @param delta [Integer] (default: 1) 189 | # @return [Pattern] 190 | # 191 | def isaw(*args) 192 | -P.saw(*args) + 1 193 | end 194 | 195 | # Generates a triangle waveform, discretized to +quant+ events for the 196 | # duration of +delta+ cycles 197 | # 198 | # Values range from 0 to 1 199 | # 200 | # @example 201 | # peek P.tri(8) 202 | # #=> [(0/1), (1/4), (1/2), (3/4), (1/1), (3/4), (1/2), (1/4), (0/1), (1/4)] 203 | # 204 | # @param quant [Integer] 205 | # @param delta [Integer] (default: 1) 206 | # @return [Pattern] 207 | # 208 | def tri(quant, delta=1) 209 | Pattern.new(size: quant, delta: delta / quant) do |y| 210 | half_quant = quant / 2 211 | up_half = half_quant.to_f.ceil 212 | down_half = quant - up_half 213 | 214 | up_half.times do |i| 215 | y << i / half_quant 216 | end 217 | down_half.times do |i| 218 | j = down_half - i 219 | y << j / half_quant 220 | end 221 | end 222 | end 223 | 224 | private 225 | 226 | # @private 227 | def loop_n(length) 228 | i = 0 229 | loop do 230 | break if length != inf && i == length 231 | yield i 232 | i += 1 233 | end 234 | end 235 | end 236 | end 237 | end 238 | -------------------------------------------------------------------------------- /lib/xi/pattern/transforms.rb: -------------------------------------------------------------------------------- 1 | module Xi 2 | class Pattern 3 | module Transforms 4 | # Negates every number in the pattern 5 | # 6 | # Non-numeric values are ignored. 7 | # 8 | # @example 9 | # peek -[10, 20, 30].p #=> [-10, -20, -30] 10 | # peek -[1, -2, 3].p #=> [-1, 2, -3] 11 | # 12 | # @return [Pattern] 13 | # 14 | def -@ 15 | map { |v| v.respond_to?(:-@) ? -v : v } 16 | end 17 | 18 | # Concatenate +object+ pattern or perform a scalar sum with +object+ 19 | # 20 | # If +object+ is a Pattern, concatenate the two patterns. 21 | # Else, for each value from pattern, sum with +object+. 22 | # Values that do not respond to #+ are ignored. 23 | # 24 | # @example Concatenation of patterns 25 | # peek [1, 2, 3].p + [4, 5, 6].p #=> [1, 2, 3, 4, 5, 6] 26 | # 27 | # @example Scalar sum 28 | # peek [1, 2, 3].p + 60 #=> [61, 62, 63] 29 | # peek [0.25, 0.5].p + 0.125 #=> [0.375, 0.625] 30 | # peek [0, :foo, 2].p + 1 #=> [1, :foo, 3] 31 | # 32 | # @param object [Pattern, Numeric] pattern or numeric 33 | # @return [Pattern] 34 | # 35 | def +(object) 36 | if object.is_a?(Pattern) 37 | Pattern.new(self, size: size + object.size) { |y, d| 38 | each { |v| y << v } 39 | object.each { |v| y << v } 40 | } 41 | else 42 | map { |v| v.respond_to?(:+) ? v + object : v } 43 | end 44 | end 45 | 46 | # Performs a scalar substraction with +numeric+ 47 | # 48 | # For each value from pattern, substract with +numeric+. 49 | # Values that do not respond to #- are ignored. 50 | # 51 | # @example 52 | # peek [1, 2, 3].p - 10 #=> [-9, -8, -7] 53 | # peek [1, :foo, 3].p - 10 #=> [-9, :foo, -7] 54 | # 55 | # @param numeric [Numeric] 56 | # @return [Pattern] 57 | # 58 | def -(numeric) 59 | map { |v| v.respond_to?(:-) ? v - numeric : v } 60 | end 61 | 62 | # Performs a scalar multiplication with +numeric+ 63 | # 64 | # For each value from pattern, multiplicate with +numeric+. 65 | # Values that do not respond to #* are ignored. 66 | # 67 | # @example 68 | # peek [1, 2, 4].p * 2 #=> [2, 4, 8] 69 | # peek [1, :foo].p * 2 #=> [2, :foo] 70 | # 71 | # @param numeric [Numeric] 72 | # @return [Pattern] 73 | # 74 | def *(numeric) 75 | map { |v| v.respond_to?(:*) ? v * numeric : v } 76 | end 77 | 78 | # Performs a scalar division by +numeric+ 79 | # 80 | # For each value from pattern, divide by +numeric+. 81 | # Values that do not respond to #/ are ignored. 82 | # 83 | # @example 84 | # peek [1, 2, 4].p / 2 #=> [(1/2), (1/1), (2/1)] 85 | # peek [0.5, :foo].p / 2 #=> [0.25, :foo] 86 | # 87 | # @param numeric [Numeric] 88 | # @return [Pattern] 89 | # 90 | def /(numeric) 91 | map { |v| v.respond_to?(:/) ? v / numeric : v } 92 | end 93 | 94 | # Performs a scalar modulo against +numeric+ 95 | # 96 | # For each value from pattern, return modulo of value divided by +numeric+. 97 | # Values from pattern that do not respond to #% are ignored. 98 | # 99 | # @example 100 | # peek (1..5).p % 2 #=> [1, 0, 1, 0, 1] 101 | # peek [0, 1, 2, :bar, 4, 5, 6].p % 3 #=> [0, 1, 2, :bar, l, 2, 0] 102 | # 103 | # @param numeric [Numeric] 104 | # @return [Pattern] 105 | # 106 | def %(numeric) 107 | map { |v| v.respond_to?(:%) ? v % numeric : v } 108 | end 109 | 110 | # Raises each value to the power of +numeric+, which may be negative or 111 | # fractional. 112 | # 113 | # Values from pattern that do not respond to #** are ignored. 114 | # 115 | # @example 116 | # peek (0..5).p ** 2 #=> [0, 1, 4, 9, 16, 25] 117 | # peek [1, 2, 3].p ** -2 #=> [1, (1/4), (1/9)] 118 | # 119 | # @param numeric [Numeric] 120 | # @return [Pattern] 121 | # 122 | def **(numeric) 123 | map { |v| v.respond_to?(:**) ? v ** numeric : v } 124 | end 125 | alias_method :^, :** 126 | 127 | # Cycles pattern +repeats+ number of times, shifted by +offset+ 128 | # 129 | # @example 130 | # peek [1, 2, 3].p.seq #=> [1, 2, 3] 131 | # peek [1, 2, 3].p.seq(2) #=> [1, 2, 3, 1, 2, 3] 132 | # peek [1, 2, 3].p.seq(1, 1) #=> [2, 3, 1] 133 | # peek [1, 2, 3].p.seq(2, 2) #=> [3, 2, 1, 3, 2, 1] 134 | # 135 | # @param repeats [Integer] number (defaut: 1) 136 | # @param offset [Integer] (default: 0) 137 | # @return [Pattern] 138 | # 139 | def seq(repeats=1, offset=0) 140 | unless repeats.is_a?(Integer) && repeats >= 0 141 | fail ArgumentError, "repeats must be a non-negative Integer" 142 | end 143 | 144 | unless offset.is_a?(Integer) && offset >= 0 145 | fail ArgumentError, "offset must be a non-negative Integer" 146 | end 147 | 148 | Pattern.new(self, size: size * repeats) do |y| 149 | rep = repeats 150 | 151 | loop do 152 | if rep != inf 153 | rep -= 1 154 | break if rep < 0 155 | end 156 | 157 | c = offset 158 | offset_items = [] 159 | 160 | is_empty = true 161 | each do |v| 162 | is_empty = false 163 | if c > 0 164 | offset_items << v 165 | c -= 1 166 | else 167 | y << v 168 | end 169 | end 170 | 171 | offset_items.each { |v| y << v } 172 | 173 | break if is_empty 174 | end 175 | end 176 | end 177 | 178 | # Traverses the pattern in order and then in reverse order, skipping 179 | # first and last values if +skip_extremes+ is true. 180 | # 181 | # @example 182 | # peek (0..3).p.bounce #=> [0, 1, 2, 3, 2, 1] 183 | # peek 10.p.bounce #=> [10] 184 | # 185 | # @example with skip_extremes=false 186 | # peek (0..3).p.bounce(false) #=> [0, 1, 2, 3, 3, 2, 1, 0] 187 | # 188 | # @param skip_extremes [Boolean] Skip first and last values 189 | # to avoid repeated values (default: true) 190 | # @return [Pattern] 191 | # 192 | def bounce(skip_extremes=true) 193 | return self if size == 0 || size == 1 194 | 195 | new_size = skip_extremes ? size * 2 - 2 : size * 2 196 | Pattern.new(self, size: new_size) { |y| 197 | each { |v| y << v } 198 | last_i = size - 1 199 | reverse_each.with_index { |v, i| 200 | y << v unless skip_extremes && (i == 0 || i == last_i) 201 | } 202 | } 203 | end 204 | 205 | # Normalizes a pattern of values that range from +min+ to +max+ to 0..1 206 | # 207 | # Values from pattern that do not respond to #- are ignored. 208 | # 209 | # @example 210 | # peek (1..5).p.normalize(0, 100) 211 | # #=> [(1/100), (1/50), (3/100), (1/25), (1/20)] 212 | # peek [0, 0x40, 0x80, 0xc0].p.normalize(0, 0x100) 213 | # #=> [(0/1), (1/4), (1/2), (3/4)] 214 | # 215 | # @param min [Numeric] 216 | # @param max [Numeric] 217 | # @return [Pattern] 218 | # 219 | def normalize(min, max) 220 | map { |v| v.respond_to?(:-) ? (v - min) / (max - min) : v } 221 | end 222 | alias_method :norm, :normalize 223 | 224 | # Scales a pattern of normalized values (0..1) to a custom range 225 | # +min+..+max+ 226 | # 227 | # This is inverse of {#normalize} 228 | # Values from pattern that do not respond to #* are ignored. 229 | # 230 | # @example 231 | # peek [0.01, 0.02, 0.03, 0.04, 0.05].p.denormalize(0, 100) 232 | # #=> [1.0, 2.0, 3.0, 4.0, 5.0] 233 | # peek [0, 0.25, 0.50, 0.75].p.denormalize(0, 0x100) 234 | # #=> [0, 64.0, 128.0, 192.0] 235 | # 236 | # @param min [Numeric] 237 | # @param max [Numeric] 238 | # @return [Pattern] 239 | # 240 | def denormalize(min, max) 241 | map { |v| v.respond_to?(:*) ? (max - min) * v + min : v } 242 | end 243 | alias_method :denorm, :denormalize 244 | 245 | # Scale from one range of values to another range of values 246 | # 247 | # @example 248 | # peek [0,2,4,1,3,6].p.scale(0, 6, 0, 0x7f) 249 | # #=> [(0/1), (127/3), (254/3), (127/6), (127/2), (127/1)] 250 | # 251 | # @param min_from [Numeric] 252 | # @param max_from [Numeric] 253 | # @param min_to [Numeric] 254 | # @param max_to [Numeric] 255 | # @return [Pattern] 256 | # 257 | def scale(min_from, max_from, min_to, max_to) 258 | normalize(min_from, max_from).denormalize(min_to, max_to) 259 | end 260 | 261 | # Slows down a pattern by stretching start and duration of events 262 | # +num+ times. 263 | # 264 | # It is the inverse operation of #fast 265 | # 266 | # @see #fast 267 | # 268 | # @example 269 | # peek_events %w(a b c d).p([1/4, 1/8, 1/6]).slow(2) 270 | # #=> [E["a",0,1/2], E["b",1/2,1/4], E["c",3/4,1/3], E["d",13/12,1/2]] 271 | # 272 | # @param num [Numeric] 273 | # @return [Pattern] 274 | # 275 | def slow(num) 276 | Pattern.new(self, delta: delta.p * num) 277 | end 278 | 279 | # Advance a pattern by shrinking start and duration of events 280 | # +num+ times. 281 | # 282 | # It is the inverse operation of #slow 283 | # 284 | # @see #slow 285 | # 286 | # @example 287 | # peek_events %w(a b c d).p([1/2, 1/4]).fast(2) 288 | # #=> [E["a",0,1/4], E["b",1/4,1/8], E["c",3/8,1/4], E["d",5/8,1/8]] 289 | # 290 | # @param num [Numeric] 291 | # @return [Pattern] 292 | # 293 | def fast(num) 294 | Pattern.new(self, delta: delta.p / num) 295 | end 296 | 297 | # Based on +probability+, it yields original value or nil 298 | # 299 | # +probability+ can also be an enumerable or a *finite* Pattern. In this 300 | # case, for each value in +probability+ it will enumerate original 301 | # pattern based on that probability value. 302 | # 303 | # @example 304 | # peek (1..6).p.sometimes #=> [1, nil, 3, nil, 5, 6] 305 | # peek (1..6).p.sometimes(1/4) #=> [nil, nil, nil, 4, nil, 6] 306 | # 307 | # @example 308 | # peek (1..6).p.sometimes([0.5, 1]), 12 309 | # #=> [1, 2, nil, nil, 5, 6, 1, 2, 3, 4, 5, 6] 310 | # 311 | # @param probability [Numeric, #each] (default=0.5) 312 | # @return [Pattern] 313 | # 314 | def sometimes(probability=0.5) 315 | prob_pat = probability.p 316 | 317 | if prob_pat.infinite? 318 | fail ArgumentError, 'times must be finite' 319 | end 320 | 321 | Pattern.new(self, size: size * prob_pat.size) do |y| 322 | prob_pat.each do |prob| 323 | each { |v| y << (Kernel.rand < prob ? v : nil) } 324 | end 325 | end 326 | end 327 | 328 | # Repeats each value +times+ 329 | # 330 | # +times+ can also be an enumerable or a *finite* Pattern. In this case, 331 | # for each value in +times+, it will yield each value of original pattern 332 | # repeated a number of times based on that +times+ value. 333 | # 334 | # @example 335 | # peek [1, 2, 3].p.repeat_each(2) #=> [1, 1, 2, 2, 3, 3] 336 | # peek [1, 2, 3].p.repeat_each(3) #=> [1, 1, 1, 2, 2, 2, 3, 3, 3] 337 | # 338 | # @example 339 | # peek [1, 2, 3].p.repeat_each([3,2]), 15 340 | # #=> [1, 1, 1, 2, 2, 2, 3, 3, 3, 1, 1, 2, 2, 3, 3] 341 | # 342 | # @param times [Numeric, #each] 343 | # @return [Pattern] 344 | # 345 | def repeat_each(times) 346 | times_pat = times.p 347 | 348 | if times_pat.infinite? 349 | fail ArgumentError, 'times must be finite' 350 | end 351 | 352 | Pattern.new(self, size: size * times_pat.size) do |y| 353 | times_pat.each do |t| 354 | each { |v| t.times { y << v } } 355 | end 356 | end 357 | end 358 | 359 | # Choose items from the list randomly, +repeats+ number of times 360 | # 361 | # @see Pattern::Generators::ClassMethods#rand 362 | # 363 | # @example 364 | # peek [1, 2, 3].p.rand #=> [2] 365 | # peek [1, 2, 3, 4].p.rand(6) #=> [1, 3, 2, 2, 4, 3] 366 | # 367 | # @param repeats [Integer, Symbol] number or inf (default: 1) 368 | # @return [Pattern] 369 | # 370 | def rand(repeats=1) 371 | P.rand(self, repeats) 372 | end 373 | 374 | # Choose randomly, but only allow repeating the same item after yielding 375 | # all items from the list. 376 | # 377 | # @see Pattern::Generators::ClassMethods#xrand 378 | # 379 | # @example 380 | # peek [1, 2, 3, 4, 5].p.xrand #=> [4] 381 | # peek [1, 2, 3].p.xrand(8) #=> [1, 3, 2, 3, 1, 2, 3, 2] 382 | # 383 | # @param repeats [Integer, Symbol] number or inf (default: 1) 384 | # @return [Pattern] 385 | # 386 | def xrand(repeats=1) 387 | P.xrand(self, repeats) 388 | end 389 | 390 | # Shuffle the list in random order, and use the same random order 391 | # +repeats+ times 392 | # 393 | # @see Pattern::Generators::ClassMethods#shuf 394 | # 395 | # @example 396 | # peek [1, 2, 3, 4, 5].p.xrand #=> [4] 397 | # peek [1, 2, 3].p.xrand(8) #=> [1, 3, 2, 3, 1, 2, 3, 2] 398 | # 399 | # @param repeats [Integer, Symbol] number or inf (default: 1) 400 | # @return [Pattern] 401 | # 402 | def shuf(repeats=1) 403 | P.shuf(self, repeats) 404 | end 405 | 406 | # Returns a new Pattern where values for which +test_proc+ are true are 407 | # yielded as a pattern to another +block+ 408 | # 409 | # If no block is given, an Enumerator is returned. 410 | # 411 | # These values are grouped together as a "subpattern", then yielded to 412 | # +block+ for further transformation and finally spliced into the original 413 | # pattern. +test_proc+ will be called with +value+, +start+ and +duration+ 414 | # as parameters. 415 | # 416 | # @param test_proc [#call] 417 | # @yield [Pattern] subpattern 418 | # @yieldreturn [Pattern] transformed subpattern 419 | # @return [Pattern, Enumerator] 420 | # 421 | def when(test_proc, &block) 422 | return enum_for(__method__, test_proc) if block.nil? 423 | 424 | Pattern.new(self) do |y| 425 | each_event do |v, s, d, i| 426 | if test_proc.call(v, s, d, i) 427 | new_pat = block.call(self) 428 | new_pat.each_event(s) 429 | .take_while { |_, s_, d_| s_ + d_ <= s + d } 430 | .each { |v_, _| y << v_ } 431 | else 432 | y << v 433 | end 434 | end 435 | end 436 | end 437 | 438 | # Splices a new pattern returned from +block+ every +n+ cycles 439 | # 440 | # @see #every_iter 441 | # 442 | # @param n [Numeric] 443 | # @yield [Pattern] subpattern 444 | # @yieldreturn [Pattern] transformed subpattern 445 | # @return [Pattern] 446 | # 447 | def every(n, &block) 448 | fn = proc { |_, s, _| 449 | m = (s + 1) % n 450 | m >= 0 && m < 1 451 | } 452 | self.when(fn, &block) 453 | end 454 | 455 | # Splices a new pattern returned from +block+ every +n+ iterations 456 | # 457 | # @see #every 458 | # 459 | # @param n [Numeric] 460 | # @yield [Pattern] subpattern 461 | # @yieldreturn [Pattern] transformed subpattern 462 | # @return [Pattern] 463 | # 464 | def every_iter(n, &block) 465 | fn = proc { |_, _, _, i| 466 | m = (i + 1) % n 467 | m >= 0 && m < 1 468 | } 469 | self.when(fn, &block) 470 | end 471 | end 472 | end 473 | end 474 | -------------------------------------------------------------------------------- /lib/xi/repl.rb: -------------------------------------------------------------------------------- 1 | require "irb" 2 | require "pry" 3 | require 'io/console' 4 | require "xi/error_log" 5 | 6 | module Xi 7 | module REPL 8 | extend self 9 | 10 | CONFIG_PATH = File.expand_path("~/.config/xi") 11 | HISTORY_FILE = "history" 12 | INIT_SCRIPT_FILE = "init.rb" 13 | 14 | DEFAULT_INIT_SCRIPT = 15 | "# Here you can customize or define functions that will be available in\n" \ 16 | "# Xi, e.g. new streams or a custom clock." 17 | 18 | def start(irb: false) 19 | configure 20 | load_init_script 21 | 22 | irb ? IRB.start : Pry.start 23 | end 24 | 25 | def configure 26 | prepare_config_dir 27 | 28 | if ENV["INSIDE_EMACS"] 29 | Pry.config.correct_indent = false 30 | Pry.config.pager = false 31 | Pry.config.prompt = [ proc { "" }, proc { "" }] 32 | else 33 | Pry.config.prompt = [ proc { "xi> " }, proc { "..> " }] 34 | end 35 | 36 | Pry.config.history.file = history_path 37 | 38 | Pry.hooks.add_hook(:after_eval, "check_for_errors") do |result, pry| 39 | more_errors = ErrorLog.instance.more_errors? 40 | ErrorLog.instance.each do |msg| 41 | puts red("Error: #{msg}") 42 | end 43 | puts red("There were more errors...") if more_errors 44 | end 45 | end 46 | 47 | def red(string) 48 | "\e[31m\e[1m#{string}\e[22m\e[0m" 49 | end 50 | 51 | def load_init_script 52 | require(init_script_path) 53 | end 54 | 55 | def prepare_config_dir 56 | FileUtils.mkdir_p(CONFIG_PATH) 57 | 58 | unless File.exists?(init_script_path) 59 | File.write(init_script_path, DEFAULT_INIT_SCRIPT) 60 | end 61 | end 62 | 63 | def history_path 64 | File.join(CONFIG_PATH, HISTORY_FILE) 65 | end 66 | 67 | def init_script_path 68 | File.join(CONFIG_PATH, INIT_SCRIPT_FILE) 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/xi/scale.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | class Xi::Scale 4 | extend Forwardable 5 | 6 | DEGREES = { 7 | # TWELVE TONES PER OCTAVE 8 | # 5 note scales 9 | minorPentatonic: [0,3,5,7,10], 10 | majorPentatonic: [0,2,4,7,9], 11 | # another mode of major pentatonic 12 | ritusen: [0,2,5,7,9], 13 | # another mode of major pentatonic 14 | egyptian: [0,2,5,7,10], 15 | 16 | kumoi: [0,2,3,7,9], 17 | hirajoshi: [0,2,3,7,8], 18 | 19 | iwato: [0,1,5,6,10], # mode of hirajoshi 20 | chinese: [0,4,6,7,11], # mode of hirajoshi 21 | indian: [0,4,5,7,10], 22 | pelog: [0,1,3,7,8], 23 | 24 | prometheus: [0,2,4,6,11], 25 | scriabin: [0,1,4,7,9], 26 | 27 | # han chinese pentatonic scales 28 | gong: [0,2,4,7,9], 29 | shang: [0,2,5,7,10], 30 | jiao: [0,3,5,8,10], 31 | zhi: [0,2,5,7,9], 32 | yu: [0,3,5,7,10], 33 | 34 | # 6 note scales 35 | whole: [0, 2, 4, 6, 8, 10], 36 | augmented: [0,3,4,7,8,11], 37 | augmented2: [0,1,4,5,8,9], 38 | 39 | # hexatonic modes with no tritone 40 | hexMajor7: [0,2,4,7,9,11], 41 | hexDorian: [0,2,3,5,7,10], 42 | hexPhrygian: [0,1,3,5,8,10], 43 | hexSus: [0,2,5,7,9,10], 44 | hexMajor6: [0,2,4,5,7,9], 45 | hexAeolian: [0,3,5,7,8,10], 46 | 47 | # 7 note scales 48 | major: [0,2,4,5,7,9,11], 49 | ionian: [0,2,4,5,7,9,11], 50 | dorian: [0,2,3,5,7,9,10], 51 | phrygian: [0,1,3,5,7,8,10], 52 | lydian: [0,2,4,6,7,9,11], 53 | mixolydian: [0,2,4,5,7,9,10], 54 | aeolian: [0,2,3,5,7,8,10], 55 | minor: [0,2,3,5,7,8,10], 56 | locrian: [0,1,3,5,6,8,10], 57 | 58 | harmonicMinor: [0,2,3,5,7,8,11], 59 | harmonicMajor: [0,2,4,5,7,8,11], 60 | 61 | melodicMinor: [0,2,3,5,7,9,11], 62 | melodicMinorDesc: [0,2,3,5,7,8,10], 63 | melodicMajor: [0,2,4,5,7,8,10], 64 | 65 | bartok: [0,2,4,5,7,8,10], 66 | hindu: [0,2,4,5,7,8,10], 67 | 68 | # raga modes 69 | todi: [0,1,3,6,7,8,11], 70 | purvi: [0,1,4,6,7,8,11], 71 | marva: [0,1,4,6,7,9,11], 72 | bhairav: [0,1,4,5,7,8,11], 73 | ahirbhairav: [0,1,4,5,7,9,10], 74 | 75 | superLocrian: [0,1,3,4,6,8,10], 76 | romanianMinor: [0,2,3,6,7,9,10], 77 | hungarianMinor: [0,2,3,6,7,8,11], 78 | neapolitanMinor: [0,1,3,5,7,8,11], 79 | enigmatic: [0,1,4,6,8,10,11], 80 | spanish: [0,1,4,5,7,8,10], 81 | 82 | # modes of whole tones with added note -> 83 | leadingWhole: [0,2,4,6,8,10,11], 84 | lydianMinor: [0,2,4,6,7,8,10], 85 | neapolitanMajor: [0,1,3,5,7,9,11], 86 | locrianMajor: [0,2,4,5,6,8,10], 87 | 88 | # 8 note scales 89 | diminished: [0,1,3,4,6,7,9,10], 90 | diminished2: [0,2,3,5,6,8,9,11], 91 | 92 | # 12 note scales 93 | chromatic: (0..11).to_a, 94 | } 95 | 96 | class << self 97 | DEGREES.each do |name, list| 98 | define_method(name) { self.new(list) } 99 | end 100 | end 101 | 102 | attr_reader :notes 103 | 104 | def initialize(notes) 105 | @notes = notes 106 | end 107 | 108 | def_delegators :@notes, :size, :[], :to_a, :first 109 | 110 | def p(*delta, **metadata) 111 | [@notes].p(*delta, **metadata) 112 | end 113 | 114 | #def size 115 | #@notes.size 116 | #end 117 | end 118 | -------------------------------------------------------------------------------- /lib/xi/step_sequencer.rb: -------------------------------------------------------------------------------- 1 | class Xi::StepSequencer 2 | attr_reader :string, :values 3 | 4 | def initialize(string, *values) 5 | @string = string 6 | @values = values 7 | end 8 | 9 | def p(*args, **metadata) 10 | build_pattern(**metadata) 11 | end 12 | 13 | def inspect 14 | "s(#{@string.inspect}" \ 15 | "#{", #{@values.map(&:inspect).join(', ')}" unless @values.empty?})" 16 | end 17 | 18 | private 19 | 20 | def build_pattern(**metadata) 21 | val_keys = values_per_key 22 | 23 | values_per_bar = @string.split('|').map { |bar| 24 | vs = bar.split(/\s*/).reject(&:empty?) 25 | vs.map { |k| val_keys[k] } 26 | }.reject(&:empty?) 27 | 28 | delta = values_per_bar.map { |vs| [1 / vs.size] * vs.size }.flatten 29 | 30 | Pattern.new(values_per_bar.flatten, delta: delta, **metadata) 31 | end 32 | 33 | def values_per_key 34 | keys.map.with_index { |k, i| [k, k == '.' ? nil : @values[i]] }.to_h 35 | end 36 | 37 | def keys 38 | @string.scan(/\w/).uniq 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/xi/stream.rb: -------------------------------------------------------------------------------- 1 | require 'xi/scale' 2 | require 'set' 3 | 4 | module Xi 5 | class Stream 6 | attr_reader :clock, :opts, :source, :state, :delta, :gate 7 | 8 | DEFAULT_PARAMS = { 9 | degree: 0, 10 | octave: 5, 11 | root: 0, 12 | scale: Xi::Scale.major, 13 | steps_per_octave: 12, 14 | } 15 | 16 | def initialize(name, clock, **opts) 17 | Array(opts.delete(:include)).each { |m| include_mixin(m) } 18 | 19 | @name = name.to_sym 20 | @opts = opts 21 | 22 | @mutex = Mutex.new 23 | @playing = false 24 | @last_sound_object_id = 0 25 | @state = {} 26 | @changed_params = [].to_set 27 | @playing_sound_objects = {} 28 | @prev_ts = {} 29 | @prev_delta = {} 30 | 31 | self.clock = clock 32 | end 33 | 34 | def set(delta: nil, gate: nil, **source) 35 | @mutex.synchronize do 36 | remove_parameters_from_prev_source(source) 37 | @source = source 38 | @gate = gate || parameter_with_smallest_delta(source) 39 | @delta = delta if delta 40 | @reset = true unless @playing 41 | update_internal_structures 42 | end 43 | play 44 | self 45 | end 46 | alias_method :call, :set 47 | 48 | def delta=(new_value) 49 | @mutex.synchronize do 50 | @delta = new_value 51 | update_internal_structures 52 | end 53 | end 54 | 55 | def gate=(new_value) 56 | @mutex.synchronize do 57 | @gate = new_value 58 | update_internal_structures 59 | end 60 | end 61 | 62 | def clock=(new_clock) 63 | @clock.unsubscribe(self) if @clock 64 | new_clock.subscribe(self) if playing? 65 | @clock = new_clock 66 | end 67 | 68 | def playing? 69 | @mutex.synchronize { @playing } 70 | end 71 | 72 | def stopped? 73 | !playing? 74 | end 75 | 76 | def play 77 | @mutex.synchronize do 78 | @playing = true 79 | @clock.subscribe(self) 80 | end 81 | self 82 | end 83 | alias_method :start, :play 84 | 85 | def stop 86 | @mutex.synchronize do 87 | @playing = false 88 | @state.clear 89 | @prev_ts.clear 90 | @prev_delta.clear 91 | @clock.unsubscribe(self) 92 | end 93 | self 94 | end 95 | alias_method :pause, :play 96 | 97 | def inspect 98 | "#<#{self.class.name} :#{@name} " \ 99 | "#{playing? ? :playing : :stopped} at #{@clock.cps}cps" \ 100 | "#{" #{@opts}" if @opts.any?}>" 101 | rescue => err 102 | error(err) 103 | end 104 | 105 | def notify(now, cps) 106 | return unless playing? && @source 107 | 108 | @mutex.synchronize do 109 | @changed_params.clear 110 | 111 | update_all_state if @reset 112 | 113 | gate_off = gate_off_old_sound_objects(now) 114 | gate_on = play_enums(now, cps) 115 | 116 | # Call hooks 117 | do_gate_off_change(gate_off) unless gate_off.empty? 118 | do_state_change if state_changed? 119 | do_gate_on_change(gate_on) unless gate_on.empty? 120 | end 121 | end 122 | 123 | private 124 | 125 | def include_mixin(module_or_name) 126 | mod = if module_or_name.is_a?(Module) 127 | module_or_name 128 | else 129 | name = module_or_name.to_s 130 | require "#{self.class.name.underscore}/#{name}" 131 | self.class.const_get(name.camelize) 132 | end 133 | singleton_class.send(:include, mod) 134 | end 135 | 136 | def changed_state 137 | @state.select { |k, _| @changed_params.include?(k) } 138 | end 139 | 140 | def gate_off_old_sound_objects(now) 141 | gate_off = [] 142 | 143 | # Check if there are any currently playing sound objects that 144 | # must be gated off 145 | @playing_sound_objects.dup.each do |start_pos, h| 146 | if now + @clock.init_ts >= h[:at] - latency_sec 147 | gate_off << h 148 | @playing_sound_objects.delete(start_pos) 149 | end 150 | end 151 | 152 | gate_off 153 | end 154 | 155 | def play_enums(now, cps) 156 | gate_on = [] 157 | 158 | @enums.each do |p, enum| 159 | next unless enum.next? 160 | 161 | n_value, n_start, n_dur = enum.peek 162 | 163 | @prev_ts[p] ||= n_start / cps 164 | @prev_delta[p] ||= n_dur 165 | 166 | next_start = @prev_ts[p] + (@prev_delta[p] / cps) 167 | 168 | # Do we need to play next event? If not, skip this parameter value 169 | if now >= next_start - latency_sec 170 | # If it is too late to play this event, skip it 171 | if now < next_start 172 | starts_at = @clock.init_ts + next_start 173 | 174 | # Update state based on pattern value 175 | # TODO: Pass as parameter exact time: starts_at 176 | update_state(p, n_value) 177 | transform_state 178 | 179 | # If a gate parameter changed, create a new sound object 180 | if p == @gate 181 | # If these sounds objects are new, 182 | # consider them as new "gate on" events. 183 | unless @playing_sound_objects.key?(n_start) 184 | new_so_ids = Array(n_value) 185 | .size.times.map { new_sound_object_id } 186 | 187 | gate_on << {so_ids: new_so_ids, at: starts_at} 188 | @playing_sound_objects[n_start] = {so_ids: new_so_ids} 189 | end 190 | 191 | # Set (or update) ends_at timestamp 192 | legato = @state[:legato] || 1 193 | ends_at = @clock.init_ts + next_start + ((n_dur * legato) / cps) 194 | @playing_sound_objects[n_start][:at] = ends_at 195 | end 196 | end 197 | 198 | @prev_ts[p] = next_start 199 | @prev_delta[p] = n_dur 200 | 201 | # Because we already processed event, advance enumerator 202 | enum.next 203 | end 204 | end 205 | 206 | gate_on 207 | end 208 | 209 | def transform_state 210 | @state = DEFAULT_PARAMS.merge(@state) 211 | 212 | @state[:s] ||= @name 213 | 214 | if !changed_param?(:note) && changed_param?(:degree, :scale, :steps_per_octave) 215 | @state[:note] = reduce_to_note 216 | @changed_params << :note 217 | end 218 | 219 | if !changed_param?(:midinote) && changed_param?(:note) 220 | @state[:midinote] = reduce_to_midinote 221 | @changed_params << :midinote 222 | end 223 | end 224 | 225 | def reduce_to_midinote 226 | Array(@state[:note]).compact.map { |n| 227 | @state[:root].to_i + @state[:octave].to_i * @state[:steps_per_octave] + n 228 | } 229 | end 230 | 231 | def reduce_to_note 232 | Array(@state[:degree]).compact.map do |d| 233 | d.degree_to_key(Array(@state[:scale]), @state[:steps_per_octave]) 234 | end 235 | end 236 | 237 | def changed_param?(*params) 238 | @changed_params.any? { |p| params.include?(p) } 239 | end 240 | 241 | def new_sound_object_id 242 | @last_sound_object_id += 1 243 | end 244 | 245 | def update_internal_structures 246 | cycle = @clock.current_cycle 247 | @enums = @source.map { |k, v| [k, v.p(@delta).each_event(cycle)] }.to_h 248 | end 249 | 250 | def do_gate_on_change(ss) 251 | debug "Gate on change: #{ss}" 252 | end 253 | 254 | def do_gate_off_change(ss) 255 | debug "Gate off change: #{ss}" 256 | end 257 | 258 | def do_state_change 259 | debug "State change: #{@state 260 | .select { |k, v| @changed_params.include?(k) }.to_h}" 261 | end 262 | 263 | def update_state(param, value) 264 | kv = value.is_a?(Hash) ? value : {param => value} 265 | kv.each do |k, v| 266 | if v != @state[k] 267 | debug "Update state of :#{k}: #{v}" 268 | @changed_params << k 269 | @state[k] = v 270 | end 271 | end 272 | end 273 | 274 | def state_changed? 275 | !@changed_params.empty? 276 | end 277 | 278 | def update_all_state 279 | @enums.each do |p, enum| 280 | n_value, _ = enum.peek 281 | update_state(p, n_value) 282 | end 283 | transform_state 284 | @reset = false 285 | end 286 | 287 | def parameter_with_smallest_delta(source) 288 | source.min_by { |param, enum| 289 | delta = enum.p.delta 290 | delta.is_a?(Array) ? delta.min : delta 291 | }.first 292 | end 293 | 294 | def remove_parameters_from_prev_source(new_source) 295 | (@source.keys - new_source.keys).each { |k| @state.delete(k) } unless @source.nil? 296 | end 297 | 298 | def latency_sec 299 | 0.05 300 | end 301 | end 302 | end 303 | -------------------------------------------------------------------------------- /lib/xi/supercollider.rb: -------------------------------------------------------------------------------- 1 | require "xi/supercollider/stream" 2 | 3 | module Xi 4 | module Supercollider 5 | # Your code goes here... 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/xi/supercollider/stream.rb: -------------------------------------------------------------------------------- 1 | require 'xi/stream' 2 | require 'xi/osc' 3 | require 'set' 4 | 5 | module Xi::Supercollider 6 | class Stream < Xi::Stream 7 | include Xi::OSC 8 | 9 | MAX_NODE_ID = 10000 10 | DEFAULT_PARAMS = { 11 | out: 0, 12 | amp: 1.0, 13 | pan: 0.0, 14 | vel: 127, 15 | } 16 | 17 | def initialize(name, clock, server: 'localhost', port: 57110, base_node_id: 2000, **opts) 18 | super 19 | 20 | @base_node_id = base_node_id 21 | @playing_synths = [].to_set 22 | at_exit { free_playing_synths } 23 | end 24 | 25 | def stop 26 | @mutex.synchronize do 27 | @playing_synths.each do |so_id| 28 | n_set(node_id(so_id), gate: 0) 29 | end 30 | end 31 | super 32 | end 33 | 34 | def free_playing_synths 35 | n_free(*@playing_synths.map { |so_id| node_id(so_id) }) 36 | end 37 | 38 | def node_id(so_id) 39 | (@base_node_id + so_id) % MAX_NODE_ID 40 | end 41 | 42 | private 43 | 44 | def transform_state 45 | super 46 | 47 | @state = DEFAULT_PARAMS.merge(@state) 48 | 49 | if changed_param?(:db) && !changed_param?(:amp) 50 | @state[:amp] = @state[:db].db_to_amp 51 | @changed_params << :amp 52 | end 53 | 54 | if changed_param?(:midinote) && !changed_param?(:freq) 55 | @state[:freq] = Array(@state[:midinote]).map(&:midi_to_cps) 56 | @changed_params << :freq 57 | end 58 | end 59 | 60 | def do_gate_on_change(changes) 61 | debug "Gate on change: #{changes}" 62 | 63 | name = @state[:s] || :default 64 | state_params = @state.reject { |k, _| %i(s).include?(k) } 65 | 66 | freq = Array(state_params[:freq]) 67 | 68 | changes.each do |change| 69 | at = Time.at(change.fetch(:at)) 70 | 71 | change.fetch(:so_ids).each.with_index do |so_id, i| 72 | freq_i = freq.size > 0 ? freq[i % freq.size] : nil 73 | 74 | s_new(name, node_id(so_id), **state_params, gate: 1, freq: freq_i, at: at) 75 | @playing_synths << so_id 76 | end 77 | end 78 | end 79 | 80 | def do_gate_off_change(changes) 81 | debug "Gate off change: #{changes}" 82 | 83 | changes.each do |change| 84 | at = Time.at(change.fetch(:at)) 85 | 86 | change.fetch(:so_ids).each do |so_id| 87 | n_set(node_id(so_id), gate: 0, at: at) 88 | @playing_synths.delete(so_id) 89 | end 90 | end 91 | end 92 | 93 | def do_state_change 94 | debug "State change: #{changed_state}" 95 | @playing_synths.each do |so_id| 96 | n_set(node_id(so_id), **changed_state) 97 | end 98 | end 99 | 100 | def n_set(id, at: Time.now, **args) 101 | send_bundle('/n_set', id, *osc_args(args), at: at) 102 | end 103 | 104 | def s_new(name, id, add_action: 0, target_id: 1, at: Time.now, **args) 105 | send_bundle('/s_new', name.to_s, id.to_i, add_action.to_i, 106 | target_id.to_i, *osc_args(args), at: at) 107 | end 108 | 109 | def n_free(*ids, at: Time.now) 110 | send_bundle('/n_free', *ids, at: at) 111 | end 112 | 113 | def osc_args(**args) 114 | args.map { |k, v| [k.to_s, coerce_osc_value(v)] }.flatten(1) 115 | end 116 | 117 | def coerce_osc_value(value) 118 | v = Array(value).first 119 | v = v.to_f if v.is_a?(Rational) 120 | v = v.to_i if !v.is_a?(Float) && !v.is_a?(String) && !v.is_a?(Symbol) 121 | v 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /lib/xi/tidal_clock.rb: -------------------------------------------------------------------------------- 1 | require "websocket" 2 | require "time" 3 | 4 | module Xi 5 | class TidalClock < Clock 6 | SYNC_INTERVAL_SEC = 100 / 1000.0 7 | 8 | attr_reader :server, :port, :attached 9 | alias_method :attached?, :attached 10 | 11 | def initialize(server: 'localhost', port: 9160, **opts) 12 | @server = server 13 | @port = port 14 | @attached = true 15 | 16 | super(opts) 17 | 18 | @ws_thread = Thread.new { ws_thread_routine } 19 | end 20 | 21 | def cps=(new_cps) 22 | fail NotImplementedError, 'cps is read-only' 23 | end 24 | 25 | def dettach 26 | @attached = false 27 | self 28 | end 29 | 30 | def attach 31 | @attached = true 32 | self 33 | end 34 | 35 | private 36 | 37 | def ws_thread_routine 38 | loop do 39 | do_ws_sync 40 | sleep INTERVAL_SEC 41 | end 42 | end 43 | 44 | def do_ws_sync 45 | return unless @attached 46 | 47 | # Try to connect to websocket server 48 | connect 49 | return if @socket.nil? || @socket.closed? 50 | 51 | # Offer a handshake 52 | @handshake = WebSocket::Handshake::Client.new(url: "ws://#{@server}:#{@port}") 53 | @socket.puts @handshake.to_s 54 | 55 | # Read server response 56 | while line = @socket.gets 57 | @handshake << line 58 | break if @handshake.finished? 59 | end 60 | 61 | unless @handshake.finished? 62 | debug(__method__, "Handshake didn't finished. Disconnect") 63 | @socket.close 64 | return 65 | end 66 | 67 | unless @handshake.valid? 68 | debug(__method__, "Handshake is not valid. Disconnect") 69 | @socket.close 70 | return 71 | end 72 | 73 | frame = WebSocket::Frame::Incoming::Server.new(version: @handshake.version) 74 | 75 | # Read loop 76 | loop do 77 | data, _ = @socket.recvfrom(4096) 78 | break if data.empty? 79 | 80 | frame << data 81 | while f = frame.next 82 | if (f.type == :close) 83 | debug(__method__, "Close frame received. Disconnect") 84 | @socket.close 85 | return 86 | else 87 | debug(__method__, "Frame: #{f}") 88 | hash = parse_frame_body(f.to_s) 89 | update_clock_from_server_data(hash) 90 | end 91 | end 92 | end 93 | 94 | rescue => err 95 | error(err) 96 | end 97 | 98 | def connect 99 | @socket = TCPSocket.new(@server, @port) 100 | rescue => err 101 | error(err) 102 | sleep 1 103 | end 104 | 105 | def parse_frame_body(body) 106 | h = {} 107 | ts, _, cps = body.split(',') 108 | h[:ts] = Time.parse(ts) 109 | h[:cps] = cps.to_f 110 | h 111 | end 112 | 113 | def update_clock_from_server_data(h) 114 | # Do not set @init_ts for now 115 | #@init_ts = h[:ts].to_f 116 | @cps = h[:cps] 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/xi/version.rb: -------------------------------------------------------------------------------- 1 | module Xi 2 | VERSION = "0.2.5" 3 | end 4 | -------------------------------------------------------------------------------- /synthdefs/other.scd: -------------------------------------------------------------------------------- 1 | ( 2 | SynthDef(\kick, { |out=0, amp=1, freq=70, attack=0.001, xstart=1, xend=0.25, xdur=0.05, release=0.15, pan=0.5| 3 | var sig = SinOsc.ar(freq * XLine.ar(xstart, xend, xdur)); 4 | var env = EnvGen.ar(Env.perc(attack, release, 1, -4), doneAction: 2); 5 | sig = Pan2.ar(sig, pan) * env; 6 | sig = (sig * 5).tanh; 7 | OffsetOut.ar(out, Pan2.ar(sig * env, pan, amp * 0.25)); 8 | }).add; 9 | 10 | SynthDef(\snare, { |out=0, amp=1, freq=1000, freq2=180, attack=0.01, release=0.2, pan=0| 11 | var snd1 = WhiteNoise.ar(amp); 12 | var snd2 = SinOsc.ar(freq2, 0, amp); 13 | var env = Env.perc(attack, release).kr(doneAction: 2); 14 | var sum = RHPF.ar(snd1 * env, 2500) + LPF.ar(snd2 * env, 1500); 15 | OffsetOut.ar(out, Pan2.ar(sum, pan, amp * 0.1)) 16 | }).add; 17 | 18 | SynthDef(\bassy, { |out=0, amp=1, freq=440, hctf=1000, lctf=5000, rq=0.5, attack=0.001, release=1, mul=1, pan=0.5| 19 | var sig = Saw.ar(freq); 20 | var env = EnvGen.ar(Env.perc(attack, release), doneAction: 2); 21 | sig = mul * BHiPass.ar(RLPF.ar(sig, lctf * env, rq), hctf, rq); 22 | OffsetOut.ar(out, Pan2.ar(sig * env, pan, amp * 0.1)) 23 | }).add; 24 | 25 | SynthDef(\sin, { |out, amp=1, attack=0.001, release=1, sustain=1, pan=0, accelerate=0, freq=440, detune=0.1| 26 | var env = EnvGen.ar(Env.perc(attack, release, 1, -4), timeScale: sustain / 2, doneAction: 2); 27 | var sound = SinOsc.ar([freq, freq+detune] * Line.kr(1,1+accelerate, sustain)); 28 | OffsetOut.ar(out, Pan2.ar(sound * env, pan, amp)); 29 | }).add; 30 | 31 | SynthDef(\fm, { |out, amp=1, attack=0.001, sustain=1, pan=0, accelerate=0, freq=440, carPartial=1, modPartial=1, index=3, mul=0.1, detune=0.1| 32 | var env = EnvGen.ar(Env.perc(attack, 0.999, 1, -3), timeScale: sustain / 2, doneAction: 2); 33 | var mod = SinOsc.ar(freq * modPartial * Line.kr(1,1+accelerate, sustain), 0, freq * index * LFNoise1.kr(5.reciprocal).abs); 34 | var car = SinOsc.ar(([freq, freq+detune] * carPartial) + mod, 0, mul); 35 | OffsetOut.ar(out, Pan2.ar(car * env, pan, amp)); 36 | }).add; 37 | 38 | SynthDef(\saw, {|out, amp=1, attack=0.001, sustain=1, pan=0, accelerate=0, freq=440, detune=0.1| 39 | var env = EnvGen.ar(Env.perc(attack, 0.999, 1, -4), timeScale: sustain / 2, doneAction: 2); 40 | var sound = Saw.ar([freq, freq + detune] * Line.kr(1, 1 + accelerate, sustain)); 41 | OffsetOut.ar(out, Pan2.ar(sound * env, pan, amp * 0.1)); 42 | }).add; 43 | ) 44 | -------------------------------------------------------------------------------- /synthdefs/superdirt.scd: -------------------------------------------------------------------------------- 1 | // 2 | // The following synth definitions were taken from Superdirt almost as is. 3 | // Original file at https://github.com/musikinformatik/SuperDirt/blob/bb1536329896e6e211c3bafee7befb06d06fc856/library/default-synths-extra.scd 4 | // 5 | // Source code included in this file is licensed under GPL v2. 6 | // Please refer to LICENSE file at 7 | // https://github.com/musikinformatik/SuperDirt/blob/master/LICENSE 8 | // 9 | 10 | // Physical modeling of a vibrating string, using a delay line (CombL) excited 11 | // by an intial pulse (Impulse). To make it a bit richer, I've combined two 12 | // slightly detuned delay lines. 13 | // 14 | // "accelerate" is used for a pitch glide 15 | // "sustain" changes the envelope timescale 16 | ( 17 | SynthDef(\smandolin, { |out, amp=1, sustain=1, pan=0, accelerate=0, freq=440, detune=0.2| 18 | var env = EnvGen.ar(Env.linen(0.002, 0.996, 0.002, 1,-3), timeScale: sustain, doneAction: 2); 19 | var sound = Decay.ar(Impulse.ar(0,0,0.1), 0.1 * (freq.cpsmidi) / 69) * WhiteNoise.ar; 20 | var pitch = freq * Line.kr(1, 1 + accelerate, sustain); 21 | sound = CombL.ar(sound, 0.05, pitch.reciprocal * (1 - (detune/100)), sustain) 22 | + CombL.ar(sound, 0.05, pitch.reciprocal * (1 + (detune/100)), sustain); 23 | OffsetOut.ar(out, Pan2.ar(sound * env * amp, pan)) 24 | }).add 25 | ); 26 | 27 | // An example of additive synthesis, building up a gong-like noise from a sum 28 | // of sine-wave harmonics. Notice how the envelope timescale and amplitude can 29 | // be scaled as a function of the harmonic frequency. 30 | // "voice" provides something like a tone knob 31 | // "decay" adjusts how the harmonics decay as in the other SynthDefs 32 | // "sustain" affects the overall envelope timescale 33 | // "accelerate" for pitch glide 34 | ( 35 | SynthDef(\sgong, { |out, amp=1, sustain=1, pan=0, accelerate=0, freq=440, voice=0, decay=1| 36 | // lowest modes for clamped circular plate 37 | var freqlist = [1.000, 2.081, 3.414, 3.893, 4.995, 5.954, 6.819, 8.280, 8.722, 8.882, 10.868, 11.180, 11.754, 13.710, 13.715, 15.057, 15.484, 16.469, 16.817, 18.628] ** 1.0; 38 | var tscale = 100.0 / freq / (freqlist ** (2 - clip(decay, 0, 2))); 39 | var ascale =freqlist ** clip(voice,0,4); 40 | var sound = Mix.arFill(15, { arg i; 41 | EnvGen.ar(Env.perc(0.01 * tscale[i], 0.5 * tscale[i], 0.2 * ascale[i]), timeScale: sustain * 5) 42 | * SinOsc.ar(freq * freqlist[i] * Line.kr(1, 1 + accelerate, sustain)) 43 | }); 44 | OffsetOut.ar(out, Pan2.ar(sound * amp / 15, pan)) 45 | }).add 46 | ); 47 | 48 | // Hooking into a nice synth piano already in Supercollider. 49 | // 50 | // Uses the "velocity" parameter to affect how hard the keys are pressed 51 | // "sustain" controls envelope and decay time. 52 | ( 53 | SynthDef(\spiano, { |out, amp=1, sustain=1, pan=0, velocity=1, detune=0.1, muffle=1, stereo=0.2, freq=440| 54 | var env = EnvGen.ar(Env.linen(0.002, 0.996, 0.002, 1, -3), timeScale: sustain, doneAction: 2); 55 | // the +0.01 to freq is because of edge case rounding internal to the MdaPiano synth 56 | var sound = MdaPiano.ar(freq + 0.01, vel: velocity * 100, hard: 0.8 * velocity, decay: 0.1 * sustain, 57 | tune: 0.5, random: 0.05, stretch: detune, muffle: 0.8 * muffle, stereo: stereo); 58 | OffsetOut.ar(out, Pan2.ar(sound * env * amp * 0.35, pan)) 59 | }).add 60 | ); 61 | 62 | // Waveguide mesh, hexagonal drum-like membrane 63 | ( 64 | SynthDef(\shex, { |out, speed=1, sustain=1, pan=0, freq=440, accelerate=0| 65 | var env = EnvGen.ar(Env.linen(0.02, 0.96, 0.02, 1,-3), timeScale: sustain, doneAction: 2); 66 | var tension = 0.05 * freq / 400 * Line.kr(1, accelerate + 1, sustain); 67 | var loss = 1.0 - (0.01 * speed / freq); 68 | var sound = MembraneHexagon.ar(Decay.ar(Impulse.ar(0, 0, 1), 0.01), tension, loss); 69 | OffsetOut.ar(out, Pan2.ar(sound * env, pan)) 70 | }).add 71 | ); 72 | 73 | // Kick Drum using Rumble-San's implementation as a starting point 74 | // http://blog.rumblesan.com/post/53271713518/drum-sounds-in-supercollider-part-1 75 | // 76 | // "n" controls the kick frequency in a nonstandard way 77 | // "sustain" affects overall envelope timescale 78 | // "accelerate" sweeps the click filter freq 79 | // "pitch1" affects the click frequency 80 | // "decay" changes the click duration relative to the overall timescale 81 | ( 82 | SynthDef(\skick, { |out, sustain=1, pan=0, accelerate=0, n=60, pitch1=1, decay=1, amp=1| 83 | var env, sound, dur, clickdur; 84 | env = EnvGen.ar(Env.linen(0.01, 0, 0.5, 1, -3), timeScale: sustain, doneAction: 2); 85 | sound = SinOsc.ar((n - 25.5).midicps); 86 | clickdur = 0.02 * sustain * decay; 87 | sound = sound + (LPF.ar(WhiteNoise.ar(1), 1500 * pitch1 * Line.kr(1, 1 + accelerate, clickdur)) 88 | * Line.ar(1, 0, clickdur)); 89 | OffsetOut.ar(out, Pan2.ar(sound * env, pan, amp)) 90 | }).add 91 | ); 92 | 93 | // A vaguely 808-ish kick drum 94 | // 95 | // "n" controls the chirp frequency 96 | // "sustain" the overall timescale 97 | // "speed" the filter sweep speed 98 | // "voice" the sinewave feedback 99 | ( 100 | SynthDef(\s808, { |out, speed=1, sustain=1, pan=0, voice=0, n=60, amp=1| 101 | var env, sound, freq; 102 | n = ((n>0)*n) + ((n<1)*3); 103 | freq = (n*10).midicps; 104 | env = EnvGen.ar(Env.linen(0.01, 0, 1, 1, -3), timeScale:sustain, doneAction:2); 105 | sound = LPF.ar(SinOscFB.ar(XLine.ar(freq.expexp(10, 2000, 1000, 8000), freq, 0.025/speed), voice), 9000); 106 | OffsetOut.ar(out, Pan2.ar(sound * env, pan, amp)) 107 | }).add 108 | ); 109 | 110 | // Hi-hat using Rumble-San's implementation as a starting point 111 | // http://blog.rumblesan.com/post/53271713518/drum-sounds-in-supercollider-part-1 112 | // 113 | // "n" using it in a weird way to provide some variation on the frequency 114 | // "sustain" affects the overall envelope rate, 115 | // "accelerate" sweeps the filter 116 | ( 117 | SynthDef(\shat, {|out, sustain=1, pan=0, accelerate=0, n=60, amp=1, freq=2000| 118 | var env, sound, accel, frq; 119 | env = EnvGen.ar(Env.linen(0.01, 0, 0.3, 1, -3), timeScale: sustain, doneAction:2); 120 | accel = Line.kr(1, 1+accelerate, 0.2*sustain); 121 | frq = freq*accel*(n/5 + 1).wrap(0.5,2); 122 | sound = HPF.ar(LPF.ar(WhiteNoise.ar(1), 3*frq), frq); 123 | OffsetOut.ar(out, Pan2.ar(sound * env, pan, amp)) 124 | }).add 125 | ); 126 | 127 | // Snare drum using Rumble-San's implementation as a starting point 128 | // http://blog.rumblesan.com/post/53271713909/drum-sounds-in-supercollider-part-2 129 | // 130 | // "n" for some variation on frequency 131 | // "decay" for scaling noise duration relative to tonal part 132 | // "sustain" for overall timescale 133 | // "accelerate" for tonal glide 134 | ( 135 | SynthDef(\ssnare, {|out, sustain=1, pan=0, accelerate=0, n=60, decay=1, amp=1| 136 | var env, sound, accel; 137 | env = EnvGen.ar(Env.linen(0.01, 0, 0.6, 1, -3), timeScale:sustain, doneAction:2); 138 | accel = Line.kr(1, 1+accelerate, 0.2); 139 | sound = LPF.ar(Pulse.ar(100*accel*(n/5+1).wrap(0.5,2)), Line.ar(1030, 30, 0.2*sustain)); 140 | sound = sound + (BPF.ar(HPF.ar(WhiteNoise.ar(1), 500), 1500) * Line.ar(1, 0, 0.2*decay)); 141 | OffsetOut.ar(out, Pan2.ar(sound * env, pan, amp)) 142 | }).add 143 | ); 144 | 145 | // Hand clap using Rumble-San's implementation as a starting point 146 | // http://blog.rumblesan.com/post/53271713909/drum-sounds-in-supercollider-part-2 147 | // 148 | // "delay" controls the echo delay 149 | // "speed" will affect the decay time 150 | // "n" changes how spread is calculated 151 | // "pitch1" will scale the bandpass frequency 152 | // "sustain" the overall timescale 153 | ( 154 | SynthDef(\sclap, {|out, speed=1, sustain=1, pan=0, n=60, delay=1, pitch1=1, amp=1| 155 | var env, sound; 156 | var spr = 0.005 * delay; 157 | env = EnvGen.ar(Env.linen(0.01, 0, 0.6, 1, -3), timeScale:sustain, doneAction:2); 158 | sound = BPF.ar(LPF.ar(WhiteNoise.ar(1), 7500*pitch1), 1500*pitch1); 159 | sound = Mix.arFill(4, {arg i; sound * 0.5 * EnvGen.ar(Env.new([0,0,1,0],[spr*(i**(n.clip(0,5)+1)),0,0.04/speed]))}); 160 | OffsetOut.ar(out, Pan2.ar(sound * env, pan, amp)) 161 | }).add 162 | ); 163 | 164 | // A controllable synth siren, defaults to 1 second, draw it out with "sustain" 165 | ( 166 | SynthDef(\ssiren, {|out, sustain=1, pan=0, freq=440, amp=1| 167 | var env, sound; 168 | env = EnvGen.ar(Env.linen(0.05, 0.9, 0.05, 1, -2), timeScale:sustain, doneAction:2); 169 | sound = VarSaw.ar(freq * (1.0 + EnvGen.kr(Env.linen(0.25,0.5,0.25,3,0), timeScale:sustain, doneAction:2)), 170 | 0, width:Line.kr(0.05,1,sustain)); 171 | OffsetOut.ar(out, Pan2.ar(sound * env, pan, amp)) 172 | }).add 173 | ); 174 | 175 | // The next four synths respond to the following parameters in addition to 176 | // gain, pan, n, and all the "effect" parameters (including attack, hold, and 177 | // release). Default values in parentheses. 178 | // 179 | // sustain - scales overall duration 180 | // decay(0) - amount of decay after initial attack 181 | // accelerate(0) - pitch glide 182 | // semitone(12) - how far off in pitch the secondary oscillator is (need not be integer) 183 | // pitch1(1) - filter frequency scaling multiplier, the frequency itself follows the pitch set by "n" 184 | // speed(1)- LFO rate 185 | // lfo(1) - how much the LFO affects the filter frequency 186 | // resonance(0.2) - filter resonance 187 | // voice(0.5) - depends on the individual synth 188 | 189 | // A moog-inspired square-wave synth; variable-width pulses with filter 190 | // frequency modulated by an LFO 191 | // 192 | // "voice" controls the pulse width (exactly zero or one will make no sound) 193 | ( 194 | SynthDef(\ssquare, {|out, speed=1, decay=0, sustain=1, pan=0, accelerate=0, freq=440, 195 | voice=0.5, semitone=12, resonance=0.2, lfo=1, pitch1=1, amp=1| 196 | var env = EnvGen.ar(Env.pairs([[0,0],[0.05,1],[0.2,1-decay],[0.95,1-decay],[1,0]], -3), timeScale:sustain, doneAction:2); 197 | var basefreq = freq* Line.kr(1, 1+accelerate, sustain); 198 | var basefreq2 = basefreq / (2**(semitone/12)); 199 | var lfof1 = min(basefreq*10*pitch1, 22000); 200 | var lfof2 = min(lfof1 * (lfo + 1), 22000); 201 | var sound = (0.7 * Pulse.ar(basefreq, voice)) + (0.3 * Pulse.ar(basefreq2, voice)); 202 | sound = MoogFF.ar( 203 | sound, 204 | SinOsc.ar(basefreq/64*speed, 0).range(lfof1,lfof2), 205 | resonance*4); 206 | sound = sound.tanh * 2; 207 | OffsetOut.ar(out, Pan2.ar(sound * env, pan, amp)); 208 | }).add 209 | ); 210 | 211 | // A moog-inspired sawtooth synth; slightly detuned saws with triangle 212 | // harmonics, filter frequency modulated by LFO 213 | // 214 | // "voice" controls a relative phase and detune amount 215 | ( 216 | SynthDef(\ssaw, {|out, speed=1, decay=0, sustain=1, pan=0, accelerate=0, freq=440, 217 | voice=0.5, semitone=12, resonance=0.2, lfo=1, pitch1=1, amp=1| 218 | var env = EnvGen.ar(Env.pairs([[0,0],[0.05,1],[0.2,1-decay],[0.95,1-decay],[1,0]], -3), timeScale:sustain, doneAction:2); 219 | var basefreq = freq * Line.kr(1, 1+accelerate, sustain); 220 | var basefreq2 = basefreq * (2**(semitone/12)); 221 | var lfof1 = min(basefreq*10*pitch1, 22000); 222 | var lfof2 = min(lfof1 * (lfo + 1), 22000); 223 | var sound = MoogFF.ar( 224 | (0.5 * Mix.arFill(3, {|i| SawDPW.ar(basefreq * ((i-1)*voice/50+1), 0)})) + (0.5 * LFTri.ar(basefreq2, voice)), 225 | LFTri.ar(basefreq/64*speed, 0.5).range(lfof1,lfof2), 226 | resonance*4); 227 | sound = sound.tanh*2; 228 | OffsetOut.ar(out, Pan2.ar(sound * env, pan, amp)); 229 | }).add 230 | ); 231 | 232 | // A moog-inspired PWM synth; pulses multiplied by phase-shifted pulses, double 233 | // filtering with an envelope on the second. 234 | // 235 | // "voice" controls the phase shift rate 236 | ( 237 | SynthDef(\spwm, {|out, speed=1, decay=0, sustain=1, pan=0, accelerate=0, freq=440, 238 | voice=0.5, semitone=12, resonance=0.2, lfo=1, pitch1=1, amp=1| 239 | var env = EnvGen.ar(Env.pairs([[0,0],[0.05,1],[0.2,1-decay],[0.95,1-decay],[1,0]], -3), timeScale:sustain, doneAction:2); 240 | var env2 = EnvGen.ar(Env.pairs([[0,0.1],[0.1,1],[0.4,0.5],[0.9,0.2],[1,0.2]], -3), timeScale:sustain/speed); 241 | var basefreq = freq * Line.kr(1, 1+accelerate, sustain); 242 | var basefreq2 = basefreq / (2**(semitone/12)); 243 | var lfof1 = min(basefreq*10*pitch1, 22000); 244 | var lfof2 = min(lfof1 * (lfo + 1), 22000); 245 | var sound = 0.7 * PulseDPW.ar(basefreq) * DelayC.ar(PulseDPW.ar(basefreq), 0.2, Line.kr(0,voice,sustain)/basefreq); 246 | sound = 0.3 * PulseDPW.ar(basefreq2) * DelayC.ar(PulseDPW.ar(basefreq2), 0.2, Line.kr(0.1,0.1+voice,sustain)/basefreq) + sound; 247 | sound = MoogFF.ar(sound, SinOsc.ar(basefreq/32*speed, 0).range(lfof1,lfof2), resonance*4); 248 | sound = MoogFF.ar(sound, min(env2*lfof2*1.1, 22000), 3); 249 | sound = sound.tanh*5; 250 | OffsetOut.ar(out, Pan2.ar(sound * env, pan, amp)); 251 | }).add 252 | ); 253 | 254 | // This synth is inherently stereo, so handles the "pan" parameter itself and 255 | // tells SuperDirt not to mix down to mono. 256 | // 257 | // "voice" scales the comparator frequencies, higher values will sound "breathier" 258 | ( 259 | SynthDef(\scomparator, {|out, speed=1, decay=0, sustain=1, pan=0, accelerate=0, freq=440, 260 | voice=0.5, resonance=0.5, lfo=1, pitch1=1, amp=1| 261 | var env = EnvGen.ar(Env.pairs([[0,0],[0.05,1],[0.2,1-decay],[0.95,1-decay],[1,0]], -3), timeScale:sustain, doneAction:2); 262 | var basefreq = freq * Line.kr(1, 1+accelerate, sustain); 263 | var sound = VarSaw.ar(basefreq, 0, Line.ar(0,1,sustain)); 264 | var freqlist =[ 1.000, 2.188, 5.091, 8.529, 8.950, 9.305, 13.746, 14.653, 19.462, 22.003, 24.888, 25.991, 265 | 26.085, 30.509, 33.608, 35.081, 40.125, 42.023, 46.527, 49.481]**(voice/5); 266 | sound = Splay.arFill(16, {|i| sound > LFTri.ar(freqlist[i])}, 1); 267 | sound = MoogFF.ar( 268 | sound, 269 | pitch1 * 4 * basefreq + SinOsc.ar(basefreq/64*speed, 0, lfo*basefreq/2) + LFNoise2.ar(1,lfo*basefreq), 270 | LFNoise2.ar(0,0.1,4*resonance)); 271 | sound = 0.5 * Balance2.ar(sound[0], sound[1], pan*2-1); 272 | OffsetOut.ar(out, Pan2.ar(sound * env, 0.5, amp)); 273 | }).add 274 | ); 275 | 276 | // Uses the Atari ST emulation UGen with 3 oscillators 277 | // 278 | // "slide" is for a linear frequency glide that will repeat "speed" times (can 279 | // be fractional or negative). 280 | // "accelerate" is for an overall glide 281 | // "pitch2" and "pitch3" control the ratio of harmonics 282 | // "voice" causes variations in the levels of the 3 oscillators 283 | ( 284 | SynthDef(\schip, {|out, sustain=1, pan=0, freq=440, speed=1, slide=0, pitch2=2, pitch3=3, accelerate=0, voice=0, amp=1| 285 | var env, basefreq, sound, va, vb, vc; 286 | env = EnvGen.ar(Env.linen(0.01, 0.98, 0.01,1,-1), timeScale:sustain, doneAction:2); 287 | basefreq = freq + wrap2(slide * 100 * Line.kr(-1,1+(2*speed-2),sustain), slide * 100); 288 | basefreq = basefreq * Line.kr(1, accelerate+1, sustain); 289 | va = (voice < 0.5) * 15; 290 | vb = ((2*voice) % 1 < 0.5) * 15; 291 | vc = ((4*voice) % 1 < 0.5) * 15; 292 | sound= AY.ar( AY.freqtotone(basefreq), AY.freqtotone(pitch2*basefreq), AY.freqtotone(pitch3*basefreq), 293 | vola:va, volb:vb, volc:vc)/2; 294 | sound = tanh(sound)*2; 295 | OffsetOut.ar(out, Pan2.ar(sound * env, pan, amp)); 296 | }).add 297 | ); 298 | 299 | // Digital noise in several flavors with a bandpass filter 300 | // 301 | // "voice" at 0 is a digital noise for which "n" controls rate, at 1 is 302 | // Brown+White noise for which "n" controls knee frequency. 303 | // "accelerate" causes glide in n, "speed" will cause it to repeat 304 | // "pitch1" scales the bandpass frequency (which tracks "n") 305 | // "slide" works like accelerate on the bandpass 306 | // "resonance" is the filter resonance 307 | ( 308 | SynthDef(\snoise, {|out, sustain=1, pan=0, freq=440, accelerate=0, slide=0, pitch1=1, speed=1, resonance=0, voice=0, amp=1| 309 | var env, basefreq, sound, ffreq, acc; 310 | env = EnvGen.ar(Env.linen(0.01, 0.98, 0.01,1,-1), timeScale:sustain, doneAction:2); 311 | acc = accelerate * freq * 4; 312 | basefreq = freq * 8 + wrap2(acc* Line.kr(-1,1+(2*speed-2), sustain), acc); 313 | ffreq = basefreq*5*pitch1* Line.kr(1,1+slide, sustain); 314 | ffreq = clip(ffreq, 60,20000); 315 | sound = XFade2.ar( LFDNoise0.ar(basefreq.min(22000), 0.5), 316 | XFade2.ar(BrownNoise.ar(0.5), WhiteNoise.ar(0.5), basefreq.cpsmidi/127), 317 | 2*voice-1); 318 | sound = HPF.ar(BMoog.ar(sound, ffreq, resonance, 3), 20); 319 | sound = clip(sound, -1,1) * 0.3; 320 | OffsetOut.ar(out, Pan2.ar(sound * env, pan, amp)); 321 | }).add 322 | ); 323 | -------------------------------------------------------------------------------- /test/bjorklund_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe Xi::Bjorklund do 4 | def e(*args) 5 | Xi::Bjorklund.new(*args) 6 | end 7 | 8 | it "e(3,5)" do 9 | assert_equal "x.x.x", e(3,5).to_s 10 | end 11 | 12 | it "e(4,7)" do 13 | assert_equal 'x.x.x.x', e(4,7).to_s 14 | end 15 | 16 | it "e(5,7)" do 17 | assert_equal 'x.xx.xx', e(5,7).to_s 18 | end 19 | 20 | it "e(2,8)" do 21 | assert_equal 'x...x...', e(2,8).to_s 22 | end 23 | 24 | it "e(3,8)" do 25 | assert_equal 'x..x..x.', e(3,8).to_s 26 | end 27 | 28 | it "e(4,8)" do 29 | assert_equal 'x.x.x.x.', e(4,8).to_s 30 | end 31 | 32 | it "e(5,8)" do 33 | assert_equal 'x.xx.xx.', e(5,8).to_s 34 | end 35 | 36 | it "e(7,8)" do 37 | assert_equal 'x.xxxxxx', e(7,8).to_s 38 | end 39 | 40 | it "e(5,9)" do 41 | assert_equal 'x.x.x.x.x', e(5,9).to_s 42 | end 43 | 44 | it "e(5,12)" do 45 | assert_equal 'x..x.x..x.x.', e(5,12).to_s 46 | end 47 | 48 | it "e(5,16)" do 49 | assert_equal 'x..x..x..x..x...', e(5,16).to_s 50 | end 51 | 52 | it "e(7,16)" do 53 | assert_equal 'x..x.x.x..x.x.x.', e(7,16).to_s 54 | end 55 | 56 | it "e(9,16)" do 57 | assert_equal 'x.xx.x.x.xx.x.x.', e(9,16).to_s 58 | end 59 | 60 | it "e(10,16)" do 61 | assert_equal 'x.xx.x.xx.xx.x.x', e(10,16).to_s 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/core_ext_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'xi/scale' 3 | 4 | describe '#p' do 5 | describe Array do 6 | it 'creates a Pattern from an Array' do 7 | assert_pattern [1, 2, 3], [1, 2, 3].p 8 | end 9 | end 10 | 11 | describe Enumerator do 12 | it 'creates a Pattern from an Enumerator' do 13 | enum = (1..10).lazy.take(5) 14 | assert_pattern enum, enum.p 15 | end 16 | end 17 | 18 | describe Range do 19 | it 'creates a Pattern from a Range' do 20 | assert_pattern 1..7, (1..7).p 21 | end 22 | end 23 | 24 | describe Integer do 25 | it 'creates a Pattern from a Integer' do 26 | assert_pattern [42], 42.p 27 | end 28 | end 29 | 30 | describe Float do 31 | it 'creates a Pattern from a Float' do 32 | assert_pattern [3.14], 3.14.p 33 | end 34 | end 35 | 36 | describe String do 37 | it 'creates a Pattern from a String' do 38 | assert_pattern ['kick'], 'kick'.p 39 | end 40 | end 41 | 42 | describe Symbol do 43 | it 'creates a Pattern from a Symbol' do 44 | assert_pattern [:kick], :kick.p 45 | end 46 | end 47 | 48 | describe Rational do 49 | it 'creates a Pattern from a Rational' do 50 | assert_pattern [1/8], (1/8).p 51 | end 52 | end 53 | 54 | describe Hash do 55 | it 'creates a Pattern from a Hash' do 56 | h = {degree: [0,1,2], scale: [Xi::Scale.major]} 57 | assert_instance_of Xi::Pattern, h.p 58 | assert_equal [h], h.p.take_values(1) 59 | end 60 | end 61 | end 62 | 63 | describe Integer do 64 | describe '#/' do 65 | it 'divides number and casts it as a Rational' do 66 | assert_equal 1/2.to_r, 1/2 67 | end 68 | end 69 | end 70 | 71 | describe Numeric do 72 | describe '#db_to_amp' do 73 | it 'converts db to amp' do 74 | assert_equal 1/10, -20.db_to_amp 75 | end 76 | end 77 | 78 | describe '#degree_to_key' do 79 | it 'converts a degree number to key from a +scale+ and +steps_per_octave+' do 80 | assert_equal 4, 2.degree_to_key(Xi::Scale.major, 12) 81 | assert_equal 3, 2.degree_to_key(Xi::Scale.minor, 12) 82 | end 83 | end 84 | end 85 | 86 | describe Enumerator do 87 | describe '#next?' do 88 | it 'returns true if there is a "next" element' do 89 | assert [1].each.next? 90 | refute [].each.next? 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /test/pattern/generators_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe Xi::Pattern::Generators do 4 | describe '.series' do 5 | it do 6 | @p = P.series 7 | assert_equal (0..9).to_a, @p.take_values(10) 8 | assert @p.infinite? 9 | end 10 | 11 | it 'accepts a starting number' do 12 | @p = P.series(3) 13 | assert_equal [3, 4, 5, 6, 7, 8, 9, 10, 11, 12], @p.take_values(10) 14 | assert @p.infinite? 15 | end 16 | 17 | it 'accepts starting and step numbers' do 18 | @p = P.series(0, 2) 19 | assert_equal [0, 2, 4, 6, 8, 10, 12, 14, 16, 18], @p.take_values(10) 20 | assert @p.infinite? 21 | end 22 | 23 | it 'accepts starting, step and length numbers' do 24 | @p = P.series(0, 0.25, 8) 25 | assert_equal [0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75], @p.to_a 26 | assert @p.finite? 27 | assert_equal 8, @p.size 28 | end 29 | end 30 | 31 | describe '.geom' do 32 | it do 33 | @p = P.geom 34 | assert_equal [0] * 10, @p.take_values(10) 35 | assert @p.infinite? 36 | end 37 | 38 | it 'accepts a starting number' do 39 | @p = P.geom(3) 40 | assert_equal [3] * 10, @p.take_values(10) 41 | assert @p.infinite? 42 | end 43 | 44 | it 'accepts starting and step numbers' do 45 | @p = P.geom(1, 2) 46 | assert_equal [1, 2, 4, 8, 16, 32, 64, 128, 256, 512], @p.take_values(10) 47 | assert @p.infinite? 48 | end 49 | 50 | it 'accepts starting, step and length numbers' do 51 | @p = P.geom(1, -1, 8) 52 | assert_equal [1, -1, 1, -1, 1, -1, 1, -1], @p.to_a 53 | assert @p.finite? 54 | assert_equal 8, @p.size 55 | end 56 | end 57 | 58 | describe '.rand' do 59 | it do 60 | @p = P.rand(1..5) 61 | assert @p.finite? 62 | assert_equal 1, @p.size 63 | assert (1..5).include?(@p.each.first) 64 | 65 | @p = P.rand(1..5, 6) 66 | assert @p.finite? 67 | assert_equal 6, @p.size 68 | assert @p.each.all? { |v| (1..5).include?(v) } 69 | end 70 | end 71 | 72 | describe '.xrand' do 73 | it do 74 | @p = P.xrand(1..5) 75 | assert @p.finite? 76 | assert_equal 1, @p.size 77 | assert (1..5).each.include?(@p.each.first) 78 | 79 | @p = P.xrand(1..3, 9) 80 | assert @p.finite? 81 | assert_equal 9, @p.size 82 | @p.each.each_slice(3) do |slice| 83 | assert_equal (1..3).to_a, slice.sort 84 | end 85 | end 86 | end 87 | 88 | describe '.shuf' do 89 | it do 90 | @p = P.shuf(1..5) 91 | assert @p.finite? 92 | assert_equal 5, @p.size 93 | assert (1..5).include?(@p.each.first) 94 | 95 | @p = P.shuf(1..3, 3) 96 | assert @p.finite? 97 | assert_equal 9, @p.size 98 | @p.each.each_slice(3) do |slice| 99 | assert_equal (1..3).to_a, slice.sort 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /test/pattern/transforms_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe Xi::Pattern::Transforms do 4 | describe '#-@' do 5 | it 'returns a new pattern with inverted numbers' do 6 | @p = -(1..5).p 7 | assert_equal [-1, -2, -3, -4, -5], @p.to_a 8 | assert_equal 5, @p.size 9 | end 10 | 11 | it 'preserves non-numeric values' do 12 | @p = -[1, 42, 'w', [4, 5], [10].p].p 13 | assert_equal [-1, -42, 'w', [4, 5], -[10].p], @p.to_a 14 | assert_equal 5, @p.size 15 | end 16 | end 17 | 18 | describe '#+' do 19 | describe 'when RHS is another Pattern' do 20 | it 'returns a new pattern by concatenation' do 21 | @p = [10].p + [1,2,3].p 22 | assert_equal [10, 1, 2, 3], @p.to_a 23 | assert_equal 4, @p.size 24 | end 25 | end 26 | 27 | describe 'when RHS has #+ defined' do 28 | it 'performs a scalar sum' do 29 | @p = (1..5).p + 100 30 | assert_equal [101, 102, 103, 104, 105], @p.to_a 31 | assert_equal 5, @p.size 32 | end 33 | end 34 | end 35 | 36 | describe '#-' do 37 | describe 'when RHS has #- defined' do 38 | it 'performs a scalar substraction' do 39 | @p = (1..5).p - 10 40 | assert_equal [-9, -8, -7, -6, -5], @p.to_a 41 | assert_equal 5, @p.size 42 | end 43 | end 44 | end 45 | 46 | describe '#*' do 47 | describe 'when RHS has #* defined' do 48 | it 'performs a scalar product' do 49 | @p = [2, 4, 6].p * 3 50 | assert_equal [6, 12, 18], @p.to_a 51 | assert_equal 3, @p.size 52 | end 53 | end 54 | end 55 | 56 | describe '#/' do 57 | describe 'when RHS has #/ defined' do 58 | it 'performs a scalar floating-point division' do 59 | @p = [1, 2, 3].p / 2 60 | assert_equal [0.5, 1, 1.5], @p.to_a 61 | assert_equal 3, @p.size 62 | end 63 | end 64 | end 65 | 66 | describe '#%' do 67 | describe 'when RHS has #/ defined' do 68 | it 'performs a scalar floating-point division' do 69 | @p = (1..7).p % 2 70 | assert_equal [1, 0, 1, 0, 1, 0, 1], @p.to_a 71 | assert_equal 7, @p.size 72 | end 73 | end 74 | end 75 | 76 | describe '#**' do 77 | describe 'when RHS has #** defined' do 78 | it 'performs a scalar exponentiation' do 79 | @p = [2, 4, 6].p ** 3 80 | assert_equal [8, 64, 216], @p.to_a 81 | assert_equal 3, @p.size 82 | end 83 | end 84 | end 85 | 86 | describe '#^' do 87 | it 'is an alias of #**' do 88 | @p = [2, 4, 6].p ^ 3 89 | assert_equal [8, 64, 216], @p.to_a 90 | assert_equal 3, @p.size 91 | end 92 | end 93 | 94 | describe '#seq' do 95 | it 'fails if repeats or offset are not valid' do 96 | assert_raises(ArgumentError) { [].p.seq(-4) } 97 | assert_raises(ArgumentError) { [].p.seq("foo") } 98 | assert_raises(ArgumentError) { [].p.seq(1, :bar) } 99 | end 100 | 101 | it 'cycles sequentially :repeats times' do 102 | @p = [1, 2, 3].p.seq 103 | assert_equal [1, 2, 3], @p.to_a 104 | assert_equal 3, @p.size 105 | 106 | @p = [1, 2, 3].p.seq(2) 107 | assert_equal [1, 2, 3, 1, 2, 3], @p.to_a 108 | assert_equal 6, @p.size 109 | 110 | @p = [1, 2, 3].p.seq(0) 111 | assert_equal [], @p.to_a 112 | assert_equal 0, @p.size 113 | end 114 | 115 | it 'cycles the pattern with a different starting offset' do 116 | @p = (1..5).p.seq(1, 0) 117 | assert_equal [1, 2, 3, 4, 5], @p.to_a 118 | assert_equal 5, @p.size 119 | 120 | @p = (1..5).p.seq(1, 2) 121 | assert_equal [3, 4, 5, 1, 2], @p.to_a 122 | assert_equal 5, @p.size 123 | 124 | @p = (1..5).p.seq(1, 4) 125 | assert_equal [5, 1, 2, 3, 4], @p.to_a 126 | assert_equal 5, @p.size 127 | 128 | @p = (1..5).p.seq(1, 5) 129 | assert_equal [1, 2, 3, 4, 5], @p.to_a 130 | assert_equal 5, @p.size 131 | end 132 | end 133 | 134 | describe '#bounce' do 135 | describe 'with no parameters' do 136 | it 'traverses pattern in original order and then in reverse order without first and last values' do 137 | @p = (1..5).p.bounce.to_a 138 | assert_equal (1..4).to_a + (2..5).to_a.reverse, @p 139 | assert_equal 8, @p.size 140 | end 141 | end 142 | 143 | describe 'with parameter false' do 144 | it 'traverses pattern in original order and then in reverse order' do 145 | @p = (1..5).p.bounce(false).to_a 146 | assert_equal (1..5).to_a + (1..5).to_a.reverse, @p 147 | assert_equal 10, @p.size 148 | end 149 | end 150 | end 151 | 152 | describe '#normalize' do 153 | it 'normalizes values from a custom range' do 154 | @p = (1..5).p.normalize(0, 100).to_a 155 | assert_equal [(1/100), (1/50), (3/100), (1/25), (1/20)], @p 156 | assert_equal 5, @p.size 157 | end 158 | end 159 | 160 | describe '#denormalize' do 161 | it 'scales back normalized values to a custom range' do 162 | @p = [0, 0.25, 0.50, 0.75].p.denormalize(0, 0x100).to_a 163 | assert_equal [0, 64.0, 128.0, 192.0], @p 164 | assert_equal 4, @p.size 165 | end 166 | end 167 | 168 | describe '#scale' do 169 | it 'scales values from a range to another' do 170 | @p = [0, 1, 2, 3].p.scale(0, 4, 0, 0x80).to_a 171 | assert_equal [(0/1), (32/1), (64/1), (96/1)], @p 172 | assert_equal 4, @p.size 173 | end 174 | end 175 | end 176 | -------------------------------------------------------------------------------- /test/pattern_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe Xi::Pattern do 4 | describe '#new' do 5 | it 'takes an array as +source+' do 6 | @p = Xi::Pattern.new([1, 2, 3]) 7 | 8 | assert_instance_of Xi::Pattern, @p 9 | assert_equal [1, 2, 3], @p.source 10 | end 11 | 12 | it 'takes a block as +source+' do 13 | @p = Xi::Pattern.new { |y| 14 | y << 1 15 | y << [10, 20] 16 | y << :foo 17 | } 18 | 19 | assert_instance_of Xi::Pattern, @p 20 | assert_instance_of Proc, @p.source 21 | assert_equal [1, [10, 20], :foo], @p.take_values(3) 22 | end 23 | 24 | it 'takes a block as +source+ that yields [v, s, d]' do 25 | @p = Xi::Pattern.new(delta: 1/2) { |y, d| 26 | (1..inf).each { |v| y << v } 27 | } 28 | 29 | assert_instance_of Xi::Pattern, @p 30 | assert_instance_of Proc, @p.source 31 | assert_equal (1..10).to_a, @p.take_values(10) 32 | end 33 | 34 | it 'takes a Pattern instance as +source+' do 35 | p1 = Xi::Pattern.new([1, 2, 3]) 36 | @p = Xi::Pattern.new(p1) 37 | 38 | assert_instance_of Xi::Pattern, @p 39 | assert_equal p1.source, @p.source 40 | assert_equal [1, 2, 3], @p.take_values(3) 41 | end 42 | 43 | it 'accepts +delta+, which defaults to 1' do 44 | @p = Xi::Pattern.new([1]) 45 | assert_equal 1, @p.delta 46 | 47 | @p = Xi::Pattern.new([1], delta: 1/4) 48 | assert_equal 1/4, @p.delta 49 | end 50 | 51 | it 'accepts metadata as keyword arguments' do 52 | @p = Xi::Pattern.new([1], delta: 1/2, foo: :bar) 53 | 54 | assert_equal 1/2, @p.delta 55 | assert_equal({foo: :bar}, @p.metadata) 56 | end 57 | 58 | it 'raises ArgumentError if neither source or block are provided' do 59 | assert_raises(ArgumentError) do 60 | @p = Xi::Pattern.new 61 | end 62 | end 63 | 64 | it 'raises ArgumentError if delta is infinite' do 65 | assert_raises(ArgumentError) do 66 | @p = Xi::Pattern.new([1, 2, 3], delta: (1..inf)) 67 | end 68 | 69 | assert_raises(ArgumentError) do 70 | @p = Xi::Pattern.new([1, 2, 3], delta: P.series) 71 | end 72 | end 73 | end 74 | 75 | describe '.[]' do 76 | it 'constructs a new Pattern, same as .new' do 77 | assert_equal Xi::Pattern[1,2,3], 78 | Xi::Pattern.new([1,2,3]) 79 | 80 | assert_equal Xi::Pattern[1,2,3, delta: 1/2], 81 | Xi::Pattern.new([1,2,3], delta: 1/2) 82 | 83 | assert_equal Xi::Pattern[1,2,3, delta: 1/2, gate: :note], 84 | Xi::Pattern.new([1,2,3], delta: 1/2, gate: :note) 85 | end 86 | end 87 | 88 | describe '#p' do 89 | before do 90 | @p = Xi::Pattern.new([1], delta: 2, baz: 1) 91 | end 92 | 93 | it 'returns a new Pattern with the same source' do 94 | assert_equal @p.p.source.object_id, @p.source.object_id 95 | end 96 | 97 | it 'accepts delta values as first parameter, and overrides original' do 98 | assert_equal 2, @p.delta 99 | assert_equal 1/2, @p.p(1/2).delta 100 | assert_equal [1/4, 1/8, 1/16], @p.p(1/4, 1/8, 1/16).delta 101 | assert_equal [1,2,3], @p.p(P[1,2,3]).delta 102 | end 103 | 104 | it 'accepts metadata as keyword arguments, and is merged with original' do 105 | assert_equal({baz: 1}, @p.metadata) 106 | assert_equal({foo: :bar, baz: 1}, @p.p(foo: :bar).metadata) 107 | end 108 | end 109 | 110 | describe '#finite? and #infinite?' do 111 | it 'returns true or false whether pattern has a finite or infinite size' do 112 | @p = Xi::Pattern.new(1..10) 113 | assert @p.finite? 114 | refute @p.infinite? 115 | 116 | @p = Xi::Pattern.new { |y| y << rand } 117 | refute @p.finite? 118 | assert @p.infinite? 119 | 120 | @p = Xi::Pattern.new(size: 4) { |y| y << rand } 121 | assert @p.finite? 122 | refute @p.infinite? 123 | 124 | @p = Xi::Pattern.new(1..inf) 125 | refute @p.finite? 126 | assert @p.infinite? 127 | end 128 | end 129 | 130 | describe '#each_event' do 131 | describe 'with no block' do 132 | it 'returns an Enumerator' do 133 | assert_instance_of Enumerator, [].p.each_event 134 | end 135 | end 136 | 137 | describe 'when source responds to #call (e.g. a block)' do 138 | it 'yields events and current iteration' do 139 | @p = Xi::Pattern.new(size: 3) { |y| (1..3).each { |v| y << v } } 140 | 141 | assert_equal [[1, 0, 1, 0], 142 | [2, 1, 1, 0], 143 | [3, 2, 1, 0], 144 | [1, 3, 1, 1]], @p.each_event.take(4) 145 | 146 | assert_equal [[1, 3, 1, 1], 147 | [2, 4, 1, 1], 148 | [3, 5, 1, 1], 149 | [1, 6, 1, 2]], @p.each_event(3).take(4) 150 | 151 | assert_equal [1, 3, 1, 1], @p.each_event(3.02).first 152 | assert_equal [1, 3, 1, 1], @p.each_event(3.95).first 153 | assert_equal [2, 4, 1, 1], @p.each_event(4).first 154 | assert_equal [2, 4, 1, 1], @p.each_event(4.1).first 155 | end 156 | end 157 | 158 | describe 'when source responds to #[] and #size (e.g. an Array)' do 159 | it 'yields events and current iteration' do 160 | @p = Xi::Pattern.new([1, 2], delta: 1/4) 161 | 162 | assert_equal [[1, 0, 1/4, 0], 163 | [2, 1/4, 1/4, 0], 164 | [1, 1/2, 1/4, 1], 165 | [2, 3/4, 1/4, 1]], @p.each_event.take(4) 166 | 167 | assert_equal [[2, 1/4, 1/4, 0], 168 | [1, 1/2, 1/4, 1], 169 | [2, 3/4, 1/4, 1], 170 | [1, 1, 1/4, 2]], @p.each_event(0.26).take(4) 171 | 172 | assert_equal [[1, 1/2, 1/4, 1], 173 | [2, 3/4, 1/4, 1], 174 | [1, 1, 1/4, 2], 175 | [2, 5/4, 1/4, 2]], @p.each_event(1/2 + 0.1).take(4) 176 | 177 | @p = Xi::Pattern.new([:a, :b, :c], delta: [1/2, 1/4]) 178 | 179 | assert_equal [[:b, 42, 1/2, 18], 180 | [:c, 85/2, 1/4, 18], 181 | [:a, 171/4, 1/2, 19], 182 | [:b, 173/4, 1/4, 19]], @p.each_event(42).take(4) 183 | end 184 | end 185 | 186 | describe 'when source responds to #each_event (e.g. a Pattern)' do 187 | it 'yields events and current iteration' do 188 | @p = Xi::Pattern.new([1, 2].p, delta: 1/4) 189 | 190 | assert_equal [[1, 1/2, 1/4, 1], 191 | [2, 3/4, 1/4, 1], 192 | [1, 1, 1/4, 2], 193 | [2, 5/4, 1/4, 2]], @p.each_event(1/2 + 0.1).take(4) 194 | end 195 | end 196 | end 197 | 198 | describe '#each_delta' do 199 | describe 'with no block' do 200 | it 'returns an Enumerator' do 201 | assert_instance_of Enumerator, [].p.each_delta 202 | end 203 | end 204 | 205 | describe 'when delta is an Array' do 206 | it 'yields next delta value for current +iteration+' do 207 | @p = Xi::Pattern.new(%i(a b c), delta: [1, 2, 3]) 208 | 209 | assert_equal [1, 2, 3, 1], @p.each_delta.take(4) 210 | assert_equal [2, 3, 1, 2], @p.each_delta(1).take(4) 211 | assert_equal [2, 3, 1, 2], @p.each_delta(1.9).take(4) 212 | assert_equal [3, 1, 2, 3], @p.each_delta(2).take(4) 213 | end 214 | end 215 | 216 | describe 'when delta is a Pattern' do 217 | it 'yields next delta value for current +iteration+' do 218 | @p = Xi::Pattern.new(%i(a b c), delta: [1/2, 1/4].p * 2) 219 | 220 | assert_equal [1, 1/2, 1, 1/2], @p.each_delta.take(4) 221 | assert_equal [1/2, 1, 1/2, 1], @p.each_delta(1).take(4) 222 | end 223 | end 224 | 225 | describe 'when delta is a Numeric' do 226 | it 'yields next delta value for current +iteration+' do 227 | @p = Xi::Pattern.new(%i(a b c), delta: 2) 228 | 229 | assert_equal [2, 2, 2], @p.each_delta.take(3) 230 | assert_equal [2, 2, 2], @p.each_delta(1).take(3) 231 | end 232 | end 233 | end 234 | 235 | describe '#each' do 236 | it 'returns an Enumerator if block is not present' do 237 | assert_instance_of Enumerator, [].p.each 238 | end 239 | 240 | it 'returns all values from the first iteration' do 241 | @p = Xi::Pattern.new([1, 2, 3]) 242 | assert_equal [1, 2, 3], @p.each.to_a 243 | 244 | @p = [1, 2, 3].p + [4, 5, 6].p 245 | assert_equal [1, 2, 3, 4, 5, 6], @p.each.to_a 246 | end 247 | end 248 | 249 | describe '#reverse_each' do 250 | it 'returns an Enumerator if block is not present' do 251 | assert_instance_of Enumerator, [].p.each 252 | end 253 | 254 | it 'returns all values from the first iteration in reverse order' do 255 | @p = Xi::Pattern.new([1, 2, 3]) 256 | assert_equal [3, 2, 1], @p.reverse_each.to_a 257 | 258 | @p = [1, 2, 3].p + [4, 5, 6].p 259 | assert_equal [6, 5, 4, 3, 2, 1], @p.reverse_each.to_a 260 | end 261 | end 262 | 263 | describe '#to_a' do 264 | it 'returns an Array of values from the first iteration' do 265 | assert_equal [1, 2, 3], Xi::Pattern.new([1, 2, 3]).to_a 266 | end 267 | 268 | it 'raises an error if pattern is infinite' do 269 | assert_raises(StandardError) do 270 | Xi::Pattern.new(1..inf).to_a 271 | end 272 | end 273 | end 274 | 275 | describe '#to_events' do 276 | it 'returns an Array of events from the first iteration' do 277 | assert_equal [[1, 0, 1, 0], 278 | [2, 1, 1, 0], 279 | [3, 2, 1, 0]], Xi::Pattern.new([1, 2, 3]).to_events 280 | end 281 | 282 | it 'raises an error if pattern is infinite' do 283 | assert_raises(StandardError) do 284 | Xi::Pattern.new(1..inf).to_events 285 | end 286 | end 287 | end 288 | 289 | describe '#map' do 290 | before do 291 | @p = Xi::Pattern.new([:a, :b], delta: 1/2) 292 | end 293 | 294 | it 'returns a new Pattern with events mapped to the block' do 295 | new_p = @p.map { |v, s, d, i| "#{v}#{(s * 10).to_i}" } 296 | 297 | assert_instance_of Xi::Pattern, new_p 298 | assert_equal [["a0",0,1/2,0], ["b5",1/2,1/2,0]], new_p.to_events 299 | end 300 | 301 | it 'is an alias of #collect' do 302 | assert_equal [["a0",0,1/2,0], ["b5",1/2,1/2,0]], 303 | @p.collect { |v, s, d, i| "#{v}#{(s * 10).to_i}" }.to_events 304 | end 305 | end 306 | 307 | describe '#select' do 308 | before do 309 | @p = Xi::Pattern.new((1..4).to_a) 310 | end 311 | 312 | it 'returns a new Pattern with events selected from the block' do 313 | new_p = @p.select { |v| v % 2 == 0 } 314 | 315 | assert_instance_of Xi::Pattern, new_p 316 | assert_equal [[2, 0, 1, 0], 317 | [4, 1, 1, 0], 318 | [2, 2, 1, 0], 319 | [4, 3, 1, 0]], new_p.to_events 320 | end 321 | 322 | it 'is an alias of #find_all' do 323 | assert_equal [[2, 0, 1, 0], 324 | [4, 1, 1, 0], 325 | [2, 2, 1, 0], 326 | [4, 3, 1, 0]], @p.find_all { |v| v % 2 == 0 }.to_events 327 | end 328 | end 329 | 330 | describe '#reject' do 331 | before do 332 | @p = Xi::Pattern.new((1..4).to_a) 333 | end 334 | 335 | it 'returns a new Pattern with events rejected from the block' do 336 | new_p = @p.reject { |v| v % 2 == 0 } 337 | 338 | assert_instance_of Xi::Pattern, new_p 339 | assert_equal [[1, 0, 1, 0], 340 | [3, 1, 1, 0], 341 | [1, 2, 1, 0], 342 | [3, 3, 1, 0]], new_p.to_events 343 | end 344 | end 345 | 346 | describe '#take' do 347 | before do 348 | @p = Xi::Pattern.new([1, 2], delta: 2) 349 | end 350 | 351 | it 'returns the first +n+ events, starting from +cycle+' do 352 | assert_equal [[1, 0, 2, 0], 353 | [2, 2, 2, 0], 354 | [1, 4, 2, 1], 355 | [2, 6, 2, 1]], @p.take(4) 356 | 357 | assert_equal [[2, 2, 2, 0], 358 | [1, 4, 2, 1], 359 | [2, 6, 2, 1]], @p.take(3, 2.1) 360 | end 361 | end 362 | 363 | describe '#take_values' do 364 | before do 365 | @p = Xi::Pattern.new([1, 2], delta: 2) 366 | end 367 | 368 | it 'returns the first +n+ events, starting from +cycle+' do 369 | assert_equal [1, 2, 1, 2], @p.take_values(4) 370 | assert_equal [2, 1, 2, 1], @p.take_values(4, 2.5) 371 | end 372 | end 373 | 374 | describe '#first' do 375 | before do 376 | @p = Xi::Pattern.new([1, 2], delta: 2) 377 | end 378 | 379 | it 'returns first event if +n+ is nil' do 380 | assert_equal [1, 0, 2, 0], @p.first 381 | end 382 | 383 | it 'returns first +n+ events, like #take' do 384 | assert_equal @p.take(6), @p.first(6) 385 | end 386 | end 387 | 388 | describe '#iteration_size' do 389 | describe 'when pattern is infinite' do 390 | before do 391 | @p = Xi::Pattern.new(1..inf) 392 | end 393 | 394 | it 'returns the size of delta' do 395 | assert_equal 1, @p.iteration_size 396 | assert_equal 3, @p.p(1, 2, 3).iteration_size 397 | assert_equal 4, @p.p([1, 2].p + [3, 4].p).iteration_size 398 | end 399 | end 400 | 401 | describe 'when pattern is finite' do 402 | before do 403 | @p = Xi::Pattern.new([1, 2]) 404 | end 405 | 406 | it 'returns the LCM between pattern size and delta size' do 407 | assert_equal 2, @p.iteration_size 408 | assert_equal 6, @p.p(1, 2, 3).iteration_size 409 | assert_equal 10, @p.p([1, 2].p + [3, 4, 5].p).iteration_size 410 | end 411 | end 412 | end 413 | 414 | describe '#duration' do 415 | before do 416 | @p = Xi::Pattern.new([1, 2]) 417 | end 418 | 419 | it 'returns the sum of delta values of each pattern value in a single iteration' do 420 | assert_equal 2, @p.duration 421 | assert_equal 12, @p.p(1, 2, 3).duration 422 | assert_equal 30, @p.p([1, 2].p + [3, 4, 5].p).duration 423 | end 424 | end 425 | end 426 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'xi' 3 | 4 | require 'minitest/autorun' 5 | 6 | def assert_pattern(expected_source, pattern) 7 | assert_kind_of Xi::Pattern, pattern 8 | 9 | source = expected_source.to_a 10 | assert_equal source, pattern.take_values(source.size) 11 | end 12 | -------------------------------------------------------------------------------- /test/xi_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | describe Xi do 4 | it 'has a version number' do 5 | refute_nil ::Xi::VERSION 6 | end 7 | 8 | it 'does nothing useful' do 9 | assert true 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /xi.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'xi/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "xi-lang" 8 | spec.version = Xi::VERSION 9 | spec.authors = ["Damián Silvani"] 10 | spec.email = ["munshkr@gmail.com"] 11 | 12 | spec.summary = %q{Musical pattern language for livecoding} 13 | spec.description = %q{A musical pattern language inspired in Tidal and SuperCollider 14 | for building higher-level musical constructs easily.} 15 | spec.homepage = "https://github.com/xi-livecode/xi" 16 | spec.license = "GPL-3.0-or-later" 17 | 18 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 19 | f.match(%r{^(test|spec|features)/}) 20 | end 21 | spec.bindir = "bin" 22 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 23 | spec.require_paths = ["lib"] 24 | 25 | spec.add_development_dependency "bundler" 26 | spec.add_development_dependency "rake", "~> 10.0" 27 | spec.add_development_dependency "minitest", "~> 5.0" 28 | spec.add_development_dependency "pry-byebug" 29 | spec.add_development_dependency "guard" 30 | spec.add_development_dependency "guard-minitest" 31 | spec.add_development_dependency "yard" 32 | 33 | spec.add_dependency 'pry' 34 | spec.add_dependency 'osc-ruby' 35 | end 36 | --------------------------------------------------------------------------------