jmtk 0.0.3.3-java → 0.4-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 (56) hide show
  1. checksums.yaml +15 -0
  2. data/DEVELOPMENT_NOTES.md +20 -0
  3. data/INTRO.md +63 -31
  4. data/README.md +9 -3
  5. data/Rakefile +42 -42
  6. data/bin/jmtk +75 -32
  7. data/bin/mtk +75 -32
  8. data/examples/drum_pattern.rb +2 -2
  9. data/examples/dynamic_pattern.rb +1 -1
  10. data/examples/helpers/output_selector.rb +71 -0
  11. data/examples/notation.rb +5 -1
  12. data/examples/tone_row_melody.rb +1 -1
  13. data/lib/mtk.rb +1 -0
  14. data/lib/mtk/core/duration.rb +18 -3
  15. data/lib/mtk/core/intensity.rb +5 -3
  16. data/lib/mtk/core/interval.rb +21 -14
  17. data/lib/mtk/core/pitch.rb +2 -0
  18. data/lib/mtk/core/pitch_class.rb +6 -3
  19. data/lib/mtk/events/event.rb +2 -1
  20. data/lib/mtk/events/note.rb +1 -1
  21. data/lib/mtk/events/parameter.rb +1 -0
  22. data/lib/mtk/events/rest.rb +85 -0
  23. data/lib/mtk/events/timeline.rb +6 -2
  24. data/lib/mtk/io/jsound_input.rb +9 -3
  25. data/lib/mtk/io/midi_file.rb +38 -2
  26. data/lib/mtk/io/midi_input.rb +1 -1
  27. data/lib/mtk/io/midi_output.rb +95 -4
  28. data/lib/mtk/io/unimidi_input.rb +7 -3
  29. data/lib/mtk/lang/durations.rb +31 -26
  30. data/lib/mtk/lang/intensities.rb +29 -30
  31. data/lib/mtk/lang/intervals.rb +108 -41
  32. data/lib/mtk/lang/mtk_grammar.citrus +14 -4
  33. data/lib/mtk/lang/parser.rb +10 -5
  34. data/lib/mtk/lang/pitch_classes.rb +45 -17
  35. data/lib/mtk/lang/pitches.rb +169 -32
  36. data/lib/mtk/lang/tutorial.rb +279 -0
  37. data/lib/mtk/lang/tutorial_lesson.rb +87 -0
  38. data/lib/mtk/sequencers/event_builder.rb +29 -8
  39. data/spec/mtk/core/duration_spec.rb +14 -1
  40. data/spec/mtk/core/intensity_spec.rb +1 -1
  41. data/spec/mtk/events/event_spec.rb +10 -16
  42. data/spec/mtk/events/note_spec.rb +3 -3
  43. data/spec/mtk/events/rest_spec.rb +184 -0
  44. data/spec/mtk/events/timeline_spec.rb +5 -1
  45. data/spec/mtk/io/midi_file_spec.rb +13 -2
  46. data/spec/mtk/io/midi_output_spec.rb +42 -9
  47. data/spec/mtk/lang/durations_spec.rb +5 -5
  48. data/spec/mtk/lang/intensities_spec.rb +5 -5
  49. data/spec/mtk/lang/intervals_spec.rb +139 -13
  50. data/spec/mtk/lang/parser_spec.rb +65 -25
  51. data/spec/mtk/lang/pitch_classes_spec.rb +0 -11
  52. data/spec/mtk/lang/pitches_spec.rb +0 -15
  53. data/spec/mtk/patterns/chain_spec.rb +7 -7
  54. data/spec/mtk/patterns/for_each_spec.rb +2 -2
  55. data/spec/mtk/sequencers/event_builder_spec.rb +49 -17
  56. metadata +12 -22
@@ -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
@@ -80,7 +80,7 @@ module MTK
80
80
  end
81
81
 
82
82
  def to_timeline
83
- Timeline.new
83
+ MTK::Events::Timeline.new
84
84
  end
85
85
 
86
86
  end
@@ -105,8 +105,12 @@ module MTK
105
105
  pitch = event.midi_pitch
106
106
  velocity = event.velocity
107
107
  duration = event.duration.to_f
108
- @scheduler.at(time) { note_on(pitch,velocity,channel) }
109
- @scheduler.at(time + duration) { note_off(pitch,velocity,channel) }
108
+ # Set a lower priority (via level:1) for note-ons, so legato notes at the same pitch don't
109
+ # prematurely chop off the next note, by ensuring all note-offs at the same timepoint occur first.
110
+ @scheduler.at(time, level: 1) { note_on(pitch,velocity,channel) }
111
+ @scheduler.at(time + duration) { note_on(pitch,0,channel) }
112
+ # TODO: use a proper note off message whenever we support off velocities
113
+ #@scheduler.at(time + duration) { note_off(pitch,velocity,channel) }
110
114
 
111
115
  when :control
112
116
  @scheduler.at(time) { control(event.number, event.midi_value, channel) }
@@ -127,7 +131,10 @@ module MTK
127
131
  end
128
132
  end
129
133
 
130
- end_time = timeline.times.last + trailing_buffer
134
+ end_time = timeline.times.last
135
+ final_events = timeline[end_time]
136
+ max_length = final_events.inject(0) {|max,event| len = event.length; max > len ? max : len } || 0
137
+ end_time += max_length + trailing_buffer
131
138
  @scheduler.at(end_time) { @scheduler.stop }
132
139
 
133
140
  thread = @scheduler.run
@@ -192,4 +199,88 @@ unless $__RUNNING_RSPEC_TESTS__ # I can't get this working on Travis-CI, problem
192
199
  else
193
200
  require 'mtk/io/unimidi_output'
194
201
  end
195
- end
202
+ end
203
+
204
+
205
+
206
+ ####################################################################################
207
+ # MONKEY PATCHING gamelan to ensure note-offs come before note-ons
208
+ # when they occur at the same scheduler time.
209
+ # This patch applies https://github.com/jvoorhis/gamelan/commit/20d93f4e5d86517bd5c6f9212a0dcdbf371d1ea1
210
+ # to provide the priority/level feature for the scheduler (see @scheduler.at() calls for notes above).
211
+ # This patch should not be necessary when gamelan gem > 0.3 is released
212
+
213
+ # @private
214
+ module Gamelan
215
+
216
+ DEFAULT_PRIORITY = 0
217
+ NOTEON_PRIORITY = 0
218
+ CC_PRIORITY = -1
219
+ PC_PRIORITY = -2
220
+ NOTEOFF_PRIORITY = -3
221
+
222
+ class Priority < Struct.new(:time, :level)
223
+ include Comparable
224
+ def <=>(prio)
225
+ self.to_a <=> prio.to_a
226
+ end
227
+ end
228
+
229
+ if defined?(JRUBY_VERSION)
230
+
231
+ class Queue
232
+ def initialize(scheduler)
233
+ @scheduler = scheduler
234
+ @queue = PriorityQueue.new(10000) { |a,b|
235
+ a.priority <=> b.priority
236
+ }
237
+ end
238
+
239
+ def ready?
240
+ if top = @queue.peek
241
+ top.delay < @scheduler.phase
242
+ else
243
+ false
244
+ end
245
+ end
246
+ end
247
+
248
+ else
249
+
250
+ class Queue
251
+ def push(task)
252
+ @queue.push(task, task.priority)
253
+ end
254
+ alias << push
255
+
256
+ def ready?
257
+ if top = @queue.min
258
+ top[1].time < @scheduler.phase
259
+ else
260
+ false
261
+ end
262
+ end
263
+ end
264
+
265
+ end
266
+
267
+ class Scheduler < Timer
268
+ def at(delay, options = {}, &task)
269
+ options = { :delay => delay, :scheduler => self }.merge(options)
270
+ @queue << Task.new(options, &task)
271
+ end
272
+ end
273
+
274
+ class Task
275
+ attr_reader :level, :priority
276
+
277
+ def initialize(options, &block)
278
+ @scheduler = options[:scheduler]
279
+ @delay = options[:delay].to_f
280
+ @level = options[:level] || DEFAULT_PRIORITY
281
+ @priority = Priority.new(@delay, @level)
282
+ @proc = block
283
+ @args = options[:args] || []
284
+ end
285
+ end
286
+ end
@@ -59,7 +59,7 @@ module MTK
59
59
 
60
60
  bpm = options.fetch :bmp, 120
61
61
  beats_per_second = bpm.to_f/60
62
- timeline = Timeline.new
62
+ timeline = MTK::Events::Timeline.new
63
63
  note_ons = {}
64
64
  start = nil
65
65
 
@@ -69,9 +69,13 @@ module MTK
69
69
  time /= beats_per_second
70
70
 
71
71
  if message.is_a? MTK::Events::Event
72
- timeline.add time,message
72
+ timeline.add time,message unless message.type == :unknown
73
73
  else
74
- case message.type
74
+ message_type = message.type
75
+ message_type = :note_off if message_type == :note_on and message.velocity == 0
76
+ # TODO: this will need to be made more robust when we support off velocities
77
+
78
+ case message_type
75
79
  when :note_on
76
80
  pitch = message.pitch
77
81
  note_ons[pitch] = [message,time]
@@ -5,51 +5,56 @@ module MTK
5
5
 
6
6
  # Defines duration constants using abbreviations for standard rhythm values ('w' for whole note, 'h' for half note, etc).
7
7
  #
8
- # In order to avoid conflict with pitch class 'e', the constant for eighth note is 'i'
8
+ # These can be thought of like constants, but in order to support the lower case names,
9
+ # it was necessary to define them as "pseudo constant" methods.
10
+ # Like constants, these methods are available either through the module (MTK::Lang::Durations::q) or
11
+ # via mixin (include MTK::Lang::Durations; q). They are listed under the "Instance Attribute Summary" of this page.
9
12
  #
10
- # These can be thought of like constants, but they
11
- # use lower-case names and therefore define them as "pseudo constant" methods.
12
- # The methods are available either through the module (MTK::Core::Durations::e) or via mixin (include MTK::Core::Durations; q)
13
- #
14
- # These values assume the quarter note is one beat (1.0), so they work best with 4/4 and other */4 time signatures.
13
+ # These values assume the quarter note is one beat, so you may find they work best with 4/4 and other */4 time signatures
14
+ # (although it's certainly possible to use them with less common time signatures like 5/8).
15
15
  #
16
16
  # @note Including this module defines a bunch of single-character variables, which may shadow existing variable names.
17
17
  # Just be mindful of what is defined in this module when including it.
18
18
  #
19
- # @see Note
19
+ # @see Core::Duration
20
+ # @see Events::Note
20
21
  module Durations
21
22
  extend MTK::Lang::PseudoConstants
22
23
 
23
- # NOTE: the yard doc macros here only fill in [$2] with the actual value when generating docs under Ruby 1.9+
24
-
24
+ # @private
25
+ # @!macro [attach] define_duration
26
+ # $3: $4 beat(s)
27
+ # @!attribute [r]
28
+ # @return [MTK::Core::Duration] duration of $4 beat(s)
29
+ def self.define_duration name, value, description, beats
30
+ define_constant name, value
31
+ end
32
+
33
+
25
34
  # whole note
26
- # @macro [attach] durations.define_constant
35
+ # @macro [attach] durations.define_duration
27
36
  # @attribute [r]
28
- # @return [$2] number of beats for $1
29
- define_constant 'w', MTK::Core::Duration[4]
37
+ # @return [$2] duration for $1
38
+ define_duration 'w', MTK::Core::Duration[4], 'whole note', 4
39
+
40
+ define_duration 'h', MTK::Core::Duration[2], 'half note', 2
30
41
 
31
- # half note
32
- define_constant 'h', MTK::Core::Duration[2]
42
+ define_duration 'q', MTK::Core::Duration[1], 'quarter note', 1
33
43
 
34
- # quarter note
35
- define_constant 'q', MTK::Core::Duration[1]
44
+ define_duration 'e', MTK::Core::Duration[Rational(1,2)], 'eighth note', '1/2'
36
45
 
37
- # eight note
38
- define_constant 'i', MTK::Core::Duration[Rational(1,2)]
46
+ define_duration 's', MTK::Core::Duration[Rational(1,4)], 'sixteenth note', '1/4'
39
47
 
40
- # sixteenth note
41
- define_constant 's', MTK::Core::Duration[Rational(1,4)]
48
+ define_duration 'r', MTK::Core::Duration[Rational(1,8)], 'thirty-second note', '1/8'
42
49
 
43
- # thirty-second note
44
- define_constant 'r', MTK::Core::Duration[Rational(1,8)]
50
+ define_duration 'x', MTK::Core::Duration[Rational(1,16)], 'sixty-fourth note', '1/16'
45
51
 
46
- # sixty-fourth note
47
- define_constant 'x', MTK::Core::Duration[Rational(1,16)]
48
52
 
49
- # The values of all "psuedo constants" defined in this module
50
- DURATIONS = [w, h, q, i, s, r, x].freeze
53
+ # All "psuedo constants" defined in this module
54
+ DURATIONS = [w, h, q, e, s, r, x].freeze
51
55
 
52
56
  # The names of all "psuedo constants" defined in this module
57
+ # @see MTK::Core::Duration::NAMES
53
58
  DURATION_NAMES = MTK::Core::Duration::NAMES
54
59
 
55
60
  end