mtk 0.0.3.3 → 0.4

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 (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