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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 104f86d9527406e4ebb2bc443347c1cd6c45c4b8380c8739c435c504a4381842
|
4
|
+
data.tar.gz: a605b1bdd7c4fc7c20f6a66417422c9e55909bc9eaacaa5e4336280ca8e2f2a4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: '068a485c121ef0941c05d4fcc2e67e607aa8572fd2ba572e7481f5f54189cdeff0c4e04648b2809db35e2bc533dffe81c406bd0873093719bf5a8ea388718f44'
|
7
|
+
data.tar.gz: b5d341be94a1ef9bb7c57cf5974b647bcd943151386c8a9fd4615f1a6f3e251ca8567994870d8882263e7963bcdaeb3214490a4bd245fdb423543fc41ef032f9
|
@@ -0,0 +1,148 @@
|
|
1
|
+
class Tonal::Ratio
|
2
|
+
class Approximations
|
3
|
+
DEFAULT_MAX_PRIME = Float::INFINITY
|
4
|
+
DEFAULT_MAX_GRID_SCALE = 100
|
5
|
+
DEFAULT_MAX_GRID_BOUNDARY = 5
|
6
|
+
DEFAULT_DEPTH = Float::INFINITY
|
7
|
+
DEFAULT_COMPLEXITY_AMOUNT = 50.0
|
8
|
+
CONVERGENT_LIMIT = 10
|
9
|
+
|
10
|
+
extend Forwardable
|
11
|
+
def_delegators :@ratio, :antecedent, :consequent, :to_cents, :to_basic_ratio, :to_f
|
12
|
+
|
13
|
+
attr_reader :ratio
|
14
|
+
|
15
|
+
def initialize(ratio:)
|
16
|
+
raise ArgumentError, "Tonal::Ratio required" unless ratio.kind_of?(Tonal::Ratio)
|
17
|
+
@ratio = ratio
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [Array] of ratios within cent tolerance of self found using continued fraction approximation
|
21
|
+
# @example
|
22
|
+
# Tonal::Ratio.ed(12,1).by_continued_fraction
|
23
|
+
# => [(18/17), (196/185), (1657/1564), (7893/7450), (18904/17843), (3118/2943), (1461/1379), (89/84), (17/16)]
|
24
|
+
# @param cents_tolerance
|
25
|
+
# @param depth
|
26
|
+
# @param max_prime
|
27
|
+
# @param conv_limit
|
28
|
+
#
|
29
|
+
def by_continued_fraction(cents_tolerance: Tonal::Cents::TOLERANCE, depth: DEFAULT_DEPTH, max_prime: DEFAULT_MAX_PRIME, conv_limit: CONVERGENT_LIMIT)
|
30
|
+
self_in_cents = to_cents
|
31
|
+
within = cents_tolerance.kind_of?(Tonal::Cents) ? cents_tolerance : Tonal::Cents.new(cents: cents_tolerance)
|
32
|
+
[].tap do |results|
|
33
|
+
ContinuedFraction.new(antecedent.to_f/consequent, conv_limit).convergents_as_rationals.each do |convergent|
|
34
|
+
ratio2 = ratio.class.new(convergent.numerator,convergent.denominator)
|
35
|
+
results << ratio2 if ratio.class.within_cents?(self_in_cents, ratio2.to_cents, within) && ratio2.within_prime?(max_prime)
|
36
|
+
break if results.length >= depth
|
37
|
+
end
|
38
|
+
end.sort
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Array] of ratios within cent tolerance of self found using a quotient walk on the fraction tree
|
42
|
+
# @example
|
43
|
+
# Tonal::Ratio.ed(12,1).by_quotient_walk(max_prime: 89)
|
44
|
+
# => [(18/17), (196/185), (89/84), (71/67), (53/50), (35/33), (17/16)]
|
45
|
+
# @param cents_tolerance
|
46
|
+
# @param depth
|
47
|
+
# @param max_prime
|
48
|
+
#
|
49
|
+
def by_quotient_walk(cents_tolerance: Tonal::Cents::TOLERANCE, depth: DEFAULT_DEPTH, max_prime: DEFAULT_MAX_PRIME, conv_limit: CONVERGENT_LIMIT)
|
50
|
+
self_in_cents = to_cents
|
51
|
+
within = cents_tolerance.kind_of?(Tonal::Cents) ? cents_tolerance : Tonal::Cents.new(cents: cents_tolerance)
|
52
|
+
|
53
|
+
[].tap do |results|
|
54
|
+
FractionTree.quotient_walk(to_f, limit: conv_limit).each do |node|
|
55
|
+
ratio2 = ratio.class.new(node.weight)
|
56
|
+
results << ratio2 if ratio.class.within_cents?(self_in_cents, ratio2.to_cents, within) && ratio2.within_prime?(max_prime)
|
57
|
+
break if results.length >= depth
|
58
|
+
end
|
59
|
+
end.sort
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [Array] of fraction tree ratios within cent tolerance of self
|
63
|
+
# @example
|
64
|
+
# Tonal::Ratio.ed(12,1).by_tree_path(max_prime: 17)
|
65
|
+
# => [(18/17), (35/33), (17/16)]
|
66
|
+
# @param cents_tolerance
|
67
|
+
# @param depth
|
68
|
+
# @param max_prime
|
69
|
+
#
|
70
|
+
def by_tree_path(cents_tolerance: Tonal::Cents::TOLERANCE, depth: DEFAULT_DEPTH, max_prime: DEFAULT_MAX_PRIME)
|
71
|
+
self_in_cents = to_cents
|
72
|
+
within = cents_tolerance.kind_of?(Tonal::Cents) ? cents_tolerance : Tonal::Cents.new(cents: cents_tolerance)
|
73
|
+
[].tap do |results|
|
74
|
+
FractionTree.path_to(to_f).each do |node|
|
75
|
+
ratio2 = ratio.class.new(node.weight)
|
76
|
+
results << ratio2 if ratio.class.within_cents?(self_in_cents, ratio2.to_cents, within) && ratio2.within_prime?(max_prime)
|
77
|
+
break if results.length >= depth
|
78
|
+
end
|
79
|
+
end.sort
|
80
|
+
end
|
81
|
+
|
82
|
+
# @return [Array] of ratios within cent tolerance of self found on the ratio grid
|
83
|
+
# @example
|
84
|
+
# Tonal::Ratio.new(3,2).by_neighborhood(max_prime: 23, cents_tolerance: 5, max_boundary: 10, max_scale: 60)
|
85
|
+
# => [(175/117), (176/117)]
|
86
|
+
# @param cents_tolerance the maximum cents self is allowed from grid ratios
|
87
|
+
# @param depth the maximum depth the array will get
|
88
|
+
# @param max_prime the maximum prime the grid ratios will contain
|
89
|
+
# @param max_boundary the maximum distance grid ratios will be from the scaled ratio
|
90
|
+
# @param max_scale the maximum self will be scaled
|
91
|
+
#
|
92
|
+
def by_neighborhood(cents_tolerance: Tonal::Cents::TOLERANCE, depth: DEFAULT_DEPTH, max_prime: DEFAULT_MAX_PRIME, max_boundary: DEFAULT_MAX_GRID_BOUNDARY, max_scale: DEFAULT_MAX_GRID_SCALE)
|
93
|
+
self_in_cents = to_cents
|
94
|
+
within = cents_tolerance.kind_of?(Tonal::Cents) ? cents_tolerance : Tonal::Cents.new(cents: cents_tolerance)
|
95
|
+
[].tap do |results|
|
96
|
+
scale = 1
|
97
|
+
boundary = 1
|
98
|
+
|
99
|
+
while results.length <= depth && scale <= max_scale do
|
100
|
+
while boundary <= max_boundary
|
101
|
+
vacinity = ratio.respond_to?(:to_basic_ratio) ? to_basic_ratio.scale(scale) : ratio.scale(scale)
|
102
|
+
self.class.neighbors(away: boundary, vacinity: vacinity).each do |neighbor|
|
103
|
+
results << neighbor if ratio.class.within_cents?(self_in_cents, neighbor.to_cents, within) && neighbor.within_prime?(max_prime)
|
104
|
+
end
|
105
|
+
boundary += 1
|
106
|
+
end
|
107
|
+
boundary = 1
|
108
|
+
scale += 1
|
109
|
+
end
|
110
|
+
end.uniq(&:to_r).reject{|r| r == ratio}.sort
|
111
|
+
end
|
112
|
+
|
113
|
+
# @return [Array] of bounding ratios in the ratio grid vacinity of antecedent/consequent scaled by scale
|
114
|
+
# @example
|
115
|
+
# Tonal::ReducedRatio.new(3,2).neighborhood(scale: 256, boundary: 2)
|
116
|
+
# => [(768/514), (766/512), (768/513), (767/512), (768/512), (769/512), (768/511), (770/512), (768/510)]
|
117
|
+
# @param scale [Integer] used to scale antecedent/consequent on coordinate system
|
118
|
+
# @param boundary [Integer] limit within which to calculate neighboring ratios
|
119
|
+
#
|
120
|
+
def neighborhood(scale: 2**0, boundary: 1)
|
121
|
+
scale = scale.round
|
122
|
+
vacinity = ratio.respond_to?(:to_basic_ratio) ? to_basic_ratio.scale(scale) : ratio.scale(scale)
|
123
|
+
SortedSet.new([].tap do |ratio_list|
|
124
|
+
1.upto(boundary) do |away|
|
125
|
+
ratio_list << self.class.neighbors(away: away, vacinity: vacinity)
|
126
|
+
end
|
127
|
+
end.flatten).to_a
|
128
|
+
end
|
129
|
+
|
130
|
+
# @return [Array] an array of Tonal::Ratio neighbors in the scaled ratio's grid neighborhood
|
131
|
+
# @example
|
132
|
+
# Tonal::Ratio::Approximations.neighbors(vacinity: (3/2r).ratio(reduced:false).scale(256), away: 1)
|
133
|
+
# => [(768/513), (767/512), (768/512), (769/512), (768/511)]
|
134
|
+
# @param away [Integer] the neighbors distance away from self's antecedent and consequent
|
135
|
+
#
|
136
|
+
def self.neighbors(vacinity:, away: 1)
|
137
|
+
[vacinity,
|
138
|
+
vacinity.class.new(vacinity.antecedent+away, vacinity.consequent),
|
139
|
+
vacinity.class.new(vacinity.antecedent-away, vacinity.consequent),
|
140
|
+
vacinity.class.new(vacinity.antecedent, vacinity.consequent+away),
|
141
|
+
vacinity.class.new(vacinity.antecedent, vacinity.consequent-away),
|
142
|
+
vacinity.class.new(vacinity.antecedent+away, vacinity.consequent+away),
|
143
|
+
vacinity.class.new(vacinity.antecedent+away, vacinity.consequent-away),
|
144
|
+
vacinity.class.new(vacinity.antecedent-away, vacinity.consequent+away),
|
145
|
+
vacinity.class.new(vacinity.antecedent-away, vacinity.consequent-away)]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
data/lib/tonal/cents.rb
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
class Tonal::Cents
|
2
|
+
extend Forwardable
|
3
|
+
include Comparable
|
4
|
+
|
5
|
+
def_delegators :@log, :logarithm, :logarithmand, :base
|
6
|
+
|
7
|
+
HUNDREDTHS_ROUNDOFF = -2
|
8
|
+
FLOAT_PRECISION = 100
|
9
|
+
CENT_SCALE = 1200.0
|
10
|
+
TOLERANCE = 5
|
11
|
+
PRECISION = 2
|
12
|
+
DEFAULT_ROUNDING_PRECISION = 2
|
13
|
+
|
14
|
+
attr_reader :log, :ratio
|
15
|
+
|
16
|
+
# @return [Cents]
|
17
|
+
# @example
|
18
|
+
# Tonal::Cents.new(ratio: 2**(2.0/12)) => 200.0
|
19
|
+
# @param cents [Numeric, Tonal::Log2]
|
20
|
+
# @param log [Numeric, Tonal::Log2]
|
21
|
+
# @param ratio [Numeric, Tonal::Log2]
|
22
|
+
# @param precision [Numeric]
|
23
|
+
#
|
24
|
+
def initialize(cents: nil, log: nil, ratio: nil, precision: PRECISION)
|
25
|
+
raise ArgumentError, "One of cents:, log: or ratio: must be provided" unless [cents, log, ratio].compact.count == 1
|
26
|
+
|
27
|
+
@precision = precision
|
28
|
+
|
29
|
+
if cents
|
30
|
+
@log = derive_log(cents: cents)
|
31
|
+
@value = derive_cents(cents: cents)
|
32
|
+
@ratio = derive_ratio(log: @log)
|
33
|
+
elsif log
|
34
|
+
@log = derive_log(log: log)
|
35
|
+
@value = derive_cents(log: @log)
|
36
|
+
@ratio = derive_ratio(log: @log)
|
37
|
+
elsif ratio
|
38
|
+
@log = derive_log(ratio: ratio)
|
39
|
+
@value = derive_cents(log: @log)
|
40
|
+
@ratio = derive_ratio(ratio: ratio)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [Cents] the default cents tolerance
|
45
|
+
# @example
|
46
|
+
# Tonal::Cents.default_tolerance => 5
|
47
|
+
#
|
48
|
+
def self.default_tolerance
|
49
|
+
self.new(cents: TOLERANCE)
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [Float] value of self
|
53
|
+
# @example
|
54
|
+
# Tonal::Cents.new(ratio: 2**(1.0/12)).value => 100.00
|
55
|
+
#
|
56
|
+
def value(precision: @precision)
|
57
|
+
@value.round(precision)
|
58
|
+
end
|
59
|
+
alias :cents :value
|
60
|
+
|
61
|
+
# @return
|
62
|
+
# [Cents] nearest hundredth cent value
|
63
|
+
# @example
|
64
|
+
# Tonal::Cents.new(cents: 701.9550008653874).nearest_hundredth => 700.0
|
65
|
+
#
|
66
|
+
def nearest_hundredth
|
67
|
+
self.class.new(cents: value.round(Tonal::Cents::HUNDREDTHS_ROUNDOFF).to_f)
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return
|
71
|
+
# [Cents] nearest hundredth cent difference
|
72
|
+
# @example
|
73
|
+
# Tonal::Cents.new(701.9550008653874).nearest_hundredth_difference => 1.955000865387433
|
74
|
+
#
|
75
|
+
def nearest_hundredth_difference
|
76
|
+
self.class.new(cents: (value - nearest_hundredth))
|
77
|
+
end
|
78
|
+
|
79
|
+
# TODO: Document or Consider if needed
|
80
|
+
#
|
81
|
+
def plus_minus(limit = 5)
|
82
|
+
[self - limit, self + limit]
|
83
|
+
end
|
84
|
+
|
85
|
+
# @return
|
86
|
+
# [String] the string representation of Cents
|
87
|
+
# @example
|
88
|
+
# Tonal::Cents.new(100.0).inspect => "100.0"
|
89
|
+
#
|
90
|
+
def inspect
|
91
|
+
"#{value.round(@precision)}"
|
92
|
+
end
|
93
|
+
|
94
|
+
#
|
95
|
+
# Challenges to comparing floats
|
96
|
+
# https://www.rubydoc.info/gems/rubocop/RuboCop/Cop/Lint/FloatComparison
|
97
|
+
# https://embeddeduse.com/2019/08/26/qt-compare-two-floats/
|
98
|
+
#
|
99
|
+
def <=>(rhs)
|
100
|
+
rhs.kind_of?(self.class) ? value.round(2) <=> rhs.value.round(2) : value.round(2) <=> rhs.round(2)
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
def derive_log(cents: nil, ratio: nil, log: nil)
|
105
|
+
return Tonal::Log2.new(logarithm: cents / CENT_SCALE) if cents
|
106
|
+
return Tonal::Log2.new(logarithmand: ratio) if ratio
|
107
|
+
log.kind_of?(Tonal::Log2) ? log : Tonal::Log2.new(logarithm: log)
|
108
|
+
end
|
109
|
+
|
110
|
+
def derive_ratio(log: nil, ratio: nil)
|
111
|
+
return Tonal::ReducedRatio.new(log.logarithmand) if log
|
112
|
+
Tonal::ReducedRatio.new(ratio.numerator, ratio.denominator)
|
113
|
+
end
|
114
|
+
|
115
|
+
def derive_cents(cents: nil, log: nil)
|
116
|
+
return cents if cents
|
117
|
+
log.logarithm * CENT_SCALE if log
|
118
|
+
end
|
119
|
+
|
120
|
+
# All these operators are binary except for |, ~ and typeof , so the left
|
121
|
+
# hand (A) object would be the context object, and the right hand object (B)
|
122
|
+
# would be the argument passed to the operator member in A. For unary
|
123
|
+
# operators, there won't be arguments, and the function would be called upon
|
124
|
+
# its target. operator functions would only be called if the operator is
|
125
|
+
# explicitly used in the source code.
|
126
|
+
#
|
127
|
+
def method_missing(op, *args, &blk)
|
128
|
+
rhs = args.collect do |arg|
|
129
|
+
arg.kind_of?(self.class) ? arg.value : arg
|
130
|
+
end
|
131
|
+
result = value.send(op, *rhs)
|
132
|
+
return result if op == :coerce
|
133
|
+
case result
|
134
|
+
when Numeric
|
135
|
+
self.class.new(cents: result)
|
136
|
+
# TODO: Work this case out or remove
|
137
|
+
#when Array
|
138
|
+
# result.collect do |e|
|
139
|
+
# e.kind_of?(Numeric) ? Cents.new(e) : e
|
140
|
+
# end
|
141
|
+
else
|
142
|
+
result
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|