tonal-tools 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,600 @@
1
+ class Tonal::Ratio
2
+ extend Forwardable
3
+ include Comparable
4
+
5
+ def_delegators :@approximations, :neighborhood
6
+
7
+ attr_reader :antecedent, :consequent, :equave, :reduced_antecedent, :reduced_consequent
8
+
9
+ # @return [Tonal::Ratio]
10
+ # @example
11
+ # Tonal::Ratio.new(3,2) => (3/2)
12
+ # @param antecedent [Numeric, Tonal::Ratio]
13
+ # @param consequent [Numeric, Tonal::Ratio]
14
+ #
15
+ def initialize(antecedent, consequent=nil, label: nil, equave: 2/1r)
16
+ raise ArgumentError, "Antecedent must be Numeric" unless antecedent.kind_of?(Numeric)
17
+ raise ArgumentError, "Consequent must be Numeric or nil" unless (consequent.kind_of?(Numeric) || consequent.nil?)
18
+
19
+ if consequent
20
+ @antecedent = antecedent.abs
21
+ @consequent = consequent.abs
22
+ else
23
+ antecedent = antecedent.abs
24
+ @antecedent = antecedent.numerator
25
+ @consequent = antecedent.denominator
26
+ end
27
+ @equave = equave
28
+ @reduced_antecedent, @reduced_consequent = _equave_reduce(equave)
29
+ @label = label
30
+ @approximations = Approximations.new(ratio: self)
31
+ end
32
+
33
+ alias :numerator :antecedent
34
+ alias :denominator :consequent
35
+
36
+ # @return [Tonal::Ratio] ratio who's numerator and denominator are 1 apart
37
+ # @example
38
+ # Tonal::Ratio.superparticular(100) = (100/99)
39
+ # @param n numerator of ratio
40
+ #
41
+ def self.superparticular(n)
42
+ superpartient(n, 1)
43
+ end
44
+
45
+ # @return [Tonal::Ratio] ratio who's numerator and denominator are a partient apart
46
+ # @example
47
+ # Tonal::Ratio.superpartient(100, 5) => (100/95)
48
+ # @param n numerator of ratio
49
+ # @param part partient separating the numerator and denominator
50
+ #
51
+ def self.superpartient(n, part)
52
+ self.new(n, n-part)
53
+ end
54
+
55
+ # @return [Tonal::Ratio] a randomly generated ratio
56
+ # @example
57
+ # Tonal::Ratio.random_ratio => (169/1)
58
+ # @param number_of_factors
59
+ # @param within
60
+ #
61
+ def self.random_ratio(number_of_factors = 2, within: 100)
62
+ primes = Prime.each(within).to_a
63
+ nums = []
64
+ dens = []
65
+ 1.upto(number_of_factors) do
66
+ nums << [primes[rand(10)], rand(3)]
67
+ dens << [primes[rand(10)], rand(3)]
68
+ end
69
+ [nums, dens].ratio_from_prime_divisions
70
+ end
71
+
72
+ # @return [Tonal::Ratio] the ratio of step in the modulo
73
+ # @example
74
+ # Tonal::Ratio.ed(12, 7)
75
+ # => (4771397596969315/4503599627370496)
76
+ # @param modulo
77
+ # @param step
78
+ # @param equave
79
+ #
80
+ def self.ed(modulo, step, equave: 2/1r)
81
+ self.new(2**(step.to_f/modulo), equave: equave)
82
+ end
83
+
84
+ # @return [Boolean] if pair of ratios are within the given cents limit
85
+ # @example
86
+ # Tonal::Ratio.within_cents?(100, 105, 2) => true
87
+ # @param cents1
88
+ # @param cents2
89
+ # @param within
90
+ #
91
+ def self.within_cents?(cents1, cents2, within)
92
+ (cents1 - cents2).abs <= within
93
+ end
94
+
95
+ # @return [Tonal::Ratio] convenience method returning self
96
+ #
97
+ def ratio
98
+ self
99
+ end
100
+
101
+ # @return [Tonal::Ratio::Approximations] self's approximation instance
102
+ #
103
+ def approx
104
+ @approximations
105
+ end
106
+
107
+ # ==================================
108
+ # Conversions
109
+ # ==================================
110
+
111
+ # @return [Array] antecedent and consequent as elements of Array
112
+ # @example
113
+ # Tonal::Ratio.new(3,2).to_a => [3, 2]
114
+ #
115
+ def to_a
116
+ [antecedent, consequent]
117
+ end
118
+
119
+ # @return [Vector] antecedent and consequent as elements of Vector
120
+ # @example
121
+ # Tonal::Ratio.new(3,2).to_v => Vector[3, 2]
122
+ #
123
+ def to_v
124
+ Vector[antecedent, consequent]
125
+ end
126
+
127
+ # @return [Rational] self as a Rational
128
+ # @example
129
+ # Tonal::Ratio.new(3,2).to_r => (3/2)
130
+ #
131
+ def to_r
132
+ return Float::INFINITY if consequent.zero? || !antecedent.finite?
133
+ Rational(antecedent, consequent)
134
+ end
135
+
136
+ # @return [Float] self as a Float
137
+ # @example
138
+ # Tonal::Ratio.new(3,2).to_f => 1.5
139
+ #
140
+ def to_f
141
+ antecedent.to_f / consequent.to_f
142
+ end
143
+
144
+ # @return [Tonal::Log] Math.log of self in given base
145
+ # @example
146
+ # Tonal::Ratio.new(3,2).log(3) => 0.3690702464285425
147
+ # @param base
148
+ #
149
+ def to_log(base=2)
150
+ Tonal::Log.new(logarithmand: self, base: base)
151
+ end
152
+ alias :log :to_log
153
+
154
+ # @return [Tonal::Log2] Math.log2 of self
155
+ # @example
156
+ # Tonal::ReducedRatio.new(3,2).to_log2 => 0.5849625007211562
157
+ #
158
+ def to_log2
159
+ Tonal::Log2.new(logarithmand: self)
160
+ end
161
+ alias :log2 :to_log2
162
+
163
+ # @return [Tonal::Cents] cents value of self
164
+ # @example
165
+ # Tonal::Ratio.new(3,2).to_cents => 701.96
166
+ #
167
+ def to_cents
168
+ Tonal::Cents.new(ratio: self)
169
+ end
170
+ alias :cents :to_cents
171
+
172
+ # @return [Integer] the step of self in the given modulo
173
+ # @example
174
+ # Tonal::ReducedRatio.new(3,2).step(12) => 7
175
+ #
176
+ def step(modulo=12)
177
+ to_log2.step(modulo)
178
+ end
179
+
180
+ # @return [Float] degrees
181
+ # @example
182
+ # Tonal::Ratio.new(3,2).period_degrees => 210.59
183
+ # @param round
184
+ #
185
+ def period_degrees(round: 2)
186
+ (360.0 * Math.log(to_f, equave)).round(round)
187
+ end
188
+
189
+ # @return [Float] radians
190
+ # @example
191
+ # Tonal::Ratio.new(3,2).period_radians => 3.68
192
+ # @param round
193
+ #
194
+ def period_radians(round: 2)
195
+ (2 * Math::PI * Math.log(to_f, equave)).round(round)
196
+ end
197
+
198
+ # @return [Tonal::Ratio] copy of self rationally reduced
199
+ # @example
200
+ # Tonal::Ratio.new(16,14).fraction_reduce => (8/7)
201
+ # @see to_r
202
+ #
203
+ def fraction_reduce
204
+ self.class.new(to_r)
205
+ end
206
+
207
+ # @return [Tonal::Ratio] copy of self reduced to the given equave
208
+ # @example
209
+ # Tonal::Ratio.new(48,14).equave_reduce(3) => (8/7)
210
+ # @param equave Numeric
211
+ #
212
+ def equave_reduce(equave=2/1r)
213
+ self.class.new(*_equave_reduce(equave))
214
+ end
215
+ alias :reduce :equave_reduce
216
+ alias :reduced :equave_reduce
217
+
218
+ # @return [Tonal::Ratio] self reduced to the given equave
219
+ # @example
220
+ # Tonal::Ratio.new(48,14).equave_reduce!(3) => (8/7)
221
+ # @param equave Numeric
222
+ #
223
+ def equave_reduce!(equave=2/1r)
224
+ @antecedent, @consequent = _equave_reduce(equave)
225
+ self
226
+ end
227
+ alias :reduce! :equave_reduce!
228
+ alias :reduced! :equave_reduce!
229
+
230
+ # @return [Tonal::ReducedRatio] of self
231
+ # @example
232
+ # Tonal::Ratio.new(1,9).to_reduced_ratio => (16/9)
233
+ #
234
+ def to_reduced_ratio
235
+ Tonal::ReducedRatio.new(reduced_antecedent, reduced_consequent, equave: equave)
236
+ end
237
+ alias :reduced_ratio :to_reduced_ratio
238
+
239
+ # @return [Tonal::Ratio] copy of self with the antecedent and precedent switched
240
+ # @example
241
+ # Tonal::Ratio.new(3,2).invert => (2/3)
242
+ #
243
+ def invert
244
+ self.class.new(consequent, antecedent)
245
+ end
246
+ alias :reflect :invert
247
+
248
+ # @return [self] with antecedent and precedent switched
249
+ # @example
250
+ # Tonal::Ratio.new(3,2).invert! => (2/3)
251
+ #
252
+ def invert!
253
+ tmp = antecedent
254
+ @antecedent, @consequent = consequent, tmp
255
+ self
256
+ end
257
+
258
+ # @return [Tonal::ReducedRatio] the mirror of self along the axis (default 1/1)
259
+ # @example
260
+ # Tonal::ReducedRatio.new(4,3).mirror => (3/2)
261
+ # @param axis
262
+ #
263
+ def mirror(axis=1/1r)
264
+ (self.class.new(axis) ** 2) / self
265
+ end
266
+
267
+ # TODO: Justify or remove
268
+ #
269
+ def mirror2(ratio)
270
+ self.class.new(invert.to_r / ratio)
271
+ end
272
+
273
+ # @return Tonal::ReducedRatio the Ernst Levy negative of self
274
+ # @example
275
+ # Tonal::ReducedRatio.new(7/4r).negative => (12/7)
276
+ #
277
+ def negative
278
+ self.class.new(3/2r) / self
279
+ end
280
+
281
+ # ==================================
282
+ # Ratio grid transformations
283
+ # numerator mapped on x-axis,
284
+ # denominator mapped on y-axis
285
+ # ==================================
286
+ #
287
+ # @return [Tonal::Ratio] with the antecedent and consequent translated by x and y
288
+ # @example
289
+ # Tonal::Ratio.new(3,2).translate(3,3) => (6/5)
290
+ # @param x [Numeric]
291
+ # @param y [Numeric]
292
+ #
293
+ def translate(x=1, y=0)
294
+ raise_if_negative(x,y)
295
+ self.class.new(*(Vector[antecedent, consequent] + Vector[x, y]))
296
+ end
297
+
298
+ # @return [Tonal::Ratio] self scaled by given arguments
299
+ # @example
300
+ # Tonal::Ratio.new(3,2).scale(2**5) => (96/64)
301
+ # @param a [Numeric]
302
+ # @param b [Numeric]
303
+ #
304
+ def scale(a, b=a)
305
+ raise_if_negative(a,b)
306
+ self.class.new(*(Matrix[[a, 0],[0, b]] * Vector[antecedent, consequent]))
307
+ end
308
+
309
+ # @return [Tonal::Ratio] self sheared by given arguments
310
+ # @example
311
+ # Tonal::Ratio.new(3,2).shear(1, 3) => (9/11)
312
+ # @param a [Numeric]
313
+ # @param b [Numeric]
314
+ #
315
+ def shear(a, b=a)
316
+ raise_if_negative(a,b)
317
+ self.class.new(*((Matrix[[1,a],[0,1]] * Matrix[[1,0], [b,1]]) * Vector[antecedent, consequent]))
318
+ end
319
+
320
+ # @return [Float] degrees of antecedent (x) and consequent (y) on a 2D plane
321
+ # @example
322
+ # Tonal::Ratio.new(3,2).planar_degrees => 33.69
323
+ # @param round
324
+ #
325
+ def planar_degrees(round: 2)
326
+ (Math.atan2(consequent, antecedent) * 180/Math::PI).round(round)
327
+ end
328
+
329
+ # @return [Float] radians
330
+ # @example
331
+ # Tonal::Ratio.new(3,2).planar_radians => 0.59
332
+ # @param round
333
+ #
334
+ def planar_radians(round: 2)
335
+ Math.atan2(consequent, antecedent).round(round)
336
+ end
337
+
338
+ # @return [Array], self decomposed into its prime factors
339
+ # @example
340
+ # Tonal::Ratio.new(31/30r).prime_divisions => [[[31, 1]], [[2, 1], [3, 1], [5, 1]]]
341
+ #
342
+ def prime_divisions
343
+ return [[[2, 1]], [[2, 1]]] if antecedent == 1
344
+ [antecedent.prime_division, consequent.prime_division]
345
+ end
346
+
347
+ # @return [Vector], self represented as a prime vector
348
+ # @example
349
+ # Tonal::Ratio.new(3/2r).prime_vector => Vector[-1, 1]
350
+ #
351
+ def prime_vector
352
+ pds = prime_divisions
353
+ max = [pds.first.max{|p| p.first}, pds.last.max{|p| p.first}].max.first
354
+
355
+ pds.last.collect!{|i| [i.first, -i.last]}
356
+
357
+ p_arr = Prime.each(max).to_a
358
+ Array.new(p_arr.count, 0).tap do |arr|
359
+ pds.flatten(1).each{|e| arr[p_arr.find_index(e.first)] = e.last}
360
+ end.to_vector
361
+ end
362
+ alias :monzo :prime_vector
363
+
364
+ # @return [Integer] the maximum prime factor of self
365
+ # @example
366
+ # Tonal::Ratio.new(31/30r).max_prime => 31
367
+ #
368
+ def max_prime
369
+ prime_divisions.flatten(1).map(&:first).max
370
+ end
371
+
372
+ # @return [Integer] the minimum prime factor of self
373
+ # @example
374
+ # Tonal::Ratio.new(31/30r).min_prime => 2
375
+ #
376
+ def min_prime
377
+ prime_divisions.flatten(1).map(&:first).min
378
+ end
379
+
380
+ def within_prime?(prime)
381
+ max_prime <= prime
382
+ end
383
+
384
+ # @return [Integer] the product complexity of self
385
+ # @example
386
+ # Tonal::ReducedRatio.new(3/2r).benedetti_height => 6
387
+ #
388
+ def benedetti_height
389
+ reduced_antecedent * reduced_consequent
390
+ end
391
+ alias :product_complexity :benedetti_height
392
+
393
+ # @return [Tonal::Log2] the log product complexity of self
394
+ # @example
395
+ # Tonal::ReducedRatio.new(3/2r).tenney_height => 2.584962500721156
396
+ #
397
+ def tenney_height
398
+ Tonal::Log2.new(logarithmand: benedetti_height)
399
+ end
400
+ alias :log_product_complexity :tenney_height
401
+ alias :harmonic_distance :tenney_height
402
+
403
+ # @return [Integer] the Weil height
404
+ # @example
405
+ # Tonal::ReducedRatio.new(3/2r).weil_height => 3
406
+ #
407
+ def weil_height
408
+ [reduced_antecedent, reduced_consequent].max
409
+ end
410
+
411
+
412
+ # @return [Tonal::Log2] the log of Weil height
413
+ # @example
414
+ # Tonal::ReducedRatio.new(3/2r).log_weil_height => 1.5849625007211563
415
+ #
416
+ def log_weil_height
417
+ Tonal::Log2.new(logarithmand: weil_height)
418
+ end
419
+
420
+ # @return [Integer] the Wilson height. The sum of self's prime factors (greater than 2) times the absolute values of their exponents
421
+ # @example
422
+ # Tonal::ReducedRatio.new(14/9r).wilson_height => 13
423
+ #
424
+ def wilson_height(prime_rejects: [2])
425
+ benedetti_height.prime_division.reject{|p| prime_rejects.include?(p.first) }.sum{|p| p.first * p.last }
426
+ end
427
+
428
+ # @return [Float] the cents difference between self and its step in the given modulo
429
+ # @example
430
+ # Tonal::ReducedRatio.new(3,2).efficiency(12) => -1.955000865387433
431
+ # @param modulo
432
+ #
433
+ def efficiency(modulo)
434
+ (Tonal::Cents::CENT_SCALE * step(modulo).step / modulo.to_f) - to_cents
435
+ end
436
+
437
+ # @return [Array] the results of ratio dividing and multiplying self
438
+ # @example
439
+ # Tonal::ReducedRatio.new(3/2r).div_times(5/4r) => [(6/5), (15/8)]
440
+ # @param ratio
441
+ #
442
+ def div_times(ratio)
443
+ ratio = ratio.ratio
444
+ [self / ratio, self * ratio]
445
+ end
446
+
447
+ # @return [Array] the results of ratio subtracted and added to self
448
+ # @example
449
+ # Tonal::ReducedRatio.new(3/2r).plus_minus(5/4r) => [(1/1), (11/8)]
450
+ # @param ratio
451
+ #
452
+ def plus_minus(ratio)
453
+ ratio = ratio.ratio
454
+ [self - ratio, self + ratio]
455
+ end
456
+
457
+ # @return [Cents] cent difference between self and other ratio
458
+ # @example
459
+ # Tonal::ReducedRatio.new(3,2).cent_diff(4/3r) => 203.92
460
+ # @param other_ratio [Tonal::ReducedRatio, Numeric] from which self's cents is measured
461
+ #
462
+ def cent_diff(other_ratio)
463
+ cents - other_ratio.ratio.cents
464
+ end
465
+
466
+ # @return [String] symbolic representation of Tonal::Ratio
467
+ #
468
+ def label
469
+ # Return label, if defined; or,
470
+ # Return the "antecedent/consequent", if antecedent is less than 7 digits long; or
471
+ # Return the floating point representation rounded to 2 digits of precision
472
+ @label || ((Math.log10(antecedent).to_i + 1) <= 6 ? "#{antecedent}/#{consequent}" : to_f.round(2))
473
+ end
474
+
475
+ # @return [String] the string representation of Tonal::Ratio
476
+ # @example
477
+ # Tonal::Ratio.new(3, 2).inspect => "[3, 2]"
478
+ #
479
+ def inspect
480
+ "(#{antecedent}/#{consequent})"
481
+ end
482
+
483
+ def +(rhs)
484
+ operate(rhs, :+)
485
+ end
486
+
487
+ def -(rhs)
488
+ operate(rhs, :-)
489
+ end
490
+
491
+ def *(rhs)
492
+ self.class.new(antecedent * rhs.antecedent, consequent * rhs.consequent)
493
+ end
494
+
495
+ def /(rhs)
496
+ self.class.new(antecedent * rhs.consequent, consequent * rhs.antecedent)
497
+ end
498
+
499
+ def **(rhs)
500
+ operate(rhs, :**)
501
+ end
502
+
503
+ # @return [Tonal::Ratio] the mediant (Farey) sum of self and another number
504
+ # @example
505
+ # Tonal::Ratio.new(3,2).mediant_sum(4/3r) => (7/5)
506
+ # @param number [Numeric, Tonal::Ratio]
507
+ #
508
+ def mediant_sum(number)
509
+ self.class.new(antecedent + number.numerator, consequent + number.denominator)
510
+ end
511
+ alias :mediant :mediant_sum
512
+ alias :farey_sum :mediant_sum
513
+
514
+ # ==================================
515
+ # Measurements
516
+ # ==================================
517
+
518
+ # @return [Integer] the least common multiple with self's denominator and the given number's denominator
519
+ # @example
520
+ # Tonal::Ratio.new(3/2r).lcm(5/4r) => 4
521
+ # @param lhs [Numeric, Tonal::Ratio] the number with which the lcm with self is computed
522
+ #
523
+ def lcm(lhs)
524
+ [self.denominator, lhs.denominator].lcm
525
+ end
526
+
527
+ # @return [Integer] the difference between antecedent and consequent
528
+ # @example
529
+ # Tonal::ReducedRatio.new(3,2).difference => 1
530
+ #
531
+ def difference
532
+ antecedent - consequent
533
+ end
534
+ alias :diff :difference
535
+
536
+ # @return [Integer] the sum of antecedent and consequent
537
+ # @example
538
+ # Tonal::ReducedRatio.new(3,2).combination => 5
539
+ #
540
+ def combination
541
+ antecedent + consequent
542
+ end
543
+ alias :comb :combination
544
+
545
+ def <=>(rhs)
546
+ left = consequent == 0 ? Float::INFINITY : Rational(antecedent, consequent)
547
+ right = rhs.denominator == 0 ? Float::INFINITY : Rational(rhs.numerator, rhs.denominator)
548
+ left <=> right
549
+ end
550
+
551
+ private
552
+ def raise_if_negative(*args)
553
+ raise ArgumentError, "Arguments must be greater than zero" if args.any?{|i| i < 0 }
554
+ end
555
+
556
+ def _equave_reduce(equave=2/1r)
557
+ case to_r
558
+ when Float::INFINITY
559
+ ante, cons = antecedent, 0
560
+ when 1
561
+ ante, cons = 1, 1
562
+ when 0
563
+ ante, cons = 0, consequent
564
+ else
565
+ power = (equave == Tonal::ReducedRatio::IDENTITY_RATIO) ? Tonal::ReducedRatio::IDENTITY_RATIO : Math.log(to_f.abs, equave)
566
+ r = Rational(self, equave**(power - 1).ceil)
567
+ r = 1/1r if r == Tonal::Interval::INTERVAL_OF_EQUIVALENCE
568
+ ante, cons = r.numerator, r.denominator
569
+ end
570
+ end
571
+
572
+ def operate(rhs, op)
573
+ case op
574
+ when :+, :-
575
+ case rhs
576
+ when Tonal::Ratio
577
+ self.class.new((antecedent * rhs.consequent).send(op, rhs.antecedent * consequent).abs, consequent * rhs.consequent)
578
+ when Rational
579
+ self.class.new((antecedent * rhs.denominator).send(op, rhs.numerator * consequent).abs, consequent * rhs.denominator)
580
+ when Array
581
+ self.class.new((antecedent * rhs[1]).send(op, rhs[0] * consequent), consequent * rhs[1])
582
+ else
583
+ r = Rational(rhs)
584
+ self.class.new((antecedent * r.denominator).send(op, r.numerator * consequent), consequent * r.denominator)
585
+ end
586
+ when :*
587
+ case rhs
588
+ when Rational
589
+ self.class.new(antecedent.send(op, rhs.numerator), consequent.send(op, rhs.denominator))
590
+ when Array
591
+ self.class.new(antecedent.send(op, rhs[0]), consequent.send(op, rhs[1]))
592
+ else
593
+ r = Rational(rhs)
594
+ self.class.new(antecedent.send(op, r.numerator), consequent.send(op, r.denominator))
595
+ end
596
+ when :**
597
+ self.class.new(antecedent.send(op, rhs), consequent.send(op, rhs))
598
+ end
599
+ end
600
+ end
@@ -0,0 +1,37 @@
1
+ class Tonal::ReducedRatio < Tonal::Ratio
2
+ IDENTITY_RATIO = 1/1r
3
+
4
+ # @return [Tonal::ReducedRatio]
5
+ # @example
6
+ # Tonal::ReducedRatio.new(12,2) => (3/2)
7
+ # @param antecedent [Numeric, Tonal::Ratio]
8
+ # @param consequent [Numeric, Tonal::Ratio]
9
+ # @param equave the interval of equivalence, default 2/1
10
+ #
11
+ def initialize(antecedent, consequent=1, label: nil, equave: 2/1r)
12
+ super(antecedent, consequent, label: label, equave: equave)
13
+ @antecedent, @consequent = @reduced_antecedent, @reduced_consequent
14
+ end
15
+
16
+ def self.identity
17
+ self.new(IDENTITY_RATIO)
18
+ end
19
+
20
+ # @return [Tonal::Ratio] self as an instance of unreduced ratio
21
+ # @example
22
+ # Tonal::ReducedRatio.new(3,2).to_basic_ratio => (3/2)
23
+ #
24
+ def to_basic_ratio
25
+ Tonal::Ratio.new(antecedent, consequent)
26
+ end
27
+
28
+ # @return [Interval] between self (upper) and ratio (lower)
29
+ # @example
30
+ # Tonal::ReducedRatio.new(133).interval_with(3/2r) => 133/96 (133/128 / 3/2)
31
+ # @param ratio
32
+ #
33
+ def interval_with(ratio)
34
+ r = ratio.is_a?(self.class) ? ratio : self.class.new(ratio)
35
+ Tonal::Interval.new(self, r)
36
+ end
37
+ end
data/lib/tonal/step.rb ADDED
@@ -0,0 +1,67 @@
1
+ class Tonal::Step
2
+ extend Forwardable
3
+ include Comparable
4
+
5
+ def_delegators :@log, :logarithmand
6
+
7
+ attr_reader :modulo, :log, :step, :ratio, :tempered
8
+
9
+ def initialize(modulo: nil, log: nil, step: nil, ratio: nil)
10
+ raise ArgumentError, "modulo: required" unless modulo
11
+ raise ArgumentError, "One of log:, step: or ratio: must be provided" unless [log, step, ratio].compact.count == 1
12
+ @modulo = modulo.round
13
+
14
+ if ratio
15
+ @ratio, @log = derive_ratio_and_log(ratio: ratio)
16
+ elsif step
17
+ @ratio, @log = derive_ratio_and_log(step: step)
18
+ elsif log
19
+ @ratio, @log = derive_ratio_and_log(log: log)
20
+ end
21
+
22
+ @step = (modulo * @log).round
23
+ @tempered = 2**(@step.to_f/@modulo)
24
+ end
25
+
26
+ def inspect
27
+ "#{step}\\#{modulo}"
28
+ end
29
+ alias :to_s :inspect
30
+
31
+ def convert(new_modulo)
32
+ self.class.new(log: log, modulo: new_modulo)
33
+ end
34
+
35
+ def to_r
36
+ ratio.to_r
37
+ end
38
+
39
+ def cents
40
+ ratio.to_cents
41
+ end
42
+
43
+ def +(rhs)
44
+ self.class.new(step: (rhs % modulo), modulo: modulo)
45
+ end
46
+ alias :% :+
47
+
48
+ def <=>(rhs)
49
+ rhs.kind_of?(self.class) && modulo <=> rhs.modulo && log <=> rhs.log && step <=> rhs.step
50
+ end
51
+
52
+ private
53
+ def derive_ratio_and_log(ratio: nil, log: nil, step: nil)
54
+ if ratio
55
+ [Tonal::ReducedRatio.new(ratio), Tonal::Log2.new(logarithmand: ratio)]
56
+ elsif log
57
+ if log.kind_of?(Tonal::Log2)
58
+ [log.logarithmand, log]
59
+ else
60
+ lg = Tonal::Log2.new(logarithm: log)
61
+ [Tonal::ReducedRatio.new(lg.logarithmand), lg]
62
+ end
63
+ elsif step
64
+ [Tonal::ReducedRatio.new(2.0**(step.to_f/@modulo)), Tonal::Log2.new(logarithmand: (2.0 ** (step.to_f/@modulo)))]
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,17 @@
1
+ module Tonal
2
+ require "prime"
3
+ require "matrix"
4
+ require "sorted_set"
5
+ require "continued_fractions"
6
+ require "fraction_tree"
7
+ require "tonal/cents"
8
+ require "tonal/hertz"
9
+ require "tonal/log"
10
+ require "tonal/log2"
11
+ require "tonal/approximations"
12
+ require "tonal/ratio"
13
+ require "tonal/reduced_ratio"
14
+ require "tonal/interval"
15
+ require "tonal/step"
16
+ require "tonal/extensions"
17
+ end