tonal-tools 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/tonal/approximations.rb +148 -0
- data/lib/tonal/cents.rb +145 -0
- data/lib/tonal/extensions.rb +464 -0
- data/lib/tonal/hertz.rb +61 -0
- data/lib/tonal/interval.rb +38 -0
- data/lib/tonal/log.rb +121 -0
- data/lib/tonal/log2.rb +6 -0
- data/lib/tonal/ratio.rb +600 -0
- data/lib/tonal/reduced_ratio.rb +37 -0
- data/lib/tonal/step.rb +67 -0
- data/lib/tonal/tools.rb +17 -0
- metadata +167 -0
data/lib/tonal/ratio.rb
ADDED
@@ -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
|
data/lib/tonal/tools.rb
ADDED
@@ -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
|