tonal-tools 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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