mtk 0.0.3.2 → 0.0.3.3

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 (102) hide show
  1. data/.yardopts +2 -2
  2. data/DEVELOPMENT_NOTES.md +20 -0
  3. data/README.md +9 -3
  4. data/Rakefile +47 -13
  5. data/bin/mtk +55 -20
  6. data/examples/crescendo.rb +4 -4
  7. data/examples/{drum_pattern1.rb → drum_pattern.rb} +8 -8
  8. data/examples/dynamic_pattern.rb +5 -5
  9. data/examples/gets_and_play.rb +3 -2
  10. data/examples/notation.rb +3 -3
  11. data/examples/play_midi.rb +4 -4
  12. data/examples/print_midi.rb +2 -2
  13. data/examples/random_tone_row.rb +3 -3
  14. data/examples/syntax_to_midi.rb +2 -2
  15. data/examples/test_output.rb +4 -5
  16. data/examples/tone_row_melody.rb +7 -5
  17. data/lib/mtk/core/duration.rb +213 -0
  18. data/lib/mtk/core/intensity.rb +158 -0
  19. data/lib/mtk/core/interval.rb +157 -0
  20. data/lib/mtk/core/pitch.rb +154 -0
  21. data/lib/mtk/core/pitch_class.rb +194 -0
  22. data/lib/mtk/events/event.rb +4 -4
  23. data/lib/mtk/events/note.rb +12 -12
  24. data/lib/mtk/events/timeline.rb +232 -0
  25. data/lib/mtk/groups/chord.rb +56 -0
  26. data/lib/mtk/{helpers → groups}/collection.rb +33 -1
  27. data/lib/mtk/groups/melody.rb +96 -0
  28. data/lib/mtk/groups/pitch_class_set.rb +163 -0
  29. data/lib/mtk/{helpers → groups}/pitch_collection.rb +1 -1
  30. data/lib/mtk/{midi → io}/dls_synth_device.rb +3 -1
  31. data/lib/mtk/{midi → io}/dls_synth_output.rb +10 -10
  32. data/lib/mtk/{midi → io}/jsound_input.rb +2 -2
  33. data/lib/mtk/{midi → io}/jsound_output.rb +9 -9
  34. data/lib/mtk/{midi/file.rb → io/midi_file.rb} +13 -13
  35. data/lib/mtk/{midi/input.rb → io/midi_input.rb} +4 -4
  36. data/lib/mtk/{midi/output.rb → io/midi_output.rb} +8 -8
  37. data/lib/mtk/{helpers/lilypond.rb → io/notation.rb} +5 -5
  38. data/lib/mtk/{midi → io}/unimidi_input.rb +2 -2
  39. data/lib/mtk/{midi → io}/unimidi_output.rb +14 -9
  40. data/lib/mtk/{constants → lang}/durations.rb +11 -11
  41. data/lib/mtk/{constants → lang}/intensities.rb +11 -11
  42. data/lib/mtk/{constants → lang}/intervals.rb +17 -17
  43. data/lib/mtk/lang/mtk_grammar.citrus +9 -9
  44. data/lib/mtk/{constants → lang}/pitch_classes.rb +5 -5
  45. data/lib/mtk/{constants → lang}/pitches.rb +7 -7
  46. data/lib/mtk/{helpers → lang}/pseudo_constants.rb +1 -1
  47. data/lib/mtk/{variable.rb → lang/variable.rb} +1 -1
  48. data/lib/mtk/numeric_extensions.rb +40 -47
  49. data/lib/mtk/patterns/for_each.rb +1 -1
  50. data/lib/mtk/patterns/pattern.rb +3 -3
  51. data/lib/mtk/sequencers/event_builder.rb +16 -15
  52. data/lib/mtk/sequencers/legato_sequencer.rb +1 -1
  53. data/lib/mtk/sequencers/rhythmic_sequencer.rb +1 -1
  54. data/lib/mtk/sequencers/sequencer.rb +8 -8
  55. data/lib/mtk/sequencers/step_sequencer.rb +2 -2
  56. data/lib/mtk.rb +33 -39
  57. data/spec/mtk/{duration_spec.rb → core/duration_spec.rb} +3 -3
  58. data/spec/mtk/{intensity_spec.rb → core/intensity_spec.rb} +3 -3
  59. data/spec/mtk/{interval_spec.rb → core/interval_spec.rb} +1 -1
  60. data/spec/mtk/{pitch_class_spec.rb → core/pitch_class_spec.rb} +1 -1
  61. data/spec/mtk/{pitch_spec.rb → core/pitch_spec.rb} +8 -8
  62. data/spec/mtk/events/event_spec.rb +4 -4
  63. data/spec/mtk/events/note_spec.rb +8 -8
  64. data/spec/mtk/{timeline_spec.rb → events/timeline_spec.rb} +47 -47
  65. data/spec/mtk/{chord_spec.rb → groups/chord_spec.rb} +18 -16
  66. data/spec/mtk/{helpers → groups}/collection_spec.rb +3 -3
  67. data/spec/mtk/{melody_spec.rb → groups/melody_spec.rb} +36 -34
  68. data/spec/mtk/{pitch_class_set_spec.rb → groups/pitch_class_set_spec.rb} +57 -55
  69. data/spec/mtk/{midi/file_spec.rb → io/midi_file_spec.rb} +17 -17
  70. data/spec/mtk/{midi/output_spec.rb → io/midi_output_spec.rb} +6 -6
  71. data/spec/mtk/{constants → lang}/durations_spec.rb +1 -1
  72. data/spec/mtk/{constants → lang}/intensities_spec.rb +1 -1
  73. data/spec/mtk/{constants → lang}/intervals_spec.rb +1 -1
  74. data/spec/mtk/lang/parser_spec.rb +12 -6
  75. data/spec/mtk/{constants → lang}/pitch_classes_spec.rb +1 -1
  76. data/spec/mtk/{constants → lang}/pitches_spec.rb +1 -1
  77. data/spec/mtk/{helpers → lang}/pseudo_constants_spec.rb +2 -2
  78. data/spec/mtk/{variable_spec.rb → lang/variable_spec.rb} +4 -4
  79. data/spec/mtk/numeric_extensions_spec.rb +35 -55
  80. data/spec/mtk/patterns/for_each_spec.rb +1 -1
  81. data/spec/mtk/patterns/sequence_spec.rb +1 -1
  82. data/spec/mtk/sequencers/legato_sequencer_spec.rb +2 -2
  83. data/spec/mtk/sequencers/rhythmic_sequencer_spec.rb +4 -4
  84. data/spec/mtk/sequencers/step_sequencer_spec.rb +5 -5
  85. data/spec/spec_helper.rb +7 -6
  86. metadata +75 -61
  87. data/ext/mkrf_conf.rb +0 -25
  88. data/lib/mtk/chord.rb +0 -55
  89. data/lib/mtk/duration.rb +0 -211
  90. data/lib/mtk/helpers/convert.rb +0 -36
  91. data/lib/mtk/helpers/output_selector.rb +0 -67
  92. data/lib/mtk/intensity.rb +0 -156
  93. data/lib/mtk/interval.rb +0 -155
  94. data/lib/mtk/melody.rb +0 -94
  95. data/lib/mtk/pitch.rb +0 -152
  96. data/lib/mtk/pitch_class.rb +0 -192
  97. data/lib/mtk/pitch_class_set.rb +0 -161
  98. data/lib/mtk/timeline.rb +0 -230
  99. data/spec/mtk/midi/jsound_input_spec.rb +0 -11
  100. data/spec/mtk/midi/jsound_output_spec.rb +0 -11
  101. data/spec/mtk/midi/unimidi_input_spec.rb +0 -11
  102. data/spec/mtk/midi/unimidi_output_spec.rb +0 -11
@@ -0,0 +1,194 @@
1
+ module MTK
2
+ module Core
3
+
4
+ # A set of all pitches that are an integer number of octaves apart.
5
+ # A {Pitch} has the same PitchClass as the pitches one or more octaves away.
6
+ # @see https://en.wikipedia.org/wiki/Pitch_class
7
+ #
8
+ class PitchClass
9
+
10
+ # The normalized names of the 12 pitch classes in the chromatic scale.
11
+ # The index of each {#name} is the pitch class's numeric {#value}.
12
+ NAMES = %w( C Db D Eb E F Gb G Ab A Bb B ).freeze
13
+
14
+ # All enharmonic names of the 12 pitch classes, including sharps, flats, double-sharps, and double-flats,
15
+ # organized such that each index contains the allowed names of the pitch class with a {#value} equal to that index.
16
+ # @see VALID_NAMES
17
+ VALID_NAMES_BY_VALUE =
18
+ [ # (valid names ), # value # normalized name
19
+ %w( B# C Dbb ), # 0 # C
20
+ %w( B## C# Db ), # 1 # Db
21
+ %w( C## D Ebb ), # 2 # D
22
+ %w( D# Eb Fbb ), # 3 # Eb
23
+ %w( D## E Fb ), # 4 # E
24
+ %w( E# F Gbb ), # 5 # F
25
+ %w( E## F# Gb ), # 6 # Gb
26
+ %w( F## G Abb ), # 7 # G
27
+ %w( G# Ab ), # 8 # Ab
28
+ %w( G## A Bbb ), # 9 # A
29
+ %w( A# Bb Cbb ), # 10 # Bb
30
+ %w( A## B Cb ) # 11 # B
31
+ ].freeze
32
+
33
+ # All valid enharmonic pitch class names in a flat list.
34
+ # @see VALID_NAMES_BY_VALUE
35
+ VALID_NAMES = VALID_NAMES_BY_VALUE.flatten.freeze
36
+
37
+ # A mapping from valid names to the value of the pitch class with that name
38
+ VALUES_BY_NAME = Hash[ # a map from a list of name,value pairs
39
+ VALID_NAMES_BY_VALUE.map.with_index do |valid_names,value|
40
+ valid_names.map{|name| [name,value] }
41
+ end.flatten(1)
42
+ ].freeze
43
+
44
+
45
+ # The name of this pitch class.
46
+ # One of the {NAMES} defined by this class.
47
+ attr_reader :name
48
+
49
+ # The value of this pitch class.
50
+ # An integer from 0..11 that indexes this pitch class in {PITCH_CLASSES} and the {#name} in {NAMES}.
51
+ attr_reader :value
52
+
53
+
54
+ private ######
55
+ # Even though new is a private_class_method, YARD gets confused so we temporarily go private
56
+
57
+ def initialize(name, value)
58
+ @name, @value = name, value
59
+ end
60
+ private_class_method :new
61
+
62
+ @flyweight = {}
63
+
64
+ public ######
65
+
66
+
67
+ # Lookup a PitchClass by name or value.
68
+ # @param name_or_value [String,Symbol,Numeric] one of {VALID_NAMES} or 0..12
69
+ # @return the PitchClass representing the argument
70
+ # @raise ArgumentError for arguments that cannot be converted to a PitchClass
71
+ def self.[] name_or_value
72
+ @flyweight[name_or_value] ||= case name_or_value
73
+ when String,Symbol then from_name(name_or_value)
74
+ when Numeric then from_value(name_or_value.round)
75
+ else raise ArgumentError.new("PitchClass.[] doesn't understand #{name_or_value.class}")
76
+ end
77
+ end
78
+
79
+ # Lookup a PitchClass by name.
80
+ # @param name [String,#to_s] one of {VALID_NAMES} (case-insensitive)
81
+ def self.from_name(name)
82
+ @flyweight[name] ||= (
83
+ valid_name = name.to_s.capitalize
84
+ value = VALUES_BY_NAME[valid_name] or raise ArgumentError.new("Invalid PitchClass name: #{name}")
85
+ new(valid_name,value)
86
+ )
87
+ end
88
+
89
+ class << self
90
+ alias from_s from_name
91
+ end
92
+
93
+ # All 12 pitch classes in the chromatic scale.
94
+ # The index of each pitch class is the pitch class's numeric {#value}.
95
+ PITCH_CLASSES = NAMES.map{|name| from_name name }.freeze
96
+
97
+ # return the pitch class with the given integer value mod 12
98
+ # @param value [Integer,#to_i]
99
+ def self.from_value(value)
100
+ PITCH_CLASSES[value.to_i % 12]
101
+ end
102
+
103
+ class << self
104
+ alias from_i from_value
105
+ end
106
+
107
+ # return the pitch class with the given float rounded to the nearest integer, mod 12
108
+ # @param value [Float,#to_f]
109
+ def self.from_f(value)
110
+ from_i value.to_f.round
111
+ end
112
+
113
+ # Compare 2 pitch classes for equal values.
114
+ # @param other [PitchClass]
115
+ # @return true if this pitch class's value is equal to the other pitch class's value
116
+ def == other
117
+ other.is_a? PitchClass and other.value == @value
118
+ end
119
+
120
+ # Compare a pitch class with another pitch class or integer value
121
+ # @param other [PitchClass,#to_i]
122
+ # @return -1, 0, or +1 depending on whether this pitch class's value is less than, equal to, or greater than the other object's integer value
123
+ # @see http://ruby-doc.org/core-1.9.3/Comparable.html
124
+ def <=> other
125
+ @value <=> other.to_i
126
+ end
127
+
128
+ # This pitch class's normalized {#name}.
129
+ # @see NAMES
130
+ def to_s
131
+ @name.to_s
132
+ end
133
+
134
+ # This pitch class's integer {#value}
135
+ def to_i
136
+ @value.to_i
137
+ end
138
+
139
+ # This pitch class's {#value} as a floating point number
140
+ def to_f
141
+ @value.to_f
142
+ end
143
+
144
+ # Transpose this pitch class by adding it's value to the value given (mod 12)
145
+ # @param interval [PitchClass,Float,#to_f]
146
+ def + interval
147
+ new_value = (value + interval.to_f).round
148
+ self.class.from_value new_value
149
+ end
150
+ alias transpose +
151
+
152
+ # Transpose this pitch class by subtracing the given value from this value (mod 12)
153
+ # @param interval [PitchClass,Float,#to_f]
154
+ def - interval
155
+ new_value = (value - interval.to_f).round
156
+ self.class.from_value new_value
157
+ end
158
+
159
+ # Inverts (mirrors) the pitch class around the given center
160
+ # @param center [PitchClass,Pitch,Float,#to_f] the value to "mirror" this pitch class around
161
+ def invert(center)
162
+ delta = (2*(center.to_f - value)).round
163
+ self + delta
164
+ end
165
+
166
+ # the smallest interval in semitones that needs to be added to this PitchClass to reach the given PitchClass
167
+ # @param pitch_class [PitchClass,#value]
168
+ def distance_to(pitch_class)
169
+ delta = (pitch_class.value - value) % 12
170
+ if delta > 6
171
+ delta -= 12
172
+ elsif delta == 6 and to_i >= 6
173
+ # this is a special edge case to prevent endlessly ascending pitch sequences when alternating between two pitch classes a tritone apart
174
+ delta = -6
175
+ end
176
+ delta
177
+ end
178
+
179
+ end
180
+ end
181
+
182
+ # Construct a {PitchClass} from any supported type
183
+ # @param anything [PitchClass,String,Symbol,Numeric]
184
+ def PitchClass(anything)
185
+ case anything
186
+ when Numeric then MTK::Core::PitchClass.from_f(anything)
187
+ when String, Symbol then MTK::Core::PitchClass.from_s(anything)
188
+ when MTK::Core::PitchClass then anything
189
+ else raise ArgumentError.new("PitchClass doesn't understand #{anything.class}")
190
+ end
191
+ end
192
+ module_function :PitchClass
193
+
194
+ end
@@ -29,7 +29,7 @@ module MTK
29
29
 
30
30
  def duration= duration
31
31
  @duration = duration
32
- @duration = ::MTK::Duration[@duration || 0] unless @duration.is_a? ::MTK::Duration
32
+ @duration = ::MTK::Core::Duration[@duration || 0] unless @duration.is_a? ::MTK::Core::Duration
33
33
  @duration
34
34
  end
35
35
 
@@ -42,15 +42,15 @@ module MTK
42
42
  @value = options[:value]
43
43
  @number = options[:number]
44
44
  @duration = options.fetch(:duration, 0)
45
- @duration = ::MTK::Duration[@duration] unless @duration.is_a? ::MTK::Duration
45
+ @duration = ::MTK::Core::Duration[@duration] unless @duration.is_a? ::MTK::Core::Duration
46
46
  @channel = options[:channel]
47
47
  end
48
48
 
49
- def self.from_hash(hash)
49
+ def self.from_h(hash)
50
50
  new(hash[:type], hash)
51
51
  end
52
52
 
53
- def to_hash
53
+ def to_h
54
54
  hash = {:type => @type}
55
55
  hash[:value] = @value unless @value.nil?
56
56
  hash[:duration] = @duration unless @duration.nil?
@@ -5,8 +5,8 @@ module MTK
5
5
  # A musical {Event} defined by a {Pitch}, intensity, and duration
6
6
  class Note < Event
7
7
 
8
- DEFAULT_DURATION = MTK::Duration[1]
9
- DEFAULT_INTENSITY = MTK::Intensity[0.75]
8
+ DEFAULT_DURATION = MTK::Core::Duration[1]
9
+ DEFAULT_INTENSITY = MTK::Core::Intensity[0.75]
10
10
 
11
11
  # Frequency of the note as a {Pitch}.
12
12
  alias :pitch :number
@@ -24,16 +24,16 @@ module MTK
24
24
  super :note, number:pitch, duration:duration, value:intensity, channel:channel
25
25
  end
26
26
 
27
- def self.from_hash(hash)
27
+ def self.from_h(hash)
28
28
  new(hash[:pitch]||hash[:number], hash[:duration], hash[:intensity]||hash[:value], hash[:channel])
29
29
  end
30
30
 
31
- def to_hash
31
+ def to_h
32
32
  super.merge({ pitch: @number, intensity: @value })
33
33
  end
34
34
 
35
35
  def self.from_midi(pitch, velocity, duration_in_beats, channel=0)
36
- new( MTK::Constants::Pitches::PITCHES[pitch.to_i], MTK::Duration[duration_in_beats], MTK::Intensity[velocity/127.0], channel )
36
+ new( MTK::Lang::Pitches::PITCHES[pitch.to_i], MTK::Core::Duration[duration_in_beats], MTK::Core::Intensity[velocity/127.0], channel )
37
37
  end
38
38
 
39
39
  def midi_pitch
@@ -74,7 +74,7 @@ module MTK
74
74
  case anything
75
75
  when MTK::Events::Note then anything
76
76
 
77
- when MTK::Pitch then MTK::Events::Note.new(anything)
77
+ when MTK::Core::Pitch then MTK::Events::Note.new(anything)
78
78
 
79
79
  when Array
80
80
  pitch = nil
@@ -84,18 +84,18 @@ module MTK
84
84
  unknowns = []
85
85
  anything.each do |item|
86
86
  case item
87
- when MTK::Pitch then pitch = item
88
- when MTK::Duration then duration = item
89
- when MTK::Intensity then intensity = item
87
+ when MTK::Core::Pitch then pitch = item
88
+ when MTK::Core::Duration then duration = item
89
+ when MTK::Core::Intensity then intensity = item
90
90
  else unknowns << item
91
91
  end
92
92
  end
93
93
 
94
- pitch = MTK::Pitch(unknowns.shift) if pitch.nil? and not unknowns.empty?
94
+ pitch = MTK.Pitch(unknowns.shift) if pitch.nil? and not unknowns.empty?
95
95
  raise "MTK::Note() couldn't find a pitch in arguments: #{anything.inspect}" if pitch.nil?
96
96
 
97
- duration = MTK::Duration(unknowns.shift) if duration.nil? and not unknowns.empty?
98
- intensity = MTK::Intensity(unknowns.shift) if intensity.nil? and not unknowns.empty?
97
+ duration = MTK.Duration(unknowns.shift) if duration.nil? and not unknowns.empty?
98
+ intensity = MTK.Intensity(unknowns.shift) if intensity.nil? and not unknowns.empty?
99
99
  channel = unknowns.shift.to_i if channel.nil? and not unknowns.empty?
100
100
 
101
101
  duration ||= MTK::Events::Note::DEFAULT_DURATION
@@ -0,0 +1,232 @@
1
+ module MTK
2
+ module Events
3
+
4
+ # A collection of timed events. The core data structure used to interface with input and output.
5
+ #
6
+ # Maps sorted floating point times to lists of events.
7
+ #
8
+ # Enumerable as [time,event_list] pairs.
9
+ #
10
+ class Timeline
11
+ include Enumerable
12
+
13
+ def initialize()
14
+ @timeline = {}
15
+ end
16
+
17
+ class << self
18
+ def from_a(enumerable)
19
+ new.merge enumerable
20
+ end
21
+ alias from_h from_a
22
+ end
23
+
24
+ def merge enumerable
25
+ enumerable.each do |time,events|
26
+ add(time,events)
27
+ end
28
+ self
29
+ end
30
+
31
+ def clear
32
+ @timeline.clear
33
+ self
34
+ end
35
+
36
+ def to_h
37
+ @timeline
38
+ end
39
+
40
+ def == other
41
+ other = other.to_h unless other.is_a? Hash
42
+ @timeline == other
43
+ end
44
+
45
+ def [](time)
46
+ @timeline[time.to_f]
47
+ end
48
+
49
+ def []=(time, events)
50
+ time = time.to_f unless time.is_a? Numeric
51
+ case events
52
+ when nil?
53
+ @timeline.delete time.to_f
54
+ when Array
55
+ @timeline[time.to_f] = events
56
+ else
57
+ @timeline[time.to_f] = [events]
58
+ end
59
+ end
60
+
61
+ def add(time, event)
62
+ events = @timeline[time.to_f]
63
+ if events
64
+ if event.is_a? Array
65
+ events.concat event
66
+ else
67
+ events << event
68
+ end
69
+ else
70
+ self[time] = event
71
+ end
72
+ end
73
+
74
+ def delete(time)
75
+ @timeline.delete(time.to_f)
76
+ end
77
+
78
+ def has_time? time
79
+ @timeline.has_key? time.to_f
80
+ end
81
+
82
+ def times
83
+ @timeline.keys.sort
84
+ end
85
+
86
+ def length
87
+ last_time = times.last
88
+ events = @timeline[last_time]
89
+ last_time + events.map{|event| event.duration }.max
90
+ end
91
+
92
+ def empty?
93
+ @timeline.empty?
94
+ end
95
+
96
+ def events
97
+ times.map{|t| @timeline[t] }.flatten
98
+ end
99
+
100
+ def each
101
+ # this is similar to @timeline.each, but by iterating over #times, we yield the events in chronological order
102
+ times.each do |time|
103
+ yield time, @timeline[time]
104
+ end
105
+ end
106
+
107
+ # the original Enumerable#map implementation, which returns an Array
108
+ alias enumerable_map map
109
+
110
+ # Constructs a new Timeline by mapping each [time,event_list] pair
111
+ # @see #map!
112
+ def map &block
113
+ self.class.from_a enumerable_map(&block)
114
+ end
115
+
116
+ # Perform #map in place
117
+ # @see #map
118
+ def map! &block
119
+ mapped = enumerable_map(&block)
120
+ clear
121
+ merge mapped
122
+ end
123
+
124
+ # Map every individual event, without regard for the time at which is occurs
125
+ def map_events
126
+ mapped_timeline = Timeline.new
127
+ self.each do |time,events|
128
+ mapped_timeline[time] = events.map{|event| yield event }
129
+ end
130
+ mapped_timeline
131
+ end
132
+
133
+ # Map every individual event in place, without regard for the time at which is occurs
134
+ def map_events!
135
+ each do |time,events|
136
+ self[time] = events.map{|event| yield event }
137
+ end
138
+ end
139
+
140
+ def clone
141
+ self.class.from_h(to_h)
142
+ end
143
+
144
+ def compact!
145
+ @timeline.delete_if {|t,events| events.empty? }
146
+ end
147
+
148
+ def flatten
149
+ flattened = Timeline.new
150
+ self.each do |time,events|
151
+ events.each do |event|
152
+ if event.is_a? Timeline
153
+ event.flatten.each do |subtime,subevent|
154
+ flattened.add(time+subtime, subevent)
155
+ end
156
+ else
157
+ flattened.add(time,event)
158
+ end
159
+ end
160
+ end
161
+ flattened
162
+ end
163
+
164
+ # @return a new Timeline where all times have been quantized to multiples of the given interval
165
+ # @example timeline.quantize(0.5) # quantize to eight notes (assuming the beat is a quarter note)
166
+ # @see quantize!
167
+ def quantize interval
168
+ map{|time,events| [self.class.quantize_time(time,interval), events] }
169
+ end
170
+
171
+ def quantize! interval
172
+ map!{|time,events| [self.class.quantize_time(time,interval), events] }
173
+ end
174
+
175
+ # shifts all times by the given amount
176
+ # @see #shift!
177
+ # @see #shift_to
178
+ def shift time_delta
179
+ map{|time,events| [time+time_delta, events] }
180
+ end
181
+
182
+ # shifts all times in place by the given amount
183
+ # @see #shift
184
+ # @see #shift_to!
185
+ def shift! time_delta
186
+ map!{|time,events| [time+time_delta, events] }
187
+ end
188
+
189
+ # shifts the times so that the start of the timeline is at the given time
190
+ # @see #shift_to!
191
+ # @see #shift
192
+ def shift_to absolute_time
193
+ start = times.first
194
+ if start
195
+ shift absolute_time - start
196
+ else
197
+ clone
198
+ end
199
+ end
200
+
201
+ # shifts the times in place so that the start of the timeline is at the given time
202
+ # @see #shift_to
203
+ # @see #shift!
204
+ def shift_to! absolute_time
205
+ start = times.first
206
+ if start
207
+ shift! absolute_time - start
208
+ end
209
+ self
210
+ end
211
+
212
+ def to_s
213
+ times = self.times
214
+ last = times.last
215
+ width = sprintf("%d",last).length + 3 # nicely align the '=>' against the longest number
216
+ times.map{|t| sprintf("%#{width}.2f",t)+" => #{@timeline[t].join ', '}" }.join "\n"
217
+ end
218
+
219
+ def inspect
220
+ @timeline.inspect
221
+ end
222
+
223
+ def self.quantize_time time, interval
224
+ upper = interval * (time.to_f/interval).ceil
225
+ lower = upper - interval
226
+ (time - lower) < (upper - time) ? lower : upper
227
+ end
228
+
229
+ end
230
+
231
+ end
232
+ end
@@ -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
@@ -1,5 +1,5 @@
1
1
  module MTK
2
- module Helpers
2
+ module Groups
3
3
 
4
4
  # Given a method #elements, which returns an Array of elements in the collection,
5
5
  # including this module will make the class Enumerable and provide various methods you'd expect from an Array.
@@ -160,5 +160,37 @@ module MTK
160
160
  end
161
161
 
162
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
+
163
195
  end
164
196
  end