jmtk 0.0.3.3-java
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/.yardopts +10 -0
- data/DEVELOPMENT_NOTES.md +115 -0
- data/INTRO.md +129 -0
- data/LICENSE.txt +27 -0
- data/README.md +50 -0
- data/Rakefile +102 -0
- data/bin/jmtk +250 -0
- data/bin/mtk +250 -0
- data/examples/crescendo.rb +20 -0
- data/examples/drum_pattern.rb +23 -0
- data/examples/dynamic_pattern.rb +36 -0
- data/examples/gets_and_play.rb +27 -0
- data/examples/notation.rb +22 -0
- data/examples/play_midi.rb +17 -0
- data/examples/print_midi.rb +13 -0
- data/examples/random_tone_row.rb +18 -0
- data/examples/syntax_to_midi.rb +28 -0
- data/examples/test_output.rb +7 -0
- data/examples/tone_row_melody.rb +23 -0
- data/lib/mtk.rb +76 -0
- data/lib/mtk/core/duration.rb +213 -0
- data/lib/mtk/core/intensity.rb +158 -0
- data/lib/mtk/core/interval.rb +157 -0
- data/lib/mtk/core/pitch.rb +154 -0
- data/lib/mtk/core/pitch_class.rb +194 -0
- data/lib/mtk/events/event.rb +119 -0
- data/lib/mtk/events/note.rb +112 -0
- data/lib/mtk/events/parameter.rb +54 -0
- data/lib/mtk/events/timeline.rb +232 -0
- data/lib/mtk/groups/chord.rb +56 -0
- data/lib/mtk/groups/collection.rb +196 -0
- data/lib/mtk/groups/melody.rb +96 -0
- data/lib/mtk/groups/pitch_class_set.rb +163 -0
- data/lib/mtk/groups/pitch_collection.rb +23 -0
- data/lib/mtk/io/dls_synth_device.rb +146 -0
- data/lib/mtk/io/dls_synth_output.rb +62 -0
- data/lib/mtk/io/jsound_input.rb +87 -0
- data/lib/mtk/io/jsound_output.rb +82 -0
- data/lib/mtk/io/midi_file.rb +209 -0
- data/lib/mtk/io/midi_input.rb +97 -0
- data/lib/mtk/io/midi_output.rb +195 -0
- data/lib/mtk/io/notation.rb +162 -0
- data/lib/mtk/io/unimidi_input.rb +117 -0
- data/lib/mtk/io/unimidi_output.rb +140 -0
- data/lib/mtk/lang/durations.rb +57 -0
- data/lib/mtk/lang/intensities.rb +61 -0
- data/lib/mtk/lang/intervals.rb +73 -0
- data/lib/mtk/lang/mtk_grammar.citrus +237 -0
- data/lib/mtk/lang/parser.rb +29 -0
- data/lib/mtk/lang/pitch_classes.rb +29 -0
- data/lib/mtk/lang/pitches.rb +52 -0
- data/lib/mtk/lang/pseudo_constants.rb +26 -0
- data/lib/mtk/lang/variable.rb +32 -0
- data/lib/mtk/numeric_extensions.rb +66 -0
- data/lib/mtk/patterns/chain.rb +49 -0
- data/lib/mtk/patterns/choice.rb +43 -0
- data/lib/mtk/patterns/cycle.rb +18 -0
- data/lib/mtk/patterns/for_each.rb +71 -0
- data/lib/mtk/patterns/function.rb +39 -0
- data/lib/mtk/patterns/lines.rb +54 -0
- data/lib/mtk/patterns/palindrome.rb +45 -0
- data/lib/mtk/patterns/pattern.rb +171 -0
- data/lib/mtk/patterns/sequence.rb +20 -0
- data/lib/mtk/sequencers/event_builder.rb +132 -0
- data/lib/mtk/sequencers/legato_sequencer.rb +24 -0
- data/lib/mtk/sequencers/rhythmic_sequencer.rb +28 -0
- data/lib/mtk/sequencers/sequencer.rb +111 -0
- data/lib/mtk/sequencers/step_sequencer.rb +26 -0
- data/spec/mtk/core/duration_spec.rb +372 -0
- data/spec/mtk/core/intensity_spec.rb +289 -0
- data/spec/mtk/core/interval_spec.rb +265 -0
- data/spec/mtk/core/pitch_class_spec.rb +343 -0
- data/spec/mtk/core/pitch_spec.rb +297 -0
- data/spec/mtk/events/event_spec.rb +234 -0
- data/spec/mtk/events/note_spec.rb +174 -0
- data/spec/mtk/events/parameter_spec.rb +220 -0
- data/spec/mtk/events/timeline_spec.rb +430 -0
- data/spec/mtk/groups/chord_spec.rb +85 -0
- data/spec/mtk/groups/collection_spec.rb +374 -0
- data/spec/mtk/groups/melody_spec.rb +225 -0
- data/spec/mtk/groups/pitch_class_set_spec.rb +340 -0
- data/spec/mtk/io/midi_file_spec.rb +243 -0
- data/spec/mtk/io/midi_output_spec.rb +102 -0
- data/spec/mtk/lang/durations_spec.rb +89 -0
- data/spec/mtk/lang/intensities_spec.rb +101 -0
- data/spec/mtk/lang/intervals_spec.rb +143 -0
- data/spec/mtk/lang/parser_spec.rb +603 -0
- data/spec/mtk/lang/pitch_classes_spec.rb +62 -0
- data/spec/mtk/lang/pitches_spec.rb +56 -0
- data/spec/mtk/lang/pseudo_constants_spec.rb +20 -0
- data/spec/mtk/lang/variable_spec.rb +52 -0
- data/spec/mtk/numeric_extensions_spec.rb +83 -0
- data/spec/mtk/patterns/chain_spec.rb +110 -0
- data/spec/mtk/patterns/choice_spec.rb +97 -0
- data/spec/mtk/patterns/cycle_spec.rb +123 -0
- data/spec/mtk/patterns/for_each_spec.rb +136 -0
- data/spec/mtk/patterns/function_spec.rb +120 -0
- data/spec/mtk/patterns/lines_spec.rb +77 -0
- data/spec/mtk/patterns/palindrome_spec.rb +108 -0
- data/spec/mtk/patterns/pattern_spec.rb +132 -0
- data/spec/mtk/patterns/sequence_spec.rb +203 -0
- data/spec/mtk/sequencers/event_builder_spec.rb +245 -0
- data/spec/mtk/sequencers/legato_sequencer_spec.rb +45 -0
- data/spec/mtk/sequencers/rhythmic_sequencer_spec.rb +84 -0
- data/spec/mtk/sequencers/sequencer_spec.rb +215 -0
- data/spec/mtk/sequencers/step_sequencer_spec.rb +93 -0
- data/spec/spec_coverage.rb +2 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/test.mid +0 -0
- metadata +226 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
module MTK
|
2
|
+
module Groups
|
3
|
+
|
4
|
+
# A sorted collection of distinct {Pitch}es.
|
5
|
+
#
|
6
|
+
# The "vertical" (simultaneous) pitch collection.
|
7
|
+
#
|
8
|
+
# @see Melody
|
9
|
+
# @see Groups::PitchClassSet
|
10
|
+
#
|
11
|
+
class Chord < Melody
|
12
|
+
|
13
|
+
# @param pitches [#to_a] the collection of pitches
|
14
|
+
# @note duplicate pitches will be removed. See #{Melody} if you want to maintain duplicates.
|
15
|
+
#
|
16
|
+
def initialize(pitches)
|
17
|
+
pitches = pitches.to_a.clone
|
18
|
+
pitches.uniq!
|
19
|
+
pitches.sort!
|
20
|
+
@pitches = pitches.freeze
|
21
|
+
end
|
22
|
+
|
23
|
+
# Generate a chord inversion (positive numbers move the lowest notes up an octave, negative moves the highest notes down)
|
24
|
+
def inversion(number)
|
25
|
+
number = number.to_i
|
26
|
+
pitch_set = Array.new(@pitches.uniq.sort)
|
27
|
+
if number > 0
|
28
|
+
number.times do |count|
|
29
|
+
index = count % pitch_set.length
|
30
|
+
pitch_set[index] += 12
|
31
|
+
end
|
32
|
+
else
|
33
|
+
number.abs.times do |count|
|
34
|
+
index = -(count + 1) % pitch_set.length # count from -1 downward to go backwards through the list starting at the end
|
35
|
+
pitch_set[index] -= 12
|
36
|
+
end
|
37
|
+
end
|
38
|
+
self.class.new pitch_set.sort
|
39
|
+
end
|
40
|
+
|
41
|
+
# Transpose the chord so that it's lowest pitch is the given pitch class.
|
42
|
+
def nearest(pitch_class)
|
43
|
+
self.transpose @pitches.first.pitch_class.distance_to(pitch_class)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Construct an ordered {MTK::Groups::Chord} with no duplicates.
|
49
|
+
# @see #MTK::Groups::Chord
|
50
|
+
# @see #MTK::Groups::Melody
|
51
|
+
def Chord(*anything)
|
52
|
+
MTK::Groups::Chord.new MTK::Groups.to_pitches(*anything)
|
53
|
+
end
|
54
|
+
module_function :Chord
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
module MTK
|
2
|
+
module Groups
|
3
|
+
|
4
|
+
# Given a method #elements, which returns an Array of elements in the collection,
|
5
|
+
# including this module will make the class Enumerable and provide various methods you'd expect from an Array.
|
6
|
+
module Collection
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
# A mutable array of elements in this collection
|
10
|
+
def to_a
|
11
|
+
Array.new(elements) # we construct a new array since some including classes make elements be immutable
|
12
|
+
end
|
13
|
+
|
14
|
+
# The number of elements in this collection
|
15
|
+
def size
|
16
|
+
elements.size
|
17
|
+
end
|
18
|
+
alias length size
|
19
|
+
|
20
|
+
def empty?
|
21
|
+
elements.nil? or elements.size == 0
|
22
|
+
end
|
23
|
+
|
24
|
+
# The each iterator for providing Enumerable functionality
|
25
|
+
def each &block
|
26
|
+
elements.each(&block)
|
27
|
+
end
|
28
|
+
|
29
|
+
# the original Enumerable#map implementation, which returns an Array
|
30
|
+
alias enumerable_map map
|
31
|
+
|
32
|
+
# the overriden #map implementation, which returns an object of the same type
|
33
|
+
def map &block
|
34
|
+
clone_with enumerable_map(&block)
|
35
|
+
end
|
36
|
+
|
37
|
+
# The first element
|
38
|
+
def first(n=nil)
|
39
|
+
n ? elements.first(n) : elements.first
|
40
|
+
end
|
41
|
+
|
42
|
+
# The last element
|
43
|
+
def last(n=nil)
|
44
|
+
n ? elements.last(n) : elements.last
|
45
|
+
end
|
46
|
+
|
47
|
+
# The element with the given index
|
48
|
+
def [](index)
|
49
|
+
elements[index]
|
50
|
+
end
|
51
|
+
|
52
|
+
def repeat(times=2)
|
53
|
+
full_repetitions, fractional_repetitions = times.floor, times%1 # split into int and fractional part
|
54
|
+
repeated = elements * full_repetitions
|
55
|
+
repeated += elements[0...elements.size*fractional_repetitions]
|
56
|
+
clone_with repeated
|
57
|
+
end
|
58
|
+
|
59
|
+
def permute
|
60
|
+
clone_with elements.shuffle
|
61
|
+
end
|
62
|
+
alias shuffle permute
|
63
|
+
|
64
|
+
def rotate(offset=1)
|
65
|
+
clone_with elements.rotate(offset)
|
66
|
+
end
|
67
|
+
|
68
|
+
def concat(other)
|
69
|
+
other_elements = (other.respond_to? :elements) ? other.elements : other
|
70
|
+
clone_with(elements + other_elements)
|
71
|
+
end
|
72
|
+
|
73
|
+
def reverse
|
74
|
+
clone_with elements.reverse
|
75
|
+
end
|
76
|
+
alias retrograde reverse
|
77
|
+
|
78
|
+
|
79
|
+
# Partition the collection into an Array of sub-collections.
|
80
|
+
#
|
81
|
+
# With a Numeric argument: partition the elements into collections of the given size (plus whatever's left over).
|
82
|
+
#
|
83
|
+
# With an Array argument: partition the elements into collections of the given sizes.
|
84
|
+
#
|
85
|
+
# Otherwise if a block is given: partition the elements into collections with the same block return value.
|
86
|
+
#
|
87
|
+
def partition(arg=nil, &block)
|
88
|
+
partitions = nil
|
89
|
+
case arg
|
90
|
+
when Numeric
|
91
|
+
partitions = self.each_slice(arg)
|
92
|
+
|
93
|
+
when Enumerable
|
94
|
+
partitions = []
|
95
|
+
items, sizes = self.to_enum, arg.to_enum
|
96
|
+
group = []
|
97
|
+
size = sizes.next
|
98
|
+
loop do
|
99
|
+
item = items.next
|
100
|
+
if group.size < size
|
101
|
+
group << item
|
102
|
+
else
|
103
|
+
partitions << group
|
104
|
+
group = []
|
105
|
+
size = sizes.next
|
106
|
+
group << item
|
107
|
+
end
|
108
|
+
end
|
109
|
+
partitions << group unless group.empty?
|
110
|
+
|
111
|
+
else
|
112
|
+
if block
|
113
|
+
group = Hash.new{|h,k| h[k] = [] }
|
114
|
+
if block.arity == 2
|
115
|
+
self.each_with_index{|item,index| group[block[item,index]] << item }
|
116
|
+
else
|
117
|
+
self.each{|item| group[block[item]] << item }
|
118
|
+
end
|
119
|
+
partitions = group.values
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
if partitions
|
124
|
+
partitions.map{|p| self.class.from_a(p) }
|
125
|
+
else
|
126
|
+
self
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def ==(other)
|
131
|
+
if other.respond_to? :elements
|
132
|
+
if other.respond_to? :options
|
133
|
+
elements == other.elements and @options == other.options
|
134
|
+
else
|
135
|
+
elements == other.elements
|
136
|
+
end
|
137
|
+
else
|
138
|
+
elements == other
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Create a copy of the collection.
|
143
|
+
# In order to use this method, the including class must implement .from_a()
|
144
|
+
def clone
|
145
|
+
clone_with to_a
|
146
|
+
end
|
147
|
+
|
148
|
+
#################################
|
149
|
+
private
|
150
|
+
|
151
|
+
# "clones" the object with the given elements, attempting to maintain any @options
|
152
|
+
# This is designed to work with 2 argument constructors: def initialize(elements, options=default)
|
153
|
+
def clone_with elements
|
154
|
+
from_a = self.class.method(:from_a)
|
155
|
+
if @options and from_a.arity == -2
|
156
|
+
from_a[elements, @options]
|
157
|
+
else
|
158
|
+
from_a[elements]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
|
164
|
+
|
165
|
+
######################################################################
|
166
|
+
# MTK::Groups
|
167
|
+
|
168
|
+
def to_pitch_classes(*anything)
|
169
|
+
anything = anything.first if anything.length == 1
|
170
|
+
if anything.respond_to? :to_pitch_classes
|
171
|
+
anything.to_pitch_classes
|
172
|
+
else
|
173
|
+
case anything
|
174
|
+
when ::Enumerable then anything.map{|item| MTK.PitchClass(item) }
|
175
|
+
else [MTK.PitchClass(anything)]
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
module_function :to_pitch_classes
|
180
|
+
|
181
|
+
|
182
|
+
def to_pitches(*anything)
|
183
|
+
anything = anything.first if anything.length == 1
|
184
|
+
if anything.respond_to? :to_pitches
|
185
|
+
anything.to_pitches
|
186
|
+
else
|
187
|
+
case anything
|
188
|
+
when ::Enumerable then anything.map{|item| MTK.Pitch(item) }
|
189
|
+
else [MTK.Pitch(anything)]
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
module_function :to_pitches
|
194
|
+
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module MTK
|
2
|
+
module Groups
|
3
|
+
|
4
|
+
# An ordered collection of {Pitch}es.
|
5
|
+
#
|
6
|
+
# The "horizontal" (sequential) pitch collection.
|
7
|
+
#
|
8
|
+
# Unlike the strict definition of melody, this class is fairly abstract and only models a succession of pitches.
|
9
|
+
# To create a true, playable melody one must combine an MTK::Melody and rhythms into a {Events::Timeline}.
|
10
|
+
#
|
11
|
+
# @see Chord
|
12
|
+
#
|
13
|
+
class Melody
|
14
|
+
include PitchCollection
|
15
|
+
|
16
|
+
attr_reader :pitches
|
17
|
+
|
18
|
+
# @param pitches [#to_a] the collection of pitches
|
19
|
+
# @see MTK#Melody
|
20
|
+
#
|
21
|
+
def initialize(pitches)
|
22
|
+
@pitches = pitches.to_a.clone.freeze
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.from_pitch_classes(pitch_classes, start=MTK::Lang::Pitches::C4, max_distance=12)
|
26
|
+
pitch = start
|
27
|
+
pitches = []
|
28
|
+
pitch_classes.each do |pitch_class|
|
29
|
+
pitch = pitch.nearest(pitch_class)
|
30
|
+
pitch -= 12 if pitch > start+max_distance # keep within max_distance of start (default is one octave)
|
31
|
+
pitch += 12 if pitch < start-max_distance
|
32
|
+
pitches << pitch
|
33
|
+
end
|
34
|
+
new pitches
|
35
|
+
end
|
36
|
+
|
37
|
+
# @see Helper::Collection
|
38
|
+
def elements
|
39
|
+
@pitches
|
40
|
+
end
|
41
|
+
|
42
|
+
# Convert to an Array of pitches.
|
43
|
+
# @note this returns a mutable copy the underlying @pitches attribute, which is otherwise unmutable
|
44
|
+
alias :to_pitches :to_a
|
45
|
+
|
46
|
+
def self.from_a enumerable
|
47
|
+
new enumerable
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_pitch_class_set(remove_duplicates=true)
|
51
|
+
PitchClassSet.new(remove_duplicates ? pitch_classes.uniq : pitch_classes)
|
52
|
+
end
|
53
|
+
|
54
|
+
def pitch_classes
|
55
|
+
@pitch_classes ||= @pitches.map{|p| p.pitch_class }
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param other [#pitches, Enumerable]
|
59
|
+
def == other
|
60
|
+
if other.respond_to? :pitches
|
61
|
+
@pitches == other.pitches
|
62
|
+
elsif other.is_a? Enumerable
|
63
|
+
@pitches == other.to_a
|
64
|
+
else
|
65
|
+
@pitches == other
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Compare for equality, ignoring order and duplicates
|
70
|
+
# @param other [#pitches, Array, #to_a]
|
71
|
+
def =~ other
|
72
|
+
@normalized_pitches ||= @pitches.uniq.sort
|
73
|
+
@normalized_pitches == case
|
74
|
+
when other.respond_to?(:pitches) then other.pitches.uniq.sort
|
75
|
+
when (other.is_a? Array and other.frozen?) then other
|
76
|
+
when other.respond_to?(:to_a) then other.to_a.uniq.sort
|
77
|
+
else other
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_s
|
82
|
+
'[' + @pitches.map{|pitch| pitch.to_s}.join(', ') + ']'
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Construct an ordered {MTK::Groups::Melody} that allows duplicates
|
89
|
+
# @see #MTK::Groups::Melody
|
90
|
+
# @see #MTK::Groups::Chord
|
91
|
+
def Melody(*anything)
|
92
|
+
MTK::Groups::Melody.new MTK::Groups.to_pitches(*anything)
|
93
|
+
end
|
94
|
+
module_function :Melody
|
95
|
+
|
96
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
module MTK
|
2
|
+
|
3
|
+
module Groups
|
4
|
+
|
5
|
+
# An ordered collection of {PitchClass}es.
|
6
|
+
#
|
7
|
+
# Unlike a mathematical Set, a PitchClassSet is ordered and may contain duplicates.
|
8
|
+
#
|
9
|
+
# @see MTK::Groups::Melody
|
10
|
+
# @see MTK::Groups::Chord
|
11
|
+
#
|
12
|
+
class PitchClassSet
|
13
|
+
include PitchCollection
|
14
|
+
|
15
|
+
attr_reader :pitch_classes
|
16
|
+
|
17
|
+
def self.random_row
|
18
|
+
new(MTK::Lang::PitchClasses::PITCH_CLASSES.shuffle)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.all
|
22
|
+
@all ||= new(MTK::Lang::PitchClasses::PITCH_CLASSES)
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param pitch_classes [#to_a] the collection of pitch classes
|
26
|
+
#
|
27
|
+
# @see MTK#PitchClassSet
|
28
|
+
#
|
29
|
+
def initialize(pitch_classes)
|
30
|
+
@pitch_classes = pitch_classes.to_a.clone.freeze
|
31
|
+
end
|
32
|
+
|
33
|
+
# @see Helper::Collection
|
34
|
+
def elements
|
35
|
+
@pitch_classes
|
36
|
+
end
|
37
|
+
|
38
|
+
# Convert to an Array of pitch_classes.
|
39
|
+
# @note this returns a mutable copy the underlying @pitch_classes attribute, which is otherwise unmutable
|
40
|
+
alias :to_pitch_classes :to_a
|
41
|
+
|
42
|
+
def self.from_a enumerable
|
43
|
+
new enumerable
|
44
|
+
end
|
45
|
+
|
46
|
+
def normal_order
|
47
|
+
ordering = Array.new(@pitch_classes.uniq.sort)
|
48
|
+
min_span, start_index_for_normal_order = nil, nil
|
49
|
+
|
50
|
+
# check every rotation for the minimal span:
|
51
|
+
size.times do |index|
|
52
|
+
span = self.class.span_between ordering.first, ordering.last
|
53
|
+
|
54
|
+
if min_span.nil? or span < min_span
|
55
|
+
# best so far
|
56
|
+
min_span = span
|
57
|
+
start_index_for_normal_order = index
|
58
|
+
|
59
|
+
elsif span == min_span
|
60
|
+
# handle ties, minimize distance between first and second-to-last note, then first and third-to-last, etc
|
61
|
+
span1, span2 = nil, nil
|
62
|
+
tie_breaker = 1
|
63
|
+
while span1 == span2 and tie_breaker < size
|
64
|
+
span1 = self.class.span_between( ordering[0], ordering[-1 - tie_breaker] )
|
65
|
+
span2 = self.class.span_between( ordering[start_index_for_normal_order], ordering[start_index_for_normal_order - tie_breaker] )
|
66
|
+
tie_breaker -= 1
|
67
|
+
end
|
68
|
+
if span1 != span2
|
69
|
+
# tie cannot be broken, pick the one starting with the lowest pitch class
|
70
|
+
if ordering[0].to_i < ordering[start_index_for_normal_order].to_i
|
71
|
+
start_index_for_normal_order = index
|
72
|
+
end
|
73
|
+
elsif span1 < span2
|
74
|
+
start_index_for_normal_order = index
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
ordering << ordering.shift # rotate
|
79
|
+
end
|
80
|
+
|
81
|
+
# we've rotated all the way around, so we now need to rotate back to the start index we just found:
|
82
|
+
start_index_for_normal_order.times{ ordering << ordering.shift }
|
83
|
+
|
84
|
+
ordering
|
85
|
+
end
|
86
|
+
|
87
|
+
def normal_form
|
88
|
+
norder = normal_order
|
89
|
+
first_pc_val = norder.first.to_i
|
90
|
+
norder.map{|pitch_class| (pitch_class.to_i - first_pc_val) % 12 }
|
91
|
+
end
|
92
|
+
|
93
|
+
# the collection of elements present in both sets
|
94
|
+
def intersection(other)
|
95
|
+
self.class.from_a(to_a & other.to_a)
|
96
|
+
end
|
97
|
+
|
98
|
+
# the collection of all elements present in either set
|
99
|
+
def union(other)
|
100
|
+
self.class.from_a(to_a | other.to_a)
|
101
|
+
end
|
102
|
+
|
103
|
+
# the collection of elements from this set with any elements from the other set removed
|
104
|
+
def difference(other)
|
105
|
+
self.class.from_a(to_a - other.to_a)
|
106
|
+
end
|
107
|
+
|
108
|
+
# the collection of elements that are members of exactly one of the sets
|
109
|
+
def symmetric_difference(other)
|
110
|
+
union(other).difference( intersection(other) )
|
111
|
+
end
|
112
|
+
|
113
|
+
# the collection of elements that are not members of this set
|
114
|
+
def complement
|
115
|
+
self.class.all.difference(self)
|
116
|
+
end
|
117
|
+
|
118
|
+
# @param other [#pitch_classes, #to_a, Array]
|
119
|
+
def == other
|
120
|
+
if other.respond_to? :pitch_classes
|
121
|
+
@pitch_classes == other.pitch_classes
|
122
|
+
elsif other.respond_to? :to_a
|
123
|
+
@pitch_classes == other.to_a
|
124
|
+
else
|
125
|
+
@pitch_classes == other
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Compare for equality, ignoring order and duplicates
|
130
|
+
# @param other [#pitch_classes, Array, #to_a]
|
131
|
+
def =~ other
|
132
|
+
@normalized_pitch_classes ||= @pitch_classes.uniq.sort
|
133
|
+
@normalized_pitch_classes == case
|
134
|
+
when other.respond_to?(:pitch_classes) then other.pitch_classes.uniq.sort
|
135
|
+
when (other.is_a? Array and other.frozen?) then other
|
136
|
+
when other.respond_to?(:to_a) then other.to_a.uniq.sort
|
137
|
+
else other
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def to_s
|
142
|
+
@pitch_classes.join(' ')
|
143
|
+
end
|
144
|
+
|
145
|
+
def inspect
|
146
|
+
@pitch_classes.inspect
|
147
|
+
end
|
148
|
+
|
149
|
+
def self.span_between(pc1, pc2)
|
150
|
+
(pc2.to_i - pc1.to_i) % 12
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Construct a {Groups::PitchClassSet}
|
157
|
+
# @see Groups::PitchClassSet#initialize
|
158
|
+
def PitchClassSet(*anything)
|
159
|
+
MTK::Groups::PitchClassSet.new MTK::Groups.to_pitch_classes(*anything)
|
160
|
+
end
|
161
|
+
module_function :PitchClassSet
|
162
|
+
|
163
|
+
end
|