mtk 0.0.3.2 → 0.0.3.3

Sign up to get free protection for your applications and to get access to all the features.
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