tonal-tools 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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