├── .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 [](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 |
--------------------------------------------------------------------------------