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.
Files changed (110) hide show
  1. data/.yardopts +10 -0
  2. data/DEVELOPMENT_NOTES.md +115 -0
  3. data/INTRO.md +129 -0
  4. data/LICENSE.txt +27 -0
  5. data/README.md +50 -0
  6. data/Rakefile +102 -0
  7. data/bin/jmtk +250 -0
  8. data/bin/mtk +250 -0
  9. data/examples/crescendo.rb +20 -0
  10. data/examples/drum_pattern.rb +23 -0
  11. data/examples/dynamic_pattern.rb +36 -0
  12. data/examples/gets_and_play.rb +27 -0
  13. data/examples/notation.rb +22 -0
  14. data/examples/play_midi.rb +17 -0
  15. data/examples/print_midi.rb +13 -0
  16. data/examples/random_tone_row.rb +18 -0
  17. data/examples/syntax_to_midi.rb +28 -0
  18. data/examples/test_output.rb +7 -0
  19. data/examples/tone_row_melody.rb +23 -0
  20. data/lib/mtk.rb +76 -0
  21. data/lib/mtk/core/duration.rb +213 -0
  22. data/lib/mtk/core/intensity.rb +158 -0
  23. data/lib/mtk/core/interval.rb +157 -0
  24. data/lib/mtk/core/pitch.rb +154 -0
  25. data/lib/mtk/core/pitch_class.rb +194 -0
  26. data/lib/mtk/events/event.rb +119 -0
  27. data/lib/mtk/events/note.rb +112 -0
  28. data/lib/mtk/events/parameter.rb +54 -0
  29. data/lib/mtk/events/timeline.rb +232 -0
  30. data/lib/mtk/groups/chord.rb +56 -0
  31. data/lib/mtk/groups/collection.rb +196 -0
  32. data/lib/mtk/groups/melody.rb +96 -0
  33. data/lib/mtk/groups/pitch_class_set.rb +163 -0
  34. data/lib/mtk/groups/pitch_collection.rb +23 -0
  35. data/lib/mtk/io/dls_synth_device.rb +146 -0
  36. data/lib/mtk/io/dls_synth_output.rb +62 -0
  37. data/lib/mtk/io/jsound_input.rb +87 -0
  38. data/lib/mtk/io/jsound_output.rb +82 -0
  39. data/lib/mtk/io/midi_file.rb +209 -0
  40. data/lib/mtk/io/midi_input.rb +97 -0
  41. data/lib/mtk/io/midi_output.rb +195 -0
  42. data/lib/mtk/io/notation.rb +162 -0
  43. data/lib/mtk/io/unimidi_input.rb +117 -0
  44. data/lib/mtk/io/unimidi_output.rb +140 -0
  45. data/lib/mtk/lang/durations.rb +57 -0
  46. data/lib/mtk/lang/intensities.rb +61 -0
  47. data/lib/mtk/lang/intervals.rb +73 -0
  48. data/lib/mtk/lang/mtk_grammar.citrus +237 -0
  49. data/lib/mtk/lang/parser.rb +29 -0
  50. data/lib/mtk/lang/pitch_classes.rb +29 -0
  51. data/lib/mtk/lang/pitches.rb +52 -0
  52. data/lib/mtk/lang/pseudo_constants.rb +26 -0
  53. data/lib/mtk/lang/variable.rb +32 -0
  54. data/lib/mtk/numeric_extensions.rb +66 -0
  55. data/lib/mtk/patterns/chain.rb +49 -0
  56. data/lib/mtk/patterns/choice.rb +43 -0
  57. data/lib/mtk/patterns/cycle.rb +18 -0
  58. data/lib/mtk/patterns/for_each.rb +71 -0
  59. data/lib/mtk/patterns/function.rb +39 -0
  60. data/lib/mtk/patterns/lines.rb +54 -0
  61. data/lib/mtk/patterns/palindrome.rb +45 -0
  62. data/lib/mtk/patterns/pattern.rb +171 -0
  63. data/lib/mtk/patterns/sequence.rb +20 -0
  64. data/lib/mtk/sequencers/event_builder.rb +132 -0
  65. data/lib/mtk/sequencers/legato_sequencer.rb +24 -0
  66. data/lib/mtk/sequencers/rhythmic_sequencer.rb +28 -0
  67. data/lib/mtk/sequencers/sequencer.rb +111 -0
  68. data/lib/mtk/sequencers/step_sequencer.rb +26 -0
  69. data/spec/mtk/core/duration_spec.rb +372 -0
  70. data/spec/mtk/core/intensity_spec.rb +289 -0
  71. data/spec/mtk/core/interval_spec.rb +265 -0
  72. data/spec/mtk/core/pitch_class_spec.rb +343 -0
  73. data/spec/mtk/core/pitch_spec.rb +297 -0
  74. data/spec/mtk/events/event_spec.rb +234 -0
  75. data/spec/mtk/events/note_spec.rb +174 -0
  76. data/spec/mtk/events/parameter_spec.rb +220 -0
  77. data/spec/mtk/events/timeline_spec.rb +430 -0
  78. data/spec/mtk/groups/chord_spec.rb +85 -0
  79. data/spec/mtk/groups/collection_spec.rb +374 -0
  80. data/spec/mtk/groups/melody_spec.rb +225 -0
  81. data/spec/mtk/groups/pitch_class_set_spec.rb +340 -0
  82. data/spec/mtk/io/midi_file_spec.rb +243 -0
  83. data/spec/mtk/io/midi_output_spec.rb +102 -0
  84. data/spec/mtk/lang/durations_spec.rb +89 -0
  85. data/spec/mtk/lang/intensities_spec.rb +101 -0
  86. data/spec/mtk/lang/intervals_spec.rb +143 -0
  87. data/spec/mtk/lang/parser_spec.rb +603 -0
  88. data/spec/mtk/lang/pitch_classes_spec.rb +62 -0
  89. data/spec/mtk/lang/pitches_spec.rb +56 -0
  90. data/spec/mtk/lang/pseudo_constants_spec.rb +20 -0
  91. data/spec/mtk/lang/variable_spec.rb +52 -0
  92. data/spec/mtk/numeric_extensions_spec.rb +83 -0
  93. data/spec/mtk/patterns/chain_spec.rb +110 -0
  94. data/spec/mtk/patterns/choice_spec.rb +97 -0
  95. data/spec/mtk/patterns/cycle_spec.rb +123 -0
  96. data/spec/mtk/patterns/for_each_spec.rb +136 -0
  97. data/spec/mtk/patterns/function_spec.rb +120 -0
  98. data/spec/mtk/patterns/lines_spec.rb +77 -0
  99. data/spec/mtk/patterns/palindrome_spec.rb +108 -0
  100. data/spec/mtk/patterns/pattern_spec.rb +132 -0
  101. data/spec/mtk/patterns/sequence_spec.rb +203 -0
  102. data/spec/mtk/sequencers/event_builder_spec.rb +245 -0
  103. data/spec/mtk/sequencers/legato_sequencer_spec.rb +45 -0
  104. data/spec/mtk/sequencers/rhythmic_sequencer_spec.rb +84 -0
  105. data/spec/mtk/sequencers/sequencer_spec.rb +215 -0
  106. data/spec/mtk/sequencers/step_sequencer_spec.rb +93 -0
  107. data/spec/spec_coverage.rb +2 -0
  108. data/spec/spec_helper.rb +12 -0
  109. data/spec/test.mid +0 -0
  110. 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