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.
- 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
@@ -0,0 +1,464 @@
|
|
1
|
+
class Prime
|
2
|
+
# @return [Array] the primes within lower and upper bounds
|
3
|
+
# @example
|
4
|
+
# Prime.within(6, 14) => [7, 11, 13]
|
5
|
+
# @param lower bound
|
6
|
+
# @param upper bound
|
7
|
+
#
|
8
|
+
def self.within(lower, upper)
|
9
|
+
self.each(upper).reject{|p| p < lower}
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Numeric
|
14
|
+
# @return [Array], a tuple of self offset positively/negatively
|
15
|
+
# @example
|
16
|
+
# Math::PI.plus_minus(3)
|
17
|
+
# => [6.141592653589793, 0.14159265358979312]
|
18
|
+
# @param offset plus and minus distance from self
|
19
|
+
#
|
20
|
+
def plus_minus(offset)
|
21
|
+
[self + offset, self - offset]
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [Tonal::ReducedRatio] the octave reduced ratio of self
|
25
|
+
# @example
|
26
|
+
# (2**(1.0/12)).ratio => (4771397596969315/4503599627370496)
|
27
|
+
#
|
28
|
+
def to_ratio(reduced: true, equave: 2/1r)
|
29
|
+
reduced ? Tonal::ReducedRatio.new(self, equave: equave) : Tonal::Ratio.new(self, equave: equave)
|
30
|
+
end
|
31
|
+
alias :ratio :to_ratio
|
32
|
+
|
33
|
+
# @return [Float], the degrees on a circle of self
|
34
|
+
# @example
|
35
|
+
# (2**(6.0/12)).period_degrees => 180.0
|
36
|
+
#
|
37
|
+
def period_degrees
|
38
|
+
self.ratio.period_degrees
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Tonal::Log] the log of self to the given base
|
42
|
+
# @example
|
43
|
+
# (3/2r).log(10) => 0.17609125905568124
|
44
|
+
#
|
45
|
+
def log(base)
|
46
|
+
Tonal::Log.new(logarithmand: self, base: base)
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [Tonal::Log2] the log2 of self
|
50
|
+
# @example
|
51
|
+
# (3/2r).log2 => 0.5849625007211562
|
52
|
+
#
|
53
|
+
def log2
|
54
|
+
Tonal::Log2.new(logarithmand: self)
|
55
|
+
end
|
56
|
+
alias :to_log2 :log2
|
57
|
+
alias :span :log2
|
58
|
+
|
59
|
+
# @return [Tonal::Cents] of self interpreted as a cents quantity
|
60
|
+
# @example
|
61
|
+
# 700.0.cents => 700.0
|
62
|
+
#
|
63
|
+
def cents
|
64
|
+
Tonal::Cents.new(cents: self)
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [Tonal::Cents] of self interpreted as a cents quantity
|
68
|
+
# @example
|
69
|
+
# 700.0.¢ => 700.0
|
70
|
+
#
|
71
|
+
def ¢
|
72
|
+
cents
|
73
|
+
end
|
74
|
+
|
75
|
+
# @return [Tonal::Cents] of self interpreted as a ratio
|
76
|
+
# @example
|
77
|
+
# (3/2r).cents => 701.96
|
78
|
+
#
|
79
|
+
def to_cents
|
80
|
+
self.log2.to_cents
|
81
|
+
end
|
82
|
+
|
83
|
+
# @return [Tonal::Hertz] of self
|
84
|
+
#
|
85
|
+
def hz
|
86
|
+
Tonal::Hertz.new(self)
|
87
|
+
end
|
88
|
+
alias :to_hz :hz
|
89
|
+
|
90
|
+
# @return [Step] the step of self in the given modulo
|
91
|
+
# @example
|
92
|
+
# (5/4r).step(12) => 4\12
|
93
|
+
# @param modulo
|
94
|
+
#
|
95
|
+
def step(modulo=12)
|
96
|
+
to_log2.step(modulo)
|
97
|
+
end
|
98
|
+
|
99
|
+
# @return [Float] the log product complexity of self
|
100
|
+
# @example
|
101
|
+
# (3/2r).tenney_height => 2.584962500721156
|
102
|
+
#
|
103
|
+
def benedetti_height
|
104
|
+
self.ratio.benedetti_height
|
105
|
+
end
|
106
|
+
alias :product_complexity :benedetti_height
|
107
|
+
|
108
|
+
# @return [Integer] the product complexity of self
|
109
|
+
# @example
|
110
|
+
# (3/2r).benedetti_height => 6
|
111
|
+
#
|
112
|
+
def tenney_height
|
113
|
+
self.ratio.tenney_height
|
114
|
+
end
|
115
|
+
alias :log_product_complexity :tenney_height
|
116
|
+
|
117
|
+
# @return [Integer] the Weil height
|
118
|
+
# @example
|
119
|
+
# (3/2r).weil_height => 3
|
120
|
+
#
|
121
|
+
def weil_height
|
122
|
+
self.ratio.weil_height
|
123
|
+
end
|
124
|
+
|
125
|
+
# @return [Tonal::Log2] the log of Weil height
|
126
|
+
# @example
|
127
|
+
# (3/2r).log_weil_height => 1.5849625007211563
|
128
|
+
#
|
129
|
+
def log_weil_height
|
130
|
+
self.ratio.log_weil_height
|
131
|
+
end
|
132
|
+
|
133
|
+
# @return [Integer] the Wilson height
|
134
|
+
# @example (14/9r).wilson_height => 13
|
135
|
+
#
|
136
|
+
def wilson_height(reduced: true, equave: 2/1r, prime_rejects: [2])
|
137
|
+
self.ratio(reduced: reduced, equave: equave).wilson_height(prime_rejects: prime_rejects)
|
138
|
+
end
|
139
|
+
|
140
|
+
# @return [Float] the cents difference between self and its step in the given modulo
|
141
|
+
# @example
|
142
|
+
# (3/2r).efficiency(12) => -1.955000865387433
|
143
|
+
# @param modulo
|
144
|
+
#
|
145
|
+
def efficiency(modulo)
|
146
|
+
(Tonal::Cents::CENT_SCALE * step(modulo).step / modulo.to_f) - to_cents
|
147
|
+
end
|
148
|
+
|
149
|
+
# @return [Interval] beween self (upper) and ratio (lower)
|
150
|
+
# @example
|
151
|
+
# (133).interval_with(3/2r) => 133/96 (133/128 / 3/2)
|
152
|
+
# @param ratio
|
153
|
+
#
|
154
|
+
def interval_with(ratio)
|
155
|
+
Tonal::Interval.new(self.ratio, ratio)
|
156
|
+
end
|
157
|
+
|
158
|
+
# @return [Vector], self represented as a prime vector
|
159
|
+
# @example
|
160
|
+
# (3/2r).prime_vector => Vector[-1, 1]
|
161
|
+
#
|
162
|
+
def prime_vector
|
163
|
+
self.ratio.prime_vector
|
164
|
+
end
|
165
|
+
alias :monzo :prime_vector
|
166
|
+
|
167
|
+
# @return [Array], self decomposed into its prime factors
|
168
|
+
# @example
|
169
|
+
# (31/30r).prime_divisions => [[[31, 1]], [[2, 1], [3, 1], [5, 1]]]
|
170
|
+
#
|
171
|
+
def prime_divisions
|
172
|
+
self.ratio.prime_divisions
|
173
|
+
end
|
174
|
+
|
175
|
+
# @return [Integer] the maximum prime factor of self
|
176
|
+
# @example
|
177
|
+
# (31/30r).max_prime => 31
|
178
|
+
#
|
179
|
+
def max_prime
|
180
|
+
prime_divisions.flatten(1).map(&:first).max
|
181
|
+
end
|
182
|
+
|
183
|
+
# @return [Integer] the minimum prime factor of self
|
184
|
+
# @example
|
185
|
+
# (31/30r).min_prime => 2
|
186
|
+
#
|
187
|
+
def min_prime
|
188
|
+
prime_divisions.flatten(1).map(&:first).min
|
189
|
+
end
|
190
|
+
|
191
|
+
# @return [Integer] the product complexity of self
|
192
|
+
# @example
|
193
|
+
# (3/2r).benedetti_height => 6
|
194
|
+
#
|
195
|
+
def benedetti_height
|
196
|
+
numerator * denominator
|
197
|
+
end
|
198
|
+
|
199
|
+
# @return [Float] the log product complexity of self
|
200
|
+
# @example
|
201
|
+
# (3/2r).tenney_height => 2.584962500721156
|
202
|
+
#
|
203
|
+
def tenney_height
|
204
|
+
Tonal::Log2.new(logarithmand: benedetti_height)
|
205
|
+
end
|
206
|
+
|
207
|
+
# @return [Tonal::ReducedRatio], the Ernst Levy negative of self
|
208
|
+
# @example
|
209
|
+
# (7/4r).negative => (12/7)
|
210
|
+
#
|
211
|
+
def negative
|
212
|
+
self.ratio.negative
|
213
|
+
end
|
214
|
+
|
215
|
+
# @return [Tonal::ReducedRatio], the ratio rotated on the given axis, default 1/1
|
216
|
+
# @example
|
217
|
+
# (3/2r).mirror => (4/3)
|
218
|
+
#
|
219
|
+
def mirror(axis=1/1r)
|
220
|
+
self.ratio.mirror(axis)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
class Rational
|
225
|
+
# @return [Vector], self expressed as a Vector
|
226
|
+
# @example
|
227
|
+
# (3/2r).to_vector => Vector[3, 2]
|
228
|
+
#
|
229
|
+
def to_vector
|
230
|
+
Vector[self.numerator, self.denominator]
|
231
|
+
end
|
232
|
+
alias :vector :to_vector
|
233
|
+
|
234
|
+
# @return [Array], self decomposed into its prime factors
|
235
|
+
# @example
|
236
|
+
# (31/30r).prime_divisions => [[[31, 1]], [[2, 1], [3, 1], [5, 1]]]
|
237
|
+
#
|
238
|
+
def prime_divisions
|
239
|
+
self.ratio.prime_divisions
|
240
|
+
end
|
241
|
+
|
242
|
+
# @return [Integer] the maximum prime factor of self
|
243
|
+
# @example
|
244
|
+
# (31/30r).max_prime => 31
|
245
|
+
#
|
246
|
+
def max_prime
|
247
|
+
self.ratio.max_prime
|
248
|
+
end
|
249
|
+
|
250
|
+
# @return [Integer] the minimum prime factor of self
|
251
|
+
# @example
|
252
|
+
# (31/30r).min_prime => 2
|
253
|
+
#
|
254
|
+
def min_prime
|
255
|
+
self.ratio.min_prime
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
class Integer
|
260
|
+
alias :prime_factors :prime_division
|
261
|
+
|
262
|
+
# @return [Integer] the maximum prime factor of self
|
263
|
+
# @example
|
264
|
+
# 72.max_prime => 3
|
265
|
+
#
|
266
|
+
def max_prime
|
267
|
+
self.prime_division.map(&:first).max
|
268
|
+
end
|
269
|
+
|
270
|
+
# @return [Integer] the minimum prime factor of self
|
271
|
+
# @example
|
272
|
+
# 72.min_prime => 2
|
273
|
+
#
|
274
|
+
def min_prime
|
275
|
+
self.prime_division.map(&:first).min
|
276
|
+
end
|
277
|
+
|
278
|
+
# @return [Integer] the factorial of self
|
279
|
+
# @example
|
280
|
+
# 5.factorial => 120
|
281
|
+
#
|
282
|
+
def factorial
|
283
|
+
(2..self).reduce(1, :*)
|
284
|
+
end
|
285
|
+
|
286
|
+
# @return [Boolean] if self is coprime with i
|
287
|
+
# @example
|
288
|
+
# 25.coprime?(7) => true
|
289
|
+
# 25.coprime?(5) => false
|
290
|
+
#
|
291
|
+
def coprime?(i)
|
292
|
+
self.gcd(i) == 1
|
293
|
+
end
|
294
|
+
|
295
|
+
# @return [Array] list of integers that are coprime with self, up to the value of self
|
296
|
+
# @example
|
297
|
+
# 10.coprimes => [1, 3, 7, 9]
|
298
|
+
#
|
299
|
+
def coprimes
|
300
|
+
[].tap do |coprime_set|
|
301
|
+
1.upto(self) do |i|
|
302
|
+
coprime_set << i if i.coprime?(self)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# @return [Integer] the count of coprimes less than self
|
308
|
+
# @example
|
309
|
+
# 10.phi => 4
|
310
|
+
#
|
311
|
+
def phi
|
312
|
+
coprimes.count
|
313
|
+
end
|
314
|
+
alias :totient :phi
|
315
|
+
|
316
|
+
# @return [Array] of integers that are n-smooth with self
|
317
|
+
# Adapted from https://rosettacode.org/wiki/N-smooth_numbers#Ruby
|
318
|
+
# @example
|
319
|
+
# 5.nsmooth(25)
|
320
|
+
# => [1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, 16, 18, 20, 24, 25, 27, 30, 32, 36, 40, 45, 48, 50, 54]
|
321
|
+
# @param limit
|
322
|
+
#
|
323
|
+
def nsmooth(limit=2)
|
324
|
+
([0] * limit).tap do |ns|
|
325
|
+
primes = Prime.each(self).to_a
|
326
|
+
ns[0] = 1
|
327
|
+
nextp = primes[0..primes.index(self)]
|
328
|
+
|
329
|
+
indices = [0] * nextp.size
|
330
|
+
(1...limit).each do |m|
|
331
|
+
ns[m] = nextp.min
|
332
|
+
(0...indices.size).each do |i|
|
333
|
+
if ns[m] == nextp[i]
|
334
|
+
indices[i] += 1
|
335
|
+
nextp[i] = primes[i] * ns[indices[i]]
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
class Array
|
344
|
+
# @return [Array] self replaced by array padded to the right up to n, with value. value default is nil
|
345
|
+
# @example
|
346
|
+
# [3,2].rpad!(3, 12) => [3, 2, 12]
|
347
|
+
# @param min_size
|
348
|
+
# @param value
|
349
|
+
#
|
350
|
+
def rpad!(min_size, value = nil)
|
351
|
+
self.length > min_size ? self : (min_size - self.length).times { self << value }
|
352
|
+
self
|
353
|
+
end
|
354
|
+
|
355
|
+
# @return [Array] padded to the right up to n, with value. value default is nil
|
356
|
+
# @example
|
357
|
+
# [3,2].rpad(3, 12) => [3, 2, 12]
|
358
|
+
# @param min_size
|
359
|
+
# @param value
|
360
|
+
#
|
361
|
+
def rpad(min_size, value = nil)
|
362
|
+
self.dup.rpad!(min_size, value)
|
363
|
+
end
|
364
|
+
|
365
|
+
# @return [Vector] self converted to a vector
|
366
|
+
# @example
|
367
|
+
# [3,2].to_vector => Vector[3, 2]
|
368
|
+
#
|
369
|
+
def to_vector
|
370
|
+
Vector[*self]
|
371
|
+
end
|
372
|
+
alias :vector :to_vector
|
373
|
+
|
374
|
+
# @return [Integer] least common multiple of elements of self
|
375
|
+
# @example TODO
|
376
|
+
#
|
377
|
+
def lcm
|
378
|
+
self.reduce(1, :lcm)
|
379
|
+
end
|
380
|
+
|
381
|
+
# @return [Array] of numerators of elements of self
|
382
|
+
# @example
|
383
|
+
# [3/2r, 5/4r].numerators => [3, 5]
|
384
|
+
#
|
385
|
+
def numerators
|
386
|
+
self.map(&:numerator)
|
387
|
+
end
|
388
|
+
alias :antecedents :numerators
|
389
|
+
|
390
|
+
# @return [Array] of denominators of elements of self
|
391
|
+
# @example
|
392
|
+
# [Tonal::Ratio.new(3,2), Tonal::Ratio.new(5,4)].denominators => [2, 4]
|
393
|
+
#
|
394
|
+
def denominators
|
395
|
+
self.map(&:denominator)
|
396
|
+
end
|
397
|
+
alias :consequents :denominators
|
398
|
+
|
399
|
+
# @return [Array] an array of normalized ratios
|
400
|
+
# @example
|
401
|
+
# [4/3r, 3/2r].normalize => [(8/6), (9/6)]
|
402
|
+
#
|
403
|
+
def normalize
|
404
|
+
l = denominators.lcm
|
405
|
+
map{|r| Tonal::Ratio.new(l / r.denominator * r.numerator, l)}
|
406
|
+
end
|
407
|
+
|
408
|
+
# @return [Array] of cent values for ratio or rational elements of self
|
409
|
+
# @example
|
410
|
+
# [3/2r, 4/3r].to_cents => [701.96, 498.04]
|
411
|
+
#
|
412
|
+
def to_cents
|
413
|
+
self.map{|r| r.to_cents}
|
414
|
+
end
|
415
|
+
alias :cents :to_cents
|
416
|
+
|
417
|
+
# TODO: Consider removing
|
418
|
+
#def cons_diff(cons=2)
|
419
|
+
# self.each_cons(cons).map{|a,b| (a - b).abs }
|
420
|
+
#end
|
421
|
+
|
422
|
+
# @return [Float] the mean of the elements of self
|
423
|
+
# @example
|
424
|
+
# [1, 2].mean => 1.5
|
425
|
+
#
|
426
|
+
def mean
|
427
|
+
self.sum / self.count.to_f
|
428
|
+
end
|
429
|
+
|
430
|
+
# @return [Tonal::ReducedRatio] ratio reconstructed from the result of a prime factor decomposition
|
431
|
+
# @example
|
432
|
+
# [[[3, 1]], [[2, 1]]].ratio_from_prime_divisions => (3/2)
|
433
|
+
#
|
434
|
+
def ratio_from_prime_divisions
|
435
|
+
Tonal::Ratio.new(Prime.int_from_prime_division(self.first), Prime.int_from_prime_division(self.last))
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
class Vector
|
440
|
+
# @return [Tonal::Ratio]
|
441
|
+
# @example
|
442
|
+
# Vector[3,2].ratio => (3/2)
|
443
|
+
# @param reduced
|
444
|
+
# @param equave
|
445
|
+
#
|
446
|
+
def to_ratio(reduced: true, equave: 2/1r)
|
447
|
+
reduced ? Tonal::ReducedRatio.new(*self, equave: equave) : Tonal::Ratio.new(*self, equave: equave)
|
448
|
+
end
|
449
|
+
alias :ratio :to_ratio
|
450
|
+
end
|
451
|
+
|
452
|
+
module Math
|
453
|
+
# @return [Integer] the factorial of n
|
454
|
+
# @example
|
455
|
+
# Math.factorial(10) => 3628800
|
456
|
+
# @param limit
|
457
|
+
#
|
458
|
+
def self.factorial(limit)
|
459
|
+
(2..limit).reduce(1, :*)
|
460
|
+
end
|
461
|
+
|
462
|
+
PHI = (1 + 5**(1.0/2))/2
|
463
|
+
end
|
464
|
+
|
data/lib/tonal/hertz.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
class Tonal::Hertz
|
2
|
+
include Comparable
|
3
|
+
|
4
|
+
attr_reader :value
|
5
|
+
|
6
|
+
# @return [Hertz]
|
7
|
+
# @example
|
8
|
+
# Hertz.new(1000.0) => 1000.0
|
9
|
+
# @param arg [Numeric, Tonal::Hertz]
|
10
|
+
#
|
11
|
+
def initialize(arg)
|
12
|
+
raise ArgumentError, "Argument is not Numeric or Tonal::Hertz" unless arg.kind_of?(Numeric) || arg.kind_of?(self.class)
|
13
|
+
@value = arg.kind_of?(self.class) ? arg.inspect : arg
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [Hertz] 440 Hz
|
17
|
+
# @example
|
18
|
+
# Tonal::Hertz.a440 => 440.0
|
19
|
+
#
|
20
|
+
def self.a440
|
21
|
+
self.new(440.0)
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [Rational] self as a rational
|
25
|
+
# @example
|
26
|
+
# Tonal::Hertz.new(440).to_r => (440/1)
|
27
|
+
#
|
28
|
+
def to_r
|
29
|
+
Rational(value)
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
# @return [Rational] self as a float
|
34
|
+
# @example
|
35
|
+
# Tonal::Hertz.new(440).to_f => 440.0
|
36
|
+
#
|
37
|
+
def to_f
|
38
|
+
value.to_f
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [String] the string representation of Hertz
|
42
|
+
# @example
|
43
|
+
# Hertz(1000.0).inspect => "1000.0"
|
44
|
+
#
|
45
|
+
def inspect
|
46
|
+
"#{value}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def <=>(rhs)
|
50
|
+
rhs.kind_of?(self.class) ? value <=> rhs.value : value <=> rhs
|
51
|
+
end
|
52
|
+
|
53
|
+
def method_missing(op, *args, &blk)
|
54
|
+
rhs = args.collect do |arg|
|
55
|
+
arg.kind_of?(self.class) ? arg.value : arg
|
56
|
+
end
|
57
|
+
result = value.send(op, *rhs)
|
58
|
+
return result if op == :coerce
|
59
|
+
result.kind_of?(Numeric) ? self.class.new(result) : result
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class Tonal::Interval
|
2
|
+
extend Forwardable
|
3
|
+
include Comparable
|
4
|
+
|
5
|
+
def_delegators :@interval, :to_r, :antecedent, :consequent
|
6
|
+
|
7
|
+
attr_reader :lower_ratio, :upper_ratio, :interval
|
8
|
+
|
9
|
+
INTERVAL_OF_EQUIVALENCE = 2/1r
|
10
|
+
|
11
|
+
def initialize(upper_ratio, lower_ratio)
|
12
|
+
@lower_ratio = lower_ratio.ratio
|
13
|
+
@upper_ratio = upper_ratio.ratio
|
14
|
+
@interval = @upper_ratio / @lower_ratio
|
15
|
+
end
|
16
|
+
alias :lower :lower_ratio
|
17
|
+
alias :upper :upper_ratio
|
18
|
+
alias :numerator :antecedent
|
19
|
+
alias :denominator :consequent
|
20
|
+
|
21
|
+
def to_a
|
22
|
+
[lower_ratio, upper_ratio]
|
23
|
+
end
|
24
|
+
|
25
|
+
def normalize
|
26
|
+
ratios = to_a
|
27
|
+
lcm = ratios.denominators.lcm
|
28
|
+
ratios.map{|r| Tonal::Ratio.new(lcm / r.denominator * r.numerator, lcm)}
|
29
|
+
end
|
30
|
+
|
31
|
+
def inspect
|
32
|
+
"#{self.to_r} (#{upper.to_r} / #{lower.to_r})"
|
33
|
+
end
|
34
|
+
|
35
|
+
def <=>(rhs)
|
36
|
+
interval.to_r <=> rhs.interval.to_r
|
37
|
+
end
|
38
|
+
end
|
data/lib/tonal/log.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
class Tonal::Log
|
2
|
+
extend Forwardable
|
3
|
+
include Comparable
|
4
|
+
|
5
|
+
def_delegators :@logarithmand, :ratio, :to_ratio
|
6
|
+
|
7
|
+
attr_reader :logarithmand, :logarithm, :base
|
8
|
+
|
9
|
+
# @return [Tonal::Log]
|
10
|
+
# @example
|
11
|
+
# Tonal::Log.new(logarithmand: 3/2r, base: 2) => 0.5849625007211562
|
12
|
+
# @param logarithmand
|
13
|
+
# @param logarithm
|
14
|
+
# @param base
|
15
|
+
#
|
16
|
+
def initialize(logarithmand: nil, logarithm: nil, base: nil)
|
17
|
+
raise ArgumentError, "logarithmand or logarithm must be provided" if logarithmand.nil? && logarithm.nil?
|
18
|
+
|
19
|
+
if logarithmand && logarithm && base
|
20
|
+
@logarithmand = logarithmand
|
21
|
+
@logarithm = logarithm
|
22
|
+
@base = derive_base(logarithmand: logarithmand, logarithm: logarithm)
|
23
|
+
puts "Provided base (#{base}) does not align with logarithmand and logarithm. Using calculated base (#{@base}) instead" if @base != base
|
24
|
+
elsif logarithmand && logarithm
|
25
|
+
@logarithmand = logarithmand
|
26
|
+
@logarithm = logarithm
|
27
|
+
@base = derive_base(logarithmand: logarithmand, logarithm: logarithm)
|
28
|
+
elsif logarithmand && base
|
29
|
+
@logarithmand = logarithmand
|
30
|
+
@base = base
|
31
|
+
@logarithm = derive_logarithm(logarithmand: logarithmand, base: @base)
|
32
|
+
elsif logarithm && base
|
33
|
+
@base = base
|
34
|
+
@logarithm = logarithm
|
35
|
+
@logarithmand = derive_logarithmand(logarithm: logarithm, base: @base)
|
36
|
+
elsif logarithmand
|
37
|
+
@logarithmand = logarithmand
|
38
|
+
@base = self.class.base
|
39
|
+
@logarithm = derive_logarithm(logarithmand: logarithmand, base: @base)
|
40
|
+
elsif logarithm
|
41
|
+
@base = self.class.base
|
42
|
+
@logarithm = logarithm
|
43
|
+
@logarithmand = derive_logarithmand(logarithm: logarithm, base: @base)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.base
|
48
|
+
Math::E
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [Tonal::Cents] the cents scale logarithm
|
52
|
+
# @example
|
53
|
+
# Tonal::Log.new(logarithmand: 3/2r, base: 2).to_cents => 701.9550008653874
|
54
|
+
# @see Cents
|
55
|
+
#
|
56
|
+
def to_cents(precision: Tonal::Cents::PRECISION)
|
57
|
+
Tonal::Cents.new(log: self, precision: precision)
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Step] the nearest step in the given modulo
|
61
|
+
# @example
|
62
|
+
# Tonal::Log.new(3/2r, base: 2).step(12) => 7\12
|
63
|
+
#
|
64
|
+
def step(modulo)
|
65
|
+
Tonal::Step.new(modulo: modulo, log: self)
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return [String] the string representation of Tonal::Log
|
69
|
+
# @example
|
70
|
+
# Tonal::Log.new(3/2r, base: 2).inspect => "0.5849625007211562"
|
71
|
+
#
|
72
|
+
def inspect
|
73
|
+
"#{logarithm}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def <=>(rhs)
|
77
|
+
rhs.kind_of?(self.class) ? logarithm <=> rhs.logarithm : logarithm <=> rhs
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
def derive_base(logarithmand:, logarithm:)
|
82
|
+
logarithmand**(1.0/logarithm)
|
83
|
+
end
|
84
|
+
|
85
|
+
def derive_logarithmand(logarithm:, base:)
|
86
|
+
base**logarithm
|
87
|
+
end
|
88
|
+
|
89
|
+
def derive_logarithm(logarithmand:, base:)
|
90
|
+
case logarithmand
|
91
|
+
when Tonal::Cents
|
92
|
+
logarithmand.value / Tonal::Cents::CENT_SCALE
|
93
|
+
when Tonal::Ratio
|
94
|
+
Math.log(logarithmand.antecedent.to_f / logarithmand.consequent) / Math.log(base)
|
95
|
+
else
|
96
|
+
Math.log(logarithmand) / Math.log(base)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# TODO Explain how this works
|
101
|
+
#
|
102
|
+
def method_missing(op, *args, &blk)
|
103
|
+
rhs = args.collect do |arg|
|
104
|
+
arg.kind_of?(self.class) ? arg.inspect : arg
|
105
|
+
end
|
106
|
+
result = logarithm.send(op, *rhs)
|
107
|
+
return result if op == :coerce
|
108
|
+
case result
|
109
|
+
when Numeric
|
110
|
+
self.class.new(result)
|
111
|
+
# Work this case out
|
112
|
+
#when Array
|
113
|
+
# result.collect do |e|
|
114
|
+
# e.kind_of?(Numeric) ? Cents.new(e) : e
|
115
|
+
# end
|
116
|
+
else
|
117
|
+
result
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|