mtk 0.0.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.
- data/README.md +52 -0
- data/Rakefile +31 -0
- data/lib/mtk/chord.rb +47 -0
- data/lib/mtk/constants/dynamics.rb +56 -0
- data/lib/mtk/constants/intervals.rb +76 -0
- data/lib/mtk/constants/pitch_classes.rb +18 -0
- data/lib/mtk/constants/pitches.rb +24 -0
- data/lib/mtk/constants/pseudo_constants.rb +25 -0
- data/lib/mtk/event.rb +61 -0
- data/lib/mtk/midi/file.rb +179 -0
- data/lib/mtk/note.rb +44 -0
- data/lib/mtk/numeric_extensions.rb +61 -0
- data/lib/mtk/pattern/choice.rb +21 -0
- data/lib/mtk/pattern/note_sequence.rb +60 -0
- data/lib/mtk/pattern/pitch_sequence.rb +22 -0
- data/lib/mtk/pattern/sequence.rb +65 -0
- data/lib/mtk/patterns.rb +4 -0
- data/lib/mtk/pitch.rb +112 -0
- data/lib/mtk/pitch_class.rb +113 -0
- data/lib/mtk/pitch_class_set.rb +106 -0
- data/lib/mtk/pitch_set.rb +95 -0
- data/lib/mtk/timeline.rb +160 -0
- data/lib/mtk/util/mappable.rb +14 -0
- data/lib/mtk.rb +36 -0
- data/spec/mtk/chord_spec.rb +74 -0
- data/spec/mtk/constants/dynamics_spec.rb +94 -0
- data/spec/mtk/constants/intervals_spec.rb +140 -0
- data/spec/mtk/constants/pitch_classes_spec.rb +35 -0
- data/spec/mtk/constants/pitches_spec.rb +23 -0
- data/spec/mtk/event_spec.rb +120 -0
- data/spec/mtk/midi/file_spec.rb +208 -0
- data/spec/mtk/note_spec.rb +65 -0
- data/spec/mtk/numeric_extensions_spec.rb +102 -0
- data/spec/mtk/pattern/choice_spec.rb +21 -0
- data/spec/mtk/pattern/note_sequence_spec.rb +121 -0
- data/spec/mtk/pattern/pitch_sequence_spec.rb +47 -0
- data/spec/mtk/pattern/sequence_spec.rb +54 -0
- data/spec/mtk/pitch_class_set_spec.rb +103 -0
- data/spec/mtk/pitch_class_spec.rb +165 -0
- data/spec/mtk/pitch_set_spec.rb +163 -0
- data/spec/mtk/pitch_spec.rb +217 -0
- data/spec/mtk/timeline_spec.rb +234 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/test.mid +0 -0
- metadata +97 -0
@@ -0,0 +1,60 @@
|
|
1
|
+
module MTK
|
2
|
+
module Pattern
|
3
|
+
|
4
|
+
# A {Sequence} of {Note}s
|
5
|
+
class NoteSequence
|
6
|
+
|
7
|
+
attr_reader :pitch_sequence, :intensity_sequence, :duration_sequence
|
8
|
+
|
9
|
+
attr_accessor :pitch, :intensity, :duration
|
10
|
+
|
11
|
+
def initialize(pitches, intensities=nil, durations=nil, defaults={})
|
12
|
+
@pitch_sequence = PitchSequence.new(pitches)
|
13
|
+
@intensity_sequence = Sequence.new(intensities)
|
14
|
+
@duration_sequence = Sequence.new(durations)
|
15
|
+
@default = {:pitch => Pitches::C4, :intensity => Dynamics::mf, :duration => 1}.merge defaults
|
16
|
+
reset
|
17
|
+
end
|
18
|
+
|
19
|
+
def pitches
|
20
|
+
@pitch_sequence.elements
|
21
|
+
end
|
22
|
+
|
23
|
+
def intensities
|
24
|
+
@intensity_sequence.elements
|
25
|
+
end
|
26
|
+
|
27
|
+
def durations
|
28
|
+
@duration_sequence.elements
|
29
|
+
end
|
30
|
+
|
31
|
+
# reset the Sequence to the beginning
|
32
|
+
def reset
|
33
|
+
@pitch_sequence.reset
|
34
|
+
@intensity_sequence.reset
|
35
|
+
@duration_sequence.reset
|
36
|
+
|
37
|
+
@pitch = @default[:pitch]
|
38
|
+
@intensity = @default[:intensity]
|
39
|
+
@duration = @default[:duration]
|
40
|
+
end
|
41
|
+
|
42
|
+
# return next {Note} in sequence
|
43
|
+
def next
|
44
|
+
@pitch = @pitch_sequence.next || @pitch
|
45
|
+
@intensity = @intensity_sequence.next || @intensity
|
46
|
+
@duration = @duration_sequence.next || @duration
|
47
|
+
|
48
|
+
|
49
|
+
case @pitch
|
50
|
+
when PitchSet,Array then Chord.new(@pitch, @intensity, @duration)
|
51
|
+
else Note.new(@pitch, @intensity, @duration)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module MTK
|
2
|
+
module Pattern
|
3
|
+
|
4
|
+
# A {Sequence} of pitch-related elements, which may be {Pitch}es, {PitchClass}es, {PitchSet}s, or {Intervals} (Numeric)
|
5
|
+
class PitchSequence < Sequence
|
6
|
+
|
7
|
+
protected
|
8
|
+
|
9
|
+
# extend value_of to handle intervals and PitchClasses
|
10
|
+
def value_of element
|
11
|
+
element = super # eval Procs
|
12
|
+
case element
|
13
|
+
when Numeric then @value + element if @value # add interval
|
14
|
+
when PitchClass then @value.nearest(element) if @value
|
15
|
+
else element
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module MTK
|
2
|
+
module Pattern
|
3
|
+
|
4
|
+
# An endless enumerator that outputs an element one at a time from a list of elements,
|
5
|
+
# looping back to the beginning when elements run out.
|
6
|
+
class Sequence
|
7
|
+
|
8
|
+
# The list of elements enumerated by this Sequence
|
9
|
+
attr_reader :elements
|
10
|
+
|
11
|
+
# @param elements [Array] the list of {#elements}
|
12
|
+
# @param default [Object] the default value returned by {#next} in place of nil
|
13
|
+
def initialize(elements)
|
14
|
+
if elements.respond_to? :elements
|
15
|
+
@elements = elements.elements
|
16
|
+
else
|
17
|
+
@elements = elements.to_a
|
18
|
+
end
|
19
|
+
reset
|
20
|
+
end
|
21
|
+
|
22
|
+
# reset the Sequence to its initial state
|
23
|
+
def reset
|
24
|
+
@index = -1
|
25
|
+
@element = nil
|
26
|
+
@value = nil
|
27
|
+
end
|
28
|
+
|
29
|
+
# The value of the next element in the Sequence.
|
30
|
+
# The sequence goes back to the first element if there are no more elements.
|
31
|
+
#
|
32
|
+
# @return if an element is a Proc, it is called (depending on the arity of the Proc: with either no arguments,
|
33
|
+
# the last value of #next, or the last value and last element) and its return value is returned.
|
34
|
+
# @return if an element is nil, previous value of #next is returned. If there is no previous value the @default is returned.
|
35
|
+
# @return otherwise the element itself is returned
|
36
|
+
#
|
37
|
+
def next
|
38
|
+
if @elements and not @elements.empty?
|
39
|
+
@index = (@index + 1) % @elements.length
|
40
|
+
element = @elements[@index]
|
41
|
+
value = value_of(element)
|
42
|
+
@element, @value = element, value
|
43
|
+
end
|
44
|
+
@value
|
45
|
+
end
|
46
|
+
|
47
|
+
####################
|
48
|
+
protected
|
49
|
+
|
50
|
+
def value_of element
|
51
|
+
case element
|
52
|
+
when Proc
|
53
|
+
case element.arity
|
54
|
+
when 0 then element.call
|
55
|
+
when 1 then element.call(@value)
|
56
|
+
else element.call(@value, @element)
|
57
|
+
end
|
58
|
+
else element
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
data/lib/mtk/patterns.rb
ADDED
data/lib/mtk/pitch.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
module MTK
|
2
|
+
|
3
|
+
# A frequency represented by a {PitchClass}, an integer octave, and an offset in semitones.
|
4
|
+
|
5
|
+
class Pitch
|
6
|
+
|
7
|
+
include Comparable
|
8
|
+
|
9
|
+
attr_reader :pitch_class, :octave, :offset
|
10
|
+
|
11
|
+
def initialize( pitch_class, octave, offset=0 )
|
12
|
+
@pitch_class, @octave, @offset = pitch_class, octave, offset
|
13
|
+
@value = @pitch_class.to_i + 12*(@octave+1) + @offset
|
14
|
+
end
|
15
|
+
|
16
|
+
@flyweight = {}
|
17
|
+
|
18
|
+
def self.[](pitch_class, octave)
|
19
|
+
@flyweight[[pitch_class,octave]] ||= new(pitch_class, octave)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.from_s( s )
|
23
|
+
# TODO: update to handle offset
|
24
|
+
s = s[0..0].upcase + s[1..-1].downcase # normalize name
|
25
|
+
if s =~ /^([A-G](#|##|b|bb)?)(-?\d+)$/
|
26
|
+
pitch_class = PitchClass.from_s($1)
|
27
|
+
if pitch_class
|
28
|
+
octave = $3.to_i
|
29
|
+
new( pitch_class, octave )
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Convert a Numeric semitones value into a Pitch
|
35
|
+
def self.from_f( f )
|
36
|
+
i, offset = f.floor, f%1 # split into int and fractional part
|
37
|
+
pitch_class = PitchClass.from_i(i)
|
38
|
+
octave = i/12 - 1
|
39
|
+
new( pitch_class, octave, offset )
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.from_hash(hash)
|
43
|
+
new hash[:pitch_class], hash[:octave], hash.fetch(:offset,0)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Convert a Numeric semitones value into a Pitch
|
47
|
+
def self.from_i( i )
|
48
|
+
from_f( i )
|
49
|
+
end
|
50
|
+
|
51
|
+
# The numerical value of this pitch
|
52
|
+
def to_f
|
53
|
+
@value
|
54
|
+
end
|
55
|
+
|
56
|
+
# The numerical value for the nearest semitone
|
57
|
+
def to_i
|
58
|
+
@value.round
|
59
|
+
end
|
60
|
+
|
61
|
+
def offset_in_cents
|
62
|
+
@offset * 100
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_hash
|
66
|
+
{:pitch_class => @pitch_class, :octave => @octave, :offset => @offset}
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_s
|
70
|
+
"#{@pitch_class}#{@octave}" + (@offset.zero? ? '' : "+#{offset_in_cents.round}cents")
|
71
|
+
end
|
72
|
+
|
73
|
+
def inspect
|
74
|
+
"#{@pitch_class}#{@octave}" + (@offset.zero? ? '' : "+#{offset_in_cents}cents")
|
75
|
+
end
|
76
|
+
|
77
|
+
def ==( other )
|
78
|
+
other.respond_to? :pitch_class and other.respond_to? :octave and other.respond_to? :offset and
|
79
|
+
other.pitch_class == @pitch_class and other.octave == @octave and other.offset == @offset
|
80
|
+
end
|
81
|
+
|
82
|
+
def <=> other
|
83
|
+
@value <=> other.to_f
|
84
|
+
end
|
85
|
+
|
86
|
+
def + interval_in_semitones
|
87
|
+
self.class.from_f( @value + interval_in_semitones.to_f )
|
88
|
+
end
|
89
|
+
|
90
|
+
def - interval_in_semitones
|
91
|
+
self.class.from_f( @value - interval_in_semitones.to_f )
|
92
|
+
end
|
93
|
+
|
94
|
+
def invert(center_pitch)
|
95
|
+
self + 2*(center_pitch.to_f - to_f)
|
96
|
+
end
|
97
|
+
|
98
|
+
def nearest(pitch_class)
|
99
|
+
self + self.pitch_class.distance_to(pitch_class)
|
100
|
+
end
|
101
|
+
|
102
|
+
def coerce(other)
|
103
|
+
return self.class.from_f(other.to_f), self
|
104
|
+
end
|
105
|
+
|
106
|
+
def clone_with(hash)
|
107
|
+
self.class.from_hash(to_hash.merge hash)
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module MTK
|
2
|
+
|
3
|
+
# A class of pitches under octave equivalence.
|
4
|
+
#
|
5
|
+
# A {Pitch} has the same PitchClass as the {Pitches} one or more octaves away.
|
6
|
+
|
7
|
+
class PitchClass
|
8
|
+
|
9
|
+
NAMES = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']
|
10
|
+
|
11
|
+
VALID_NAMES = [
|
12
|
+
['B#', 'C', 'Dbb'],
|
13
|
+
['B##', 'C#', 'Db'],
|
14
|
+
['C##', 'D', 'Ebb'],
|
15
|
+
['D#', 'Eb', 'Fbb'],
|
16
|
+
['D##', 'E', 'Fb'],
|
17
|
+
['E#', 'F', 'Gbb'],
|
18
|
+
['E##', 'F#', 'Gb'],
|
19
|
+
['F##', 'G', 'Abb'],
|
20
|
+
['G#', 'Ab'],
|
21
|
+
['G##', 'A', 'Bbb'],
|
22
|
+
['A#', 'Bb', 'Cbb'],
|
23
|
+
['A##', 'B', 'Cb']
|
24
|
+
]
|
25
|
+
|
26
|
+
attr_reader :name
|
27
|
+
|
28
|
+
##########################################
|
29
|
+
private
|
30
|
+
|
31
|
+
def initialize(name, int_value)
|
32
|
+
@name, @int_value = name, int_value
|
33
|
+
end
|
34
|
+
|
35
|
+
private_class_method :new
|
36
|
+
|
37
|
+
@flyweight = {}
|
38
|
+
|
39
|
+
def self.get(name, int_value)
|
40
|
+
@flyweight[name] ||= new(name, int_value)
|
41
|
+
end
|
42
|
+
|
43
|
+
private_class_method :get
|
44
|
+
|
45
|
+
##########################################
|
46
|
+
public
|
47
|
+
|
48
|
+
def self.from_s(s)
|
49
|
+
s = s.to_s
|
50
|
+
s = s[0..0].upcase + s[1..-1].downcase # normalize the name
|
51
|
+
VALID_NAMES.each_with_index do |names, index|
|
52
|
+
return get(s, index) if names.include? s
|
53
|
+
end
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
class << self
|
58
|
+
alias from_name from_s # alias self.from_name to self.from_s
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.from_i(value)
|
62
|
+
value = value.to_i % 12
|
63
|
+
name = NAMES[value]
|
64
|
+
get(name, value)
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.[](name_or_value)
|
68
|
+
if name_or_value.kind_of? Numeric
|
69
|
+
from_i(name_or_value)
|
70
|
+
else
|
71
|
+
from_s(name_or_value)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def == other
|
76
|
+
other.is_a? PitchClass and other.to_i == @int_value
|
77
|
+
end
|
78
|
+
|
79
|
+
def <=> other
|
80
|
+
@int_value <=> other.to_i
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_s
|
84
|
+
@name
|
85
|
+
end
|
86
|
+
|
87
|
+
def to_i
|
88
|
+
@int_value
|
89
|
+
end
|
90
|
+
|
91
|
+
def +(interval)
|
92
|
+
self.class.from_i(to_i + interval.to_i)
|
93
|
+
end
|
94
|
+
|
95
|
+
def -(interval)
|
96
|
+
self.class.from_i(to_i - interval.to_i)
|
97
|
+
end
|
98
|
+
|
99
|
+
# the smallest interval in semitones that needs to be added to this PitchClass to reach the given PitchClass
|
100
|
+
def distance_to(pitch_class)
|
101
|
+
delta = (pitch_class.to_i - to_i) % 12
|
102
|
+
if delta > 6
|
103
|
+
delta -= 12
|
104
|
+
elsif delta == 6 and to_i >= 6
|
105
|
+
# this is a special edge case to prevent endlessly ascending pitch sequences when alternating between two pitch classes a tritone apart
|
106
|
+
delta = -6
|
107
|
+
end
|
108
|
+
delta
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module MTK
|
2
|
+
|
3
|
+
# A Set of PitchClasses, for 12-tone set-theory pitch analysis and manipulations
|
4
|
+
#
|
5
|
+
class PitchClassSet
|
6
|
+
|
7
|
+
include Mappable
|
8
|
+
|
9
|
+
attr_reader :pitch_classes
|
10
|
+
|
11
|
+
def initialize(pitch_classes)
|
12
|
+
@pitch_classes = pitch_classes.to_a.uniq.sort.freeze
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.from_a enumerable
|
16
|
+
new enumerable
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_a
|
20
|
+
Array.new(@pitch_classes)
|
21
|
+
end
|
22
|
+
|
23
|
+
def each &block
|
24
|
+
@pitch_classes.each &block
|
25
|
+
end
|
26
|
+
|
27
|
+
def size
|
28
|
+
@pitch_classes.size
|
29
|
+
end
|
30
|
+
|
31
|
+
def normal_order
|
32
|
+
ordering = Array.new(@pitch_classes)
|
33
|
+
min_span, start_index_for_normal_order = nil, nil
|
34
|
+
|
35
|
+
# check every rotation for the minimal span:
|
36
|
+
size.times do |index|
|
37
|
+
span = self.class.span_for ordering
|
38
|
+
|
39
|
+
if min_span.nil? or span < min_span
|
40
|
+
# best so far
|
41
|
+
min_span = span
|
42
|
+
start_index_for_normal_order = index
|
43
|
+
|
44
|
+
elsif span == min_span
|
45
|
+
# handle ties, minimize distance between first and second-to-last note, then first and third-to-last, etc
|
46
|
+
span1, span2 = nil, nil
|
47
|
+
tie_breaker = 1
|
48
|
+
while span1 == span2 and tie_breaker < size
|
49
|
+
span1 = self.class.span_between( ordering[0], ordering[-1 - tie_breaker] )
|
50
|
+
span2 = self.class.span_between( ordering[start_index_for_normal_order], ordering[start_index_for_normal_order - tie_breaker] )
|
51
|
+
tie_breaker -= 1
|
52
|
+
end
|
53
|
+
if span1 != span2
|
54
|
+
# tie cannot be broken, pick the one starting with the lowest pitch class
|
55
|
+
if ordering[0].to_i < ordering[start_index_for_normal_order].to_i
|
56
|
+
start_index_for_normal_order = index
|
57
|
+
end
|
58
|
+
elsif span1 < span2
|
59
|
+
start_index_for_normal_order = index
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
ordering << ordering.shift # rotate
|
64
|
+
end
|
65
|
+
|
66
|
+
# we've rotated all the way around, so we now need to rotate back to the start index we just found:
|
67
|
+
start_index_for_normal_order.times{ ordering << ordering.shift }
|
68
|
+
|
69
|
+
ordering
|
70
|
+
end
|
71
|
+
|
72
|
+
def normal_form
|
73
|
+
norder = normal_order
|
74
|
+
first_pc_val = norder.first.to_i
|
75
|
+
norder.map{|pitch_class| (pitch_class.to_i - first_pc_val) % 12 }
|
76
|
+
end
|
77
|
+
|
78
|
+
# @param other [#pitch_classes, #to_a, Array]
|
79
|
+
def == other
|
80
|
+
if other.respond_to? :pitch_classes
|
81
|
+
@pitch_classes == other.pitch_classes
|
82
|
+
elsif other.respond_to? :to_a
|
83
|
+
@pitch_classes == other.to_a
|
84
|
+
else
|
85
|
+
@pitch_classes == other
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def to_s
|
90
|
+
@pitch_classes.join(' ')
|
91
|
+
end
|
92
|
+
|
93
|
+
def inspect
|
94
|
+
@pitch_classes.inspect
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.span_for(pitch_classes)
|
98
|
+
span_between pitch_classes.first, pitch_classes.last
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.span_between(pc1, pc2)
|
102
|
+
(pc2.to_i - pc1.to_i) % 12
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module MTK
|
2
|
+
|
3
|
+
# A set of {Pitch}es
|
4
|
+
#
|
5
|
+
class PitchSet
|
6
|
+
|
7
|
+
include Mappable
|
8
|
+
|
9
|
+
attr_reader :pitches
|
10
|
+
|
11
|
+
def initialize(pitches)
|
12
|
+
@pitches = pitches.to_a.uniq.sort.freeze
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.from_a enumerable
|
16
|
+
new enumerable
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_a
|
20
|
+
Array.new(@pitches)
|
21
|
+
end
|
22
|
+
|
23
|
+
def each &block
|
24
|
+
@pitches.each &block
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_pitch_class_set
|
28
|
+
PitchClassSet.new @pitches.map{|p| p.pitch_class }
|
29
|
+
end
|
30
|
+
|
31
|
+
def pitch_classes
|
32
|
+
@pitch_classes ||= @pitches.map{|p| p.pitch_class }.uniq
|
33
|
+
end
|
34
|
+
|
35
|
+
def + semitones
|
36
|
+
each_pitch_apply :+, semitones
|
37
|
+
end
|
38
|
+
|
39
|
+
def - semitones
|
40
|
+
each_pitch_apply :-, semitones
|
41
|
+
end
|
42
|
+
|
43
|
+
def invert(center_pitch=@pitches.first)
|
44
|
+
each_pitch_apply :invert, center_pitch
|
45
|
+
end
|
46
|
+
|
47
|
+
def inversion(number)
|
48
|
+
number = number.to_i
|
49
|
+
pitch_set = Array.new(@pitches)
|
50
|
+
if number > 0
|
51
|
+
number.times do |count|
|
52
|
+
index = count % pitch_set.length
|
53
|
+
pitch_set[index] += 12
|
54
|
+
end
|
55
|
+
else
|
56
|
+
number.abs.times do |count|
|
57
|
+
index = -(count + 1) % pitch_set.length # count from -1 downward to go backwards through the list starting at the end
|
58
|
+
pitch_set[index] -= 12
|
59
|
+
end
|
60
|
+
end
|
61
|
+
self.class.new pitch_set
|
62
|
+
end
|
63
|
+
|
64
|
+
def include? pitch
|
65
|
+
@pitches.include? pitch
|
66
|
+
end
|
67
|
+
|
68
|
+
def nearest(pitch_class)
|
69
|
+
self + @pitches.first.pitch_class.distance_to(pitch_class)
|
70
|
+
end
|
71
|
+
|
72
|
+
# @param other [#pitches, #to_a, Array]
|
73
|
+
def == other
|
74
|
+
if other.respond_to? :pitches
|
75
|
+
@pitches == other.pitches
|
76
|
+
elsif other.respond_to? :to_a
|
77
|
+
@pitches == other.to_a
|
78
|
+
else
|
79
|
+
@pitches == other
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_s
|
84
|
+
@pitches.inspect
|
85
|
+
end
|
86
|
+
|
87
|
+
#######################################
|
88
|
+
protected
|
89
|
+
|
90
|
+
def each_pitch_apply(method_name, *args, &block)
|
91
|
+
self.class.new @pitches.map{|pitch| pitch.send(method_name, *args, &block) }
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
end
|