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