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
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
|