mtk 0.0.3.3 → 0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +15 -0
  2. data/INTRO.md +63 -31
  3. data/Rakefile +3 -1
  4. data/bin/mtk +75 -32
  5. data/examples/drum_pattern.rb +2 -2
  6. data/examples/dynamic_pattern.rb +1 -1
  7. data/examples/helpers/output_selector.rb +71 -0
  8. data/examples/notation.rb +5 -1
  9. data/examples/tone_row_melody.rb +1 -1
  10. data/lib/mtk.rb +1 -0
  11. data/lib/mtk/core/duration.rb +18 -3
  12. data/lib/mtk/core/intensity.rb +5 -3
  13. data/lib/mtk/core/interval.rb +21 -14
  14. data/lib/mtk/core/pitch.rb +2 -0
  15. data/lib/mtk/core/pitch_class.rb +6 -3
  16. data/lib/mtk/events/event.rb +2 -1
  17. data/lib/mtk/events/note.rb +1 -1
  18. data/lib/mtk/events/parameter.rb +1 -0
  19. data/lib/mtk/events/rest.rb +85 -0
  20. data/lib/mtk/events/timeline.rb +6 -2
  21. data/lib/mtk/io/jsound_input.rb +9 -3
  22. data/lib/mtk/io/midi_file.rb +38 -2
  23. data/lib/mtk/io/midi_input.rb +1 -1
  24. data/lib/mtk/io/midi_output.rb +95 -4
  25. data/lib/mtk/io/unimidi_input.rb +7 -3
  26. data/lib/mtk/lang/durations.rb +31 -26
  27. data/lib/mtk/lang/intensities.rb +29 -30
  28. data/lib/mtk/lang/intervals.rb +108 -41
  29. data/lib/mtk/lang/mtk_grammar.citrus +14 -4
  30. data/lib/mtk/lang/parser.rb +10 -5
  31. data/lib/mtk/lang/pitch_classes.rb +45 -17
  32. data/lib/mtk/lang/pitches.rb +169 -32
  33. data/lib/mtk/lang/tutorial.rb +279 -0
  34. data/lib/mtk/lang/tutorial_lesson.rb +87 -0
  35. data/lib/mtk/sequencers/event_builder.rb +29 -8
  36. data/spec/mtk/core/duration_spec.rb +14 -1
  37. data/spec/mtk/core/intensity_spec.rb +1 -1
  38. data/spec/mtk/events/event_spec.rb +10 -16
  39. data/spec/mtk/events/note_spec.rb +3 -3
  40. data/spec/mtk/events/rest_spec.rb +184 -0
  41. data/spec/mtk/events/timeline_spec.rb +5 -1
  42. data/spec/mtk/io/midi_file_spec.rb +13 -2
  43. data/spec/mtk/io/midi_output_spec.rb +42 -9
  44. data/spec/mtk/lang/durations_spec.rb +5 -5
  45. data/spec/mtk/lang/intensities_spec.rb +5 -5
  46. data/spec/mtk/lang/intervals_spec.rb +139 -13
  47. data/spec/mtk/lang/parser_spec.rb +65 -25
  48. data/spec/mtk/lang/pitch_classes_spec.rb +0 -11
  49. data/spec/mtk/lang/pitches_spec.rb +0 -15
  50. data/spec/mtk/patterns/chain_spec.rb +7 -7
  51. data/spec/mtk/patterns/for_each_spec.rb +2 -2
  52. data/spec/mtk/sequencers/event_builder_spec.rb +49 -17
  53. metadata +12 -22
@@ -1,3 +1,7 @@
1
+ # NOTE: experimental example!
2
+ # This requires Lilypond to be installed, see http://lilypond.org/
3
+ # The lilypond command must be on your PATH or specificed via the LILYPOND_PATH environment variable.
4
+
1
5
  require 'mtk'
2
6
  require 'mtk/io/notation'
3
7
  include MTK
@@ -13,7 +17,7 @@ arg_error "MTK syntax string not provided" unless syntax
13
17
 
14
18
 
15
19
  file = ARGV[1]
16
- arg_error "The output_file must end in '.png', '.pdf', or '.ps'" unless file
20
+ arg_error "The output_file must end in '.png', '.pdf', or '.ps'" unless file =~ /\.(png|pdf|ps)$/
17
21
 
18
22
 
19
23
  sequencer = MTK::Lang::Parser.parse(syntax)
@@ -12,7 +12,7 @@ file = ARGV[0] || 'MTK-tone_row_melody.mid'
12
12
 
13
13
  row = PitchClassSet Db, G, Ab, F, Eb, E, D, C, B, Gb, A, Bb
14
14
  pitch_pattern = Patterns.Cycle *row
15
- rhythm_pattern = Patterns.Choice s, i, i+s, q # choose between sixteenth, eighth, dotted eighth, and quarter
15
+ rhythm_pattern = Patterns.Choice s, e, e+s, q # choose between sixteenth, eighth, dotted eighth, and quarter
16
16
 
17
17
  chain = Patterns.Chain pitch_pattern, rhythm_pattern, min_elements: 36, max_elements: 36
18
18
 
data/lib/mtk.rb CHANGED
@@ -56,6 +56,7 @@ require 'mtk/groups/chord'
56
56
 
57
57
  require 'mtk/events/event'
58
58
  require 'mtk/events/note'
59
+ require 'mtk/events/rest'
59
60
  require 'mtk/events/parameter'
60
61
  require 'mtk/events/timeline'
61
62
 
@@ -3,18 +3,20 @@ module MTK
3
3
 
4
4
  # A measure of time in musical beats.
5
5
  # May be negative to indicate a rest, which uses the absolute value for the effective duration.
6
+ #
7
+ # @see Lang::Durations
6
8
  class Duration
7
9
 
8
10
  include Comparable
9
11
 
10
12
  # The names of the base durations. See {MTK::Lang::Durations} for more info.
11
- NAMES = %w[w h q i s r x].freeze
13
+ NAMES = %w[w h q e s r x].freeze
12
14
 
13
15
  VALUES_BY_NAME = {
14
16
  'w' => 4,
15
17
  'h' => 2,
16
18
  'q' => 1,
17
- 'i' => Rational(1,2),
19
+ 'e' => Rational(1,2),
18
20
  's' => Rational(1,4),
19
21
  'r' => Rational(1,8),
20
22
  'x' => Rational(1,16)
@@ -55,7 +57,7 @@ module MTK
55
57
  # @example lookup the value of 3/4w, which three-quarters of a whole note (3 beats):
56
58
  # MTK::Core::Duration.from_s('3/4w')
57
59
  def self.from_s(s)
58
- if s =~ /^(-)?(\d+([\.\/]\d+)?)?([whqisrx])((\.|t)*)$/i
60
+ if s =~ /^(-)?(\d+([\.\/]\d+)?)?([whqesrx])((\.|t)*)$/i
59
61
  name = $4.downcase
60
62
  modifier = $5.downcase
61
63
  modifier << $1 if $1 # negation
@@ -95,6 +97,7 @@ module MTK
95
97
  # The magnitude (absolute value) of the duration.
96
98
  # This is the actual duration for rests.
97
99
  # @see #rest?
100
+ # @see #abs
98
101
  def length
99
102
  @value < 0 ? -@value : @value
100
103
  end
@@ -106,6 +109,18 @@ module MTK
106
109
  @value < 0
107
110
  end
108
111
 
112
+ # Force resets to be non-rests, otherwise don't change the duration.
113
+ # @see #-@
114
+ # @see #length
115
+ # @see #rest?
116
+ def abs
117
+ if @value < 0
118
+ -self
119
+ else
120
+ self
121
+ end
122
+ end
123
+
109
124
  # The number of beats as a floating point number
110
125
  def to_f
111
126
  @value.to_f
@@ -2,12 +2,14 @@ module MTK
2
2
  module Core
3
3
 
4
4
  # A measure of intensity, using an underlying value in the range 0.0-1.0
5
+ #
6
+ # @see Lang::Intensities
5
7
  class Intensity
6
8
 
7
9
  include Comparable
8
10
 
9
- # The names of the base intensities. See {}MTK::Lang::Intensities} for more info.
10
- NAMES = %w[ppp pp p mp mf o ff fff].freeze
11
+ # The names of the base intensities. See {MTK::Lang::Intensities} for more info.
12
+ NAMES = %w[ppp pp p mp mf f ff fff].freeze
11
13
 
12
14
  VALUES_BY_NAME = {
13
15
  'ppp' => 0.125,
@@ -15,7 +17,7 @@ module MTK
15
17
  'p' => 0.375,
16
18
  'mp' => 0.5,
17
19
  'mf' => 0.625,
18
- 'o' => 0.75,
20
+ 'f' => 0.75,
19
21
  'ff' => 0.875,
20
22
  'fff' => 1.0
21
23
  }
@@ -2,29 +2,35 @@ module MTK
2
2
  module Core
3
3
 
4
4
  # A measure of intensity, using an underlying value in the range 0.0-1.0
5
+ #
6
+ # @see Lang::Intervals
5
7
  class Interval
6
8
 
7
9
  include Comparable
8
10
 
9
11
  # The preferred names of all pre-defined intervals
12
+ # @see ALL_NAMES
10
13
  NAMES = %w[P1 m2 M2 m3 M3 P4 TT P5 m6 M6 m7 M7 P8].freeze
11
14
 
12
15
  # All valid names of pre-defined intervals, indexed by their value.
16
+ # @see ALL_NAMES
17
+ # @see NAMES
18
+ # @see http://en.wikipedia.org/wiki/Interval_(music)#Main_intervals
13
19
  NAMES_BY_VALUE =
14
- [ # names # value # description
15
- %w( P1 p1 ), # 0 # unison
16
- %w( m2 min2 ), # 1 # minor second
17
- %w( M2 maj2 ), # 2 # major second
18
- %w( m3 min3 ), # 3 # minor third
19
- %w( M3 maj3 ), # 4 # major third
20
- %w( P4 p4 ), # 5 # perfect fourth
21
- %w( TT tt ), # 6 # tritone (AKA augmented fourth, diminished fifth)
22
- %w( P5 p5 ), # 7 # perfect fifth
23
- %w( m6 min6 ), # 8 # minor sixth
24
- %w( M6 maj6 ), # 9 # major sixth
25
- %w( m7 min7 ), # 10 # minor seventh
26
- %w( M7 maj7 ), # 11 # major seventh
27
- %w( P8 p8 ) # 12 # octave
20
+ [ # names # value # description # enharmonic equivalents
21
+ %w( P1 d2 ), # 0 # unison # diminished second
22
+ %w( m2 a1 ), # 1 # minor second # augmented unison
23
+ %w( M2 d3 ), # 2 # major second # diminished third
24
+ %w( m3 a2 ), # 3 # minor third # augmented second
25
+ %w( M3 d4 ), # 4 # major third # diminished fourth
26
+ %w( P4 a3 ), # 5 # perfect fourth # augmented third
27
+ %w( TT a4 d5 ),# 6 # tritone # augmented fourth, diminished fifth
28
+ %w( P5 d6 ), # 7 # perfect fifth # diminished sixth
29
+ %w( m6 a5 ), # 8 # minor sixth # augmented fifth
30
+ %w( M6 d7 ), # 9 # major sixth # diminished seventh
31
+ %w( m7 a6 ), # 10 # minor seventh # augmented sixth
32
+ %w( M7 d8 ), # 11 # major seventh # diminished octave
33
+ %w( P8 a7 ) # 12 # octave # augmented seventh
28
34
  ].freeze
29
35
 
30
36
  # A mapping from intervals names to their value
@@ -35,6 +41,7 @@ module MTK
35
41
  ].freeze
36
42
 
37
43
  # All valid interval names
44
+ # @see NAMES_BY_VALUE
38
45
  ALL_NAMES = NAMES_BY_VALUE.flatten.freeze
39
46
 
40
47
 
@@ -2,6 +2,8 @@ module MTK
2
2
  module Core
3
3
 
4
4
  # A frequency represented by a {PitchClass}, an integer octave, and an offset in semitones.
5
+ #
6
+ # @see Lang::Pitches
5
7
  class Pitch
6
8
 
7
9
  include Comparable
@@ -4,10 +4,10 @@ module MTK
4
4
  # A set of all pitches that are an integer number of octaves apart.
5
5
  # A {Pitch} has the same PitchClass as the pitches one or more octaves away.
6
6
  # @see https://en.wikipedia.org/wiki/Pitch_class
7
- #
7
+ # @see Lang::PitchClasses
8
8
  class PitchClass
9
9
 
10
- # The normalized names of the 12 pitch classes in the chromatic scale.
10
+ # The preferred names of the 12 pitch classes in the chromatic scale.
11
11
  # The index of each {#name} is the pitch class's numeric {#value}.
12
12
  NAMES = %w( C Db D Eb E F Gb G Ab A Bb B ).freeze
13
13
 
@@ -15,7 +15,7 @@ module MTK
15
15
  # organized such that each index contains the allowed names of the pitch class with a {#value} equal to that index.
16
16
  # @see VALID_NAMES
17
17
  VALID_NAMES_BY_VALUE =
18
- [ # (valid names ), # value # normalized name
18
+ [ # (valid names ), # value # preferred name
19
19
  %w( B# C Dbb ), # 0 # C
20
20
  %w( B## C# Db ), # 1 # Db
21
21
  %w( C## D Ebb ), # 2 # D
@@ -48,6 +48,9 @@ module MTK
48
48
 
49
49
  # The value of this pitch class.
50
50
  # An integer from 0..11 that indexes this pitch class in {PITCH_CLASSES} and the {#name} in {NAMES}.
51
+ #
52
+ # This value is fairly arbitrary and just used for sorting purposes and mod 12 arithmetic when composing
53
+ # directly with pitch classes.
51
54
  attr_reader :value
52
55
 
53
56
 
@@ -51,7 +51,7 @@ module MTK
51
51
  end
52
52
 
53
53
  def to_h
54
- hash = {:type => @type}
54
+ hash = {type: @type}
55
55
  hash[:value] = @value unless @value.nil?
56
56
  hash[:duration] = @duration unless @duration.nil?
57
57
  hash[:number] = @number unless @number.nil?
@@ -82,6 +82,7 @@ module MTK
82
82
  @duration.length
83
83
  end
84
84
 
85
+ # True if this event represents a rest, false otherwise.
85
86
  # By convention, any events with negative durations are a rest
86
87
  def rest?
87
88
  @duration.rest?
@@ -2,7 +2,7 @@ module MTK
2
2
 
3
3
  module Events
4
4
 
5
- # A musical {Event} defined by a {Pitch}, intensity, and duration
5
+ # A musical {Event} defined by a {Core::Pitch}, {Core::Intensity}, and {Core::Duration}
6
6
  class Note < Event
7
7
 
8
8
  DEFAULT_DURATION = MTK::Core::Duration[1]
@@ -2,6 +2,7 @@ module MTK
2
2
 
3
3
  module Events
4
4
 
5
+ # A non-note event such as pitch bend, pressure (aftertouch), or control change (CC)
5
6
  class Parameter < Event
6
7
 
7
8
  def self.from_midi(status, data1, data2)
@@ -0,0 +1,85 @@
1
+ module MTK
2
+
3
+ module Events
4
+
5
+ # An interval of silence.
6
+ #
7
+ # By convention, MTK uses {Core::Duration}s with negative values for rests. This class forces the {#duration}
8
+ # to always have a negative value.
9
+ #
10
+ # @note Because a negative durations indicate rests, other {Event} classes may represent rests too.
11
+ # Therefore, you should always determine if an {Event} is a rest via the {#rest?} method, instead
12
+ # of seeing if the class is an MTK::Events::Rest
13
+ class Rest < Event
14
+
15
+ def initialize(duration, channel=nil)
16
+ super :rest, duration:duration, channel:channel
17
+ self.duration = @duration # force to be a rest
18
+ end
19
+
20
+ def self.from_h(hash)
21
+ new(hash[:duration], hash[:channel])
22
+ end
23
+
24
+ # Assign the duration, forcing to a negative value to indicate this is a rest.
25
+ def duration= duration
26
+ super
27
+ @duration = -@duration unless @duration.rest? # force to be a rest
28
+ end
29
+
30
+ # Rests don't have a corresponding value in MIDI, so this is nil
31
+ # @return nil
32
+ def midi_value
33
+ nil
34
+ end
35
+
36
+ # Rests don't have a corresponding value in MIDI, so this is a no-op
37
+ def midi_value= value
38
+ end
39
+
40
+ def to_s
41
+ "Rest(#{@duration})"
42
+ end
43
+
44
+ def inspect
45
+ "#<#{self.class}:#{object_id} @duration=#{@duration.inspect}" +
46
+ if @channel then ", @channel=#{@channel}>" else '>' end
47
+ end
48
+
49
+ end
50
+ end
51
+
52
+
53
+ # Construct a {Events::Rest} from a list of any supported type for the arguments: pitch, intensity, duration, channel
54
+ def Rest(*anything)
55
+ anything = anything.first if anything.size == 1
56
+ case anything
57
+ when MTK::Events::Rest then anything
58
+ when MTK::Events::Event then MTK::Events::Rest.new(anything.duration, anything.channel)
59
+ when Numeric then MTK::Events::Rest.new(anything)
60
+ when Duration then MTK::Events::Rest.new(anything)
61
+
62
+ when Array
63
+ duration = nil
64
+ channel = nil
65
+ unknowns = []
66
+ anything.each do |item|
67
+ case item
68
+ when MTK::Core::Duration then duration = item
69
+ else unknowns << item
70
+ end
71
+ end
72
+
73
+ duration = MTK.Duration(unknowns.shift) if duration.nil? and not unknowns.empty?
74
+ raise "MTK::Rest() couldn't find a duration in arguments: #{anything.inspect}" if duration.nil?
75
+ channel = unknowns.shift.to_i if channel.nil? and not unknowns.empty?
76
+
77
+ MTK::Events::Rest.new(duration, channel)
78
+
79
+ else
80
+ raise "MTK::Rest() doesn't understand #{anything.class}"
81
+ end
82
+ end
83
+ module_function :Rest
84
+
85
+ end
@@ -212,8 +212,12 @@ module MTK
212
212
  def to_s
213
213
  times = self.times
214
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"
215
+ if last
216
+ width = sprintf("%d",last).length + 3 # nicely align the '=>' against the longest number
217
+ times.map{|t| sprintf("%#{width}.2f",t)+" => #{@timeline[t].join ', '}" }.join "\n"
218
+ else
219
+ ''
220
+ end
217
221
  end
218
222
 
219
223
  def inspect
@@ -50,7 +50,7 @@ module MTK
50
50
  def to_timeline(options={})
51
51
  bpm = options.fetch :bmp, 120
52
52
  beats_per_second = bpm.to_f/60
53
- timeline = Timeline.new
53
+ timeline = MTK::Events::Timeline.new
54
54
  note_ons = {}
55
55
  start = nil
56
56
 
@@ -59,7 +59,13 @@ module MTK
59
59
  time -= start
60
60
  time /= beats_per_second
61
61
 
62
- case message.type
62
+ message_type = message.type
63
+ message_type = :note_off if message_type == :note_on and message.velocity == 0
64
+ # TODO: this will need to be made more robust when we support off velocities
65
+
66
+ next if message_type == :unknown # Ignore garbage messages
67
+
68
+ case message_type
63
69
  when :note_on
64
70
  note_ons[message.pitch] = [message,time]
65
71
 
@@ -71,7 +77,7 @@ module MTK
71
77
  timeline.add time,note
72
78
  end
73
79
 
74
- else timeline.add time, MTK::Events::Parameter.from_midi([message.type, message.channel], message.data1, message.data2)
80
+ else timeline.add time, MTK::Events::Parameter.from_midi([message_type, message.channel], message.data1, message.data2)
75
81
  end
76
82
  end
77
83
 
@@ -41,6 +41,10 @@ module MTK
41
41
  case event
42
42
  when ::MIDI::NoteOn
43
43
  note_ons[event.note] = [time,event]
44
+ # TODO: handle note ons with velocity 0 as a note off (use output from Logic Pro as a test case)
45
+ # This isn't actually necessary right now as midilibs seqreader#note_on automatically
46
+ # converts note ons with velocity 0 to note offs. In the future, for full off velocity support,
47
+ # I'll need to monkey patch midilib and update the code here
44
48
 
45
49
  when ::MIDI::NoteOff
46
50
  on_time,on_event = note_ons.delete(event.note)
@@ -99,7 +103,11 @@ module MTK
99
103
  pitch, velocity = event.midi_pitch, event.velocity
100
104
  add_event track, time => note_on(channel, pitch, velocity)
101
105
  duration = event.duration_in_pulses(clock_rate)
102
- add_event track, time+duration => note_off(channel, pitch, velocity)
106
+ # TODO: use note_off events when supporting off velocities
107
+ # add_event track, time+duration => note_off(channel, pitch, velocity)
108
+ # NOTE: cannot test the following line of code properly right now, because midilib automatically converts
109
+ # note ons with velocity 0 to note offs when reading files. See comments in #to_timelines in this file
110
+ add_event track, time+duration => note_on(channel, pitch, 0) # we use note ons with velocity 0 to indicate no off velocity
103
111
 
104
112
  when :control
105
113
  add_event track, time => cc(channel, event.number, event.midi_value)
@@ -182,8 +190,10 @@ module MTK
182
190
 
183
191
  def add_track sequence, opts={}
184
192
  track = ::MIDI::Track.new(sequence)
185
- track.name = opts.fetch :name, ''
193
+ track_name = opts[:name]
194
+ track.name = track_name if track_name
186
195
  sequence.tracks << track
196
+ sequence.format = if sequence.tracks.length > 1 then 1 else 0 end
187
197
  track
188
198
  end
189
199
 
@@ -207,3 +217,29 @@ module MTK
207
217
 
208
218
  end
209
219
 
220
+
221
+ ####################################################################################
222
+ # MONKEY PATCHING to prevent blank instrument name meta events from being generated
223
+ # This can be removed if my pull request https://github.com/jimm/midilib/pull/5
224
+ # is merged into midilib and a new gem is released with these changes.
225
+
226
+ # @private
227
+ module MIDI
228
+ module IO
229
+ class SeqWriter
230
+ alias original_write_instrument write_instrument
231
+ def write_instrument(instrument)
232
+ original_write_instrument(instrument) unless instrument.nil?
233
+ end
234
+
235
+ # Also monkey patching write_header to support alternate MIDI file formats
236
+ def write_header
237
+ @io.print 'MThd'
238
+ write32(6)
239
+ write16(@seq.format || 1)
240
+ write16(@seq.tracks.length)
241
+ write16(@seq.ppqn)
242
+ end
243
+ end
244
+ end
245
+ end