mtk 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. data/.yardopts +9 -0
  2. data/INTRO.md +73 -0
  3. data/LICENSE.txt +27 -0
  4. data/README.md +93 -18
  5. data/Rakefile +13 -1
  6. data/examples/crescendo.rb +20 -0
  7. data/examples/dynamic_pattern.rb +39 -0
  8. data/examples/play_midi.rb +19 -0
  9. data/examples/print_midi.rb +13 -0
  10. data/examples/random_tone_row.rb +18 -0
  11. data/examples/tone_row_melody.rb +21 -0
  12. data/lib/mtk/_constants/durations.rb +80 -0
  13. data/lib/mtk/_constants/intensities.rb +81 -0
  14. data/lib/mtk/{constants → _constants}/intervals.rb +10 -1
  15. data/lib/mtk/_constants/pitch_classes.rb +35 -0
  16. data/lib/mtk/_constants/pitches.rb +49 -0
  17. data/lib/mtk/{numeric_extensions.rb → _numeric_extensions.rb} +0 -0
  18. data/lib/mtk/event.rb +14 -5
  19. data/lib/mtk/helper/collection.rb +114 -0
  20. data/lib/mtk/helper/event_builder.rb +85 -0
  21. data/lib/mtk/{constants → helper}/pseudo_constants.rb +7 -6
  22. data/lib/mtk/lang/grammar.rb +17 -0
  23. data/lib/mtk/lang/mtk_grammar.citrus +60 -0
  24. data/lib/mtk/midi/file.rb +10 -15
  25. data/lib/mtk/midi/jsound_input.rb +68 -0
  26. data/lib/mtk/midi/jsound_output.rb +80 -0
  27. data/lib/mtk/note.rb +22 -3
  28. data/lib/mtk/pattern/abstract_pattern.rb +132 -0
  29. data/lib/mtk/pattern/choice.rb +25 -9
  30. data/lib/mtk/pattern/cycle.rb +51 -0
  31. data/lib/mtk/pattern/enumerator.rb +26 -0
  32. data/lib/mtk/pattern/function.rb +46 -0
  33. data/lib/mtk/pattern/lines.rb +60 -0
  34. data/lib/mtk/pattern/palindrome.rb +42 -0
  35. data/lib/mtk/pattern/sequence.rb +15 -50
  36. data/lib/mtk/pitch.rb +45 -6
  37. data/lib/mtk/pitch_class.rb +36 -35
  38. data/lib/mtk/pitch_class_set.rb +46 -14
  39. data/lib/mtk/pitch_set.rb +20 -31
  40. data/lib/mtk/sequencer/abstract_sequencer.rb +85 -0
  41. data/lib/mtk/sequencer/rhythmic_sequencer.rb +29 -0
  42. data/lib/mtk/sequencer/step_sequencer.rb +26 -0
  43. data/lib/mtk/timeline.rb +75 -22
  44. data/lib/mtk/transform/invertible.rb +15 -0
  45. data/lib/mtk/{util → transform}/mappable.rb +6 -2
  46. data/lib/mtk/transform/set_theory_operations.rb +34 -0
  47. data/lib/mtk/transform/transposable.rb +14 -0
  48. data/lib/mtk.rb +56 -22
  49. data/spec/mtk/_constants/durations_spec.rb +118 -0
  50. data/spec/mtk/{constants/dynamics_spec.rb → _constants/intensities_spec.rb} +48 -17
  51. data/spec/mtk/{constants → _constants}/intervals_spec.rb +21 -0
  52. data/spec/mtk/_constants/pitch_classes_spec.rb +58 -0
  53. data/spec/mtk/_constants/pitches_spec.rb +52 -0
  54. data/spec/mtk/{numeric_extensions_spec.rb → _numeric_extensions_spec.rb} +0 -0
  55. data/spec/mtk/event_spec.rb +19 -0
  56. data/spec/mtk/helper/collection_spec.rb +291 -0
  57. data/spec/mtk/helper/event_builder_spec.rb +92 -0
  58. data/spec/mtk/helper/pseudo_constants_spec.rb +20 -0
  59. data/spec/mtk/lang/grammar_spec.rb +100 -0
  60. data/spec/mtk/midi/file_spec.rb +41 -6
  61. data/spec/mtk/note_spec.rb +53 -3
  62. data/spec/mtk/pattern/abstract_pattern_spec.rb +45 -0
  63. data/spec/mtk/pattern/choice_spec.rb +89 -3
  64. data/spec/mtk/pattern/cycle_spec.rb +133 -0
  65. data/spec/mtk/pattern/function_spec.rb +133 -0
  66. data/spec/mtk/pattern/lines_spec.rb +93 -0
  67. data/spec/mtk/pattern/note_cycle_spec.rb.bak +116 -0
  68. data/spec/mtk/pattern/palindrome_spec.rb +124 -0
  69. data/spec/mtk/pattern/pitch_cycle_spec.rb.bak +47 -0
  70. data/spec/mtk/pattern/pitch_sequence_spec.rb.bak +37 -0
  71. data/spec/mtk/pattern/sequence_spec.rb +128 -31
  72. data/spec/mtk/pitch_class_set_spec.rb +240 -7
  73. data/spec/mtk/pitch_class_spec.rb +84 -18
  74. data/spec/mtk/pitch_set_spec.rb +45 -10
  75. data/spec/mtk/pitch_spec.rb +59 -0
  76. data/spec/mtk/sequencer/abstract_sequencer_spec.rb +159 -0
  77. data/spec/mtk/sequencer/rhythmic_sequencer_spec.rb +49 -0
  78. data/spec/mtk/sequencer/step_sequencer_spec.rb +71 -0
  79. data/spec/mtk/timeline_spec.rb +118 -15
  80. data/spec/spec_helper.rb +4 -3
  81. metadata +59 -22
  82. data/lib/mtk/chord.rb +0 -47
  83. data/lib/mtk/constants/dynamics.rb +0 -56
  84. data/lib/mtk/constants/pitch_classes.rb +0 -18
  85. data/lib/mtk/constants/pitches.rb +0 -24
  86. data/lib/mtk/pattern/note_sequence.rb +0 -60
  87. data/lib/mtk/pattern/pitch_sequence.rb +0 -22
  88. data/lib/mtk/patterns.rb +0 -4
  89. data/spec/mtk/chord_spec.rb +0 -74
  90. data/spec/mtk/constants/pitch_classes_spec.rb +0 -35
  91. data/spec/mtk/constants/pitches_spec.rb +0 -23
  92. data/spec/mtk/pattern/note_sequence_spec.rb +0 -121
  93. data/spec/mtk/pattern/pitch_sequence_spec.rb +0 -47
@@ -0,0 +1,49 @@
1
+ module MTK
2
+
3
+ # Defines a constants for each {Pitch} in the standard MIDI range using scientific pitch notation.
4
+ #
5
+ # See http://en.wikipedia.org/wiki/Scientific_pitch_notation
6
+ #
7
+ # @note Because the character '#' cannot be used in the name of a constant,
8
+ # the "black key" pitches are all named as flats with 'b' (for example, Gb3 or Cb4)
9
+ # @note Because the character '-' (minus) cannot be used in the name of a constant,
10
+ # the low pitches use '_' (underscore) in place of '-' (minus) (for example C_1).
11
+ module Pitches
12
+
13
+ # The values of all "psuedo constants" defined in this module
14
+ PITCHES = []
15
+
16
+ # The names of all "psuedo constants" defined in this module
17
+ PITCH_NAMES = []
18
+
19
+ 128.times do |note_number|
20
+ pitch = Pitch.from_i( note_number )
21
+ PITCHES << pitch
22
+
23
+ octave_str = pitch.octave.to_s.sub(/-/,'_') # '_1' for -1
24
+ name = "#{pitch.pitch_class}#{octave_str}"
25
+ PITCH_NAMES << name
26
+
27
+ const_set name, pitch
28
+ end
29
+
30
+ PITCHES.freeze
31
+ PITCH_NAMES.freeze
32
+
33
+ # Lookup the value of an pitch constant by name.
34
+ # @example lookup value of 'C3'
35
+ # MTK::Pitches['C3']
36
+ # @see Pitch.from_s
37
+ # @note Unlike {Pitch.from_s} this method will accept either '_' (underscore) or '-' (minus) and treat it like '-' (minus)
38
+ # @note Unlike {Pitch.from_s} this method only accepts the accidental 'b'
39
+ def self.[](name)
40
+ begin
41
+ const_get name.sub('-','_')
42
+ rescue
43
+ nil
44
+ end
45
+ end
46
+
47
+ end
48
+
49
+ end
data/lib/mtk/event.rb CHANGED
@@ -7,9 +7,6 @@ module MTK
7
7
  # intensity of the note as a value in the range 0.0 - 1.0
8
8
  attr_reader :intensity
9
9
 
10
- # duration of the note in beats (e.g. 1.0 is a quarter note in 4/4 time signatures)
11
- attr_reader :duration
12
-
13
10
  def initialize(intensity, duration)
14
11
  @intensity, @duration = intensity, duration
15
12
  end
@@ -36,11 +33,23 @@ module MTK
36
33
 
37
34
  # intensity scaled to the MIDI range 0-127
38
35
  def velocity
39
- (127 * @intensity).round
36
+ @velocity ||= (127 * @intensity).round
37
+ end
38
+
39
+ # Duration of the Event in beats (e.g. 1.0 is a quarter note in 4/4 time signatures)
40
+ # This is the absolute value of the duration attribute used to construct the object.
41
+ # @see rest?
42
+ def duration
43
+ @abs_duration ||= @duration.abs
44
+ end
45
+
46
+ # By convention, any events with negative durations are considered a rest
47
+ def rest?
48
+ @duration < 0
40
49
  end
41
50
 
42
51
  def duration_in_pulses(pulses_per_beat)
43
- (@duration * pulses_per_beat).round
52
+ @duration_in_pulses ||= (duration * pulses_per_beat).round
44
53
  end
45
54
 
46
55
  def == other
@@ -0,0 +1,114 @@
1
+ module MTK::Helper
2
+
3
+ # Given a method #elements, which returns an Array of elements in the collection,
4
+ # including this module will make the class Enumerable and provide various methods you'd expect from an Array.
5
+ module Collection
6
+ include Enumerable
7
+
8
+ # A mutable array of elements in this collection
9
+ def to_a
10
+ Array.new(elements) # we construct a new array since some including classes make elements be immutable
11
+ end
12
+
13
+ # The number of elements in this collection
14
+ def size
15
+ elements.size
16
+ end
17
+ alias length size
18
+
19
+ def empty?
20
+ elements.nil? or elements.size == 0
21
+ end
22
+
23
+ # The each iterator for providing Enumerable functionality
24
+ def each &block
25
+ elements.each &block
26
+ end
27
+
28
+ # The first element
29
+ def first(n=nil)
30
+ n ? elements.first(n) : elements.first
31
+ end
32
+
33
+ # The last element
34
+ def last(n=nil)
35
+ n ? elements.last(n) : elements.last
36
+ end
37
+
38
+ # The element with the given index
39
+ def [](index)
40
+ elements[index]
41
+ end
42
+
43
+ def repeat(times=2)
44
+ full_repetitions, fractional_repetitions = times.floor, times%1 # split into int and fractional part
45
+ repeated = elements * full_repetitions
46
+ repeated += elements[0...elements.size*fractional_repetitions]
47
+ clone_with repeated
48
+ end
49
+
50
+ def permute
51
+ clone_with elements.shuffle
52
+ end
53
+ alias shuffle permute
54
+
55
+ def rotate(offset=1)
56
+ clone_with elements.rotate(offset)
57
+ end
58
+
59
+ def concat(other)
60
+ other_elements = (other.respond_to? :elements) ? other.elements : other
61
+ clone_with(elements + other_elements)
62
+ end
63
+
64
+ def reverse
65
+ clone_with elements.reverse
66
+ end
67
+ alias retrograde reverse
68
+
69
+ def ==(other)
70
+ if other.respond_to? :elements
71
+ if other.respond_to? :options
72
+ elements == other.elements and @options == other.options
73
+ else
74
+ elements == other.elements
75
+ end
76
+ else
77
+ elements == other
78
+ end
79
+ end
80
+
81
+ # Create a copy of the collection.
82
+ # In order to use this method, the including class must implement .from_a()
83
+ def clone
84
+ clone_with to_a
85
+ end
86
+
87
+ #################################
88
+ private
89
+
90
+ # "clones" the object with the given elements, attempting to maintain any @options
91
+ # This is designed to work with 2 argument constructors: def initialize(elements, options={})
92
+ def clone_with elements
93
+ from_a = self.class.method(:from_a)
94
+ if from_a.arity == -2
95
+ from_a[elements, (@options || {})]
96
+ else
97
+ from_a[elements]
98
+ end
99
+ end
100
+
101
+ end
102
+
103
+ end
104
+
105
+ if not Array.instance_methods.include? :rotate
106
+ # Array#rotate is only available in Ruby 1.9, so we may have to implement it
107
+ class Array
108
+ def rotate(offset=1)
109
+ return self if empty?
110
+ offset %= length
111
+ self[offset..-1]+self[0...offset]
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,85 @@
1
+ module MTK
2
+ module Helper
3
+
4
+ # A helper class for {Sequencer}s.
5
+ # Takes a list of patterns and constructs a list of {Event}s from the next elements in each pattern.
6
+ class EventBuilder
7
+
8
+ DEFAULT_PITCH = MTK::Pitches::C4
9
+ DEFAULT_INTENSITY = MTK::Intensities::f
10
+ DEFAULT_DURATION = 1
11
+
12
+ def initialize(patterns, options={})
13
+ @patterns = patterns
14
+ @options = options
15
+ @max_interval = options.fetch :max_interval, 12
16
+ rewind
17
+ end
18
+
19
+ # Build a list of events from the next element in each {Pattern}
20
+ # @return [Array] an array of events
21
+ def next
22
+ pitches = []
23
+ intensity = @default_intensity
24
+ duration = @default_duration
25
+
26
+ for pattern in @patterns
27
+ element = pattern.next
28
+ case element
29
+ when Pitch then pitches << element
30
+ when PitchSet then pitches += element.pitches
31
+ when PitchClass then pitches += pitches_for_pitch_classes([element], @previous_pitch || @default_pitch)
32
+ when PitchClassSet then pitches += pitches_for_pitch_classes(element, @previous_pitch || @default_pitch)
33
+ else case pattern.type
34
+ when :pitch
35
+ if element.is_a? Numeric
36
+ # pitch interval case
37
+ if @previous_pitches
38
+ pitches += @previous_pitches.map{|pitch| pitch + element }
39
+ else
40
+ pitches << ((@previous_pitch || @default_pitch) + element)
41
+ end
42
+ end
43
+ when :intensity then intensity = element
44
+ when :duration then duration = element
45
+ end
46
+ end
47
+ end
48
+
49
+ if not pitches.empty?
50
+ @previous_pitch = pitches.last
51
+ @previous_pitches = pitches.length > 1 ? pitches : nil
52
+ pitches.map{|pitch| Note(pitch,intensity,duration) }
53
+ else
54
+ nil
55
+ end
56
+ end
57
+
58
+ # Reset the EventBuilder to its initial state
59
+ def rewind
60
+ @default_pitch = @options.fetch :default_pitch, DEFAULT_PITCH
61
+ @default_intensity = @options.fetch :default_intensity, DEFAULT_INTENSITY
62
+ @default_duration = @options.fetch :default_duration, DEFAULT_DURATION
63
+ @previous_pitch = nil
64
+ @previous_pitches = nil
65
+ @patterns.each{|pattern| pattern.rewind }
66
+ end
67
+
68
+ ########################
69
+ private
70
+
71
+ def pitches_for_pitch_classes(pitch_classes, previous_pitch)
72
+ pitches = []
73
+ for pitch_class in pitch_classes
74
+ pitch = previous_pitch.nearest(pitch_class)
75
+ pitch -= 12 if pitch > @default_pitch+@max_interval # keep within max_distance of start (default is one octave)
76
+ pitch += 12 if pitch < @default_pitch-@max_interval
77
+ pitches << pitch
78
+ end
79
+ pitches
80
+ end
81
+
82
+ end
83
+
84
+ end
85
+ end
@@ -1,10 +1,11 @@
1
- module MTK
2
-
1
+ module MTK::Helper
2
+
3
+
3
4
  # Extension for modules that want to define pseudo-constants (constant-like values with lower-case names)
4
5
  module PseudoConstants
5
-
6
- # Define a "constant" as a module method and module function (available both through the module namespace and as a mixin method),
7
- # in order to facility defining constant-like values with lower-case names.
6
+
7
+ # Define a module method and module function (available both through the module namespace and as a mixin method),
8
+ # to provide a constant with a lower-case name.
8
9
  #
9
10
  # @param name [Symbol] the name of the pseudo-constant
10
11
  # @param value [Object] the value of the pseudo-constant
@@ -21,5 +22,5 @@ module MTK
21
22
  end
22
23
 
23
24
  end
24
-
25
+
25
26
  end
@@ -0,0 +1,17 @@
1
+ require 'citrus'
2
+ Citrus.load File.join(File.dirname(__FILE__),'mtk_grammar')
3
+
4
+ module MTK
5
+ module Lang
6
+
7
+ # Parser for the {file:lib/mtk/lang/mtk_grammar.citrus MTK grammar}
8
+ class Grammar
9
+
10
+ def self.parse(syntax, root=:pitch)
11
+ MTK_Grammar.parse(syntax, :root => root).value
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,60 @@
1
+ grammar MTK_Grammar
2
+ include MTK
3
+
4
+ rule pitch_sequence
5
+ ( pitch (space pitch)* ) {
6
+ Pattern.PitchSequence *captures[:pitch].map{|p| p.value }
7
+ }
8
+ end
9
+
10
+ rule pitch
11
+ ( pitch_class int ) {
12
+ Pitch[pitch_class.value, int.value]
13
+ }
14
+ end
15
+
16
+ rule pitch_class
17
+ ( [A-Ga-g] [#b]*2 ) {
18
+ PitchClass[to_s]
19
+ }
20
+ end
21
+
22
+ rule interval
23
+ ( 'P' [1458] | [Mm] [2367] | 'TT' ) {
24
+ Intervals[to_s]
25
+ }
26
+ end
27
+
28
+ rule intensity
29
+ ( ('p'1*3 | 'mp' | 'mf' | 'f'1*3) ('+'|'-')? ) {
30
+ Intensities[to_s]
31
+ }
32
+ end
33
+
34
+ rule duration
35
+ ( [whqesrx] ('.'|'t')* ) {
36
+ Durations[to_s]
37
+ }
38
+ end
39
+
40
+ rule number
41
+ float | int
42
+ end
43
+
44
+ rule float
45
+ ( '-'? [0-9]+ '.' [0-9]+ ) {
46
+ to_f
47
+ }
48
+ end
49
+
50
+ rule int
51
+ ( '-'? [0-9]+ ) {
52
+ to_i
53
+ }
54
+ end
55
+
56
+ rule space
57
+ [\s]+ { nil }
58
+ end
59
+
60
+ end
data/lib/mtk/midi/file.rb CHANGED
@@ -82,24 +82,18 @@ module MTK
82
82
  track = add_track sequence
83
83
  channel = 1
84
84
 
85
- for time, event in timeline
85
+ for time,events in timeline
86
86
  time *= clock_rate
87
- case event
88
- when Note
87
+
88
+ for event in events
89
+ next if event.rest?
90
+
91
+ if event.is_a? Note
89
92
  pitch, velocity = event.pitch, event.velocity
90
93
  add_event track, time => note_on(channel, pitch, velocity)
91
94
  duration = event.duration_in_pulses(clock_rate)
92
95
  add_event track, time+duration => note_off(channel, pitch, velocity)
93
-
94
- when Chord
95
- velocity = event.velocity
96
- duration = event.duration_in_pulses(clock_rate)
97
- for pitch in event.pitches
98
- pitch = pitch.to_i
99
- add_event track, time => note_on(channel, pitch, velocity)
100
- add_event track, time+duration => note_off(channel, pitch, velocity)
101
- end
102
-
96
+ end
103
97
  end
104
98
  end
105
99
  track.recalc_delta_from_times
@@ -160,16 +154,17 @@ module MTK
160
154
 
161
155
  def add_event track, event_hash
162
156
  for time, event in event_hash
163
- event.time_from_start = time
157
+ event.time_from_start = time.round # MIDI file event times must be in whole number pulses (typically 480 or 960 per quarter note)
164
158
  track.events << event
165
159
  event
166
160
  end
167
161
  end
168
162
 
169
163
  end
170
-
171
164
  end
172
165
 
166
+ # Shortcut for MTK::MIDI::File.new
167
+ # @note Only available if you require 'mtk/midi/file'
173
168
  def MIDI_File(f)
174
169
  MIDI::File.new(f)
175
170
  end
@@ -0,0 +1,68 @@
1
+ require 'rubygems'
2
+ require 'jsound'
3
+
4
+ module MTK
5
+ module MIDI
6
+
7
+ # Provides MIDI input for JRuby via the jsound gem
8
+ class JSoundInput
9
+
10
+ def initialize(input_device)
11
+ if input_device.is_a? ::JSound::Midi::Device
12
+ @input = input_device
13
+ else
14
+ @input = ::JSound::Midi::INPUTS.send input_device
15
+ end
16
+ @recorder = ::JSound::Midi::Devices::Recorder.new(false)
17
+ @input >> @recorder
18
+ end
19
+
20
+ def record
21
+ @input.open
22
+ @recorder.clear
23
+ @recorder.start
24
+ end
25
+
26
+ def stop
27
+ @recorder.stop
28
+ @input.close
29
+ end
30
+
31
+ def to_timeline(options={})
32
+ bpm = options.fetch :bmp, 120
33
+ beats_per_second = bpm.to_f/60
34
+ timeline = Timeline.new
35
+ note_ons = {}
36
+ start = nil
37
+
38
+ for message,time in @recorder.messages_with_timestamps
39
+ start = time unless start
40
+ time -= start
41
+ time /= beats_per_second
42
+
43
+ case message.type
44
+ when :note_on
45
+ note_ons[message.pitch] = [message,time]
46
+
47
+ when :note_off
48
+ if note_ons.has_key? message.pitch
49
+ note_on, start_time = note_ons[message.pitch]
50
+ duration = time - start_time
51
+ note = Note.from_midi note_on.pitch, note_on.velocity, duration
52
+ timeline.add time,note
53
+ end
54
+
55
+ else timeline.add time,message
56
+ end
57
+ end
58
+
59
+ timeline.quantize! options[:quantize] if options.key? :quantize
60
+ timeline.shift_to! options[:shift_to] if options.key? :shift_to
61
+
62
+ timeline
63
+ end
64
+
65
+ end
66
+ end
67
+ end
68
+
@@ -0,0 +1,80 @@
1
+ require 'rubygems'
2
+ require 'jsound'
3
+ require 'gamelan'
4
+
5
+ module MTK
6
+ module MIDI
7
+
8
+ # Provides MIDI output for JRuby via the jsound gem
9
+ class JSoundOutput
10
+
11
+ attr_reader :device
12
+
13
+ def initialize(output, options={})
14
+ if output.is_a? ::JSound::Midi::Device
15
+ @device = output
16
+ else
17
+ @device = ::JSound::Midi::OUTPUTS.send output
18
+ end
19
+
20
+ @generator = ::JSound::Midi::Devices::Generator.new
21
+ if options[:monitor]
22
+ @monitor = ::JSound::Midi::Devices::Monitor.new
23
+ @generator >> [@monitor, @device]
24
+ else
25
+ @generator >> @device
26
+ end
27
+ @device.open
28
+ end
29
+
30
+ def play(timeline, options={})
31
+ scheduler_rate = options.fetch :scheduler_rate, 500 # default: 500 Hz
32
+ trailing_buffer = options.fetch :trailing_buffer, 2 # default: continue playing for 2 beats after the end of the timeline
33
+ in_background = options.fetch :background, false # default: don't run in background Thread
34
+ bpm = options.fetch :bmp, 120 # default: 120 beats per minute
35
+
36
+ @scheduler = Gamelan::Scheduler.new :tempo => bpm, :rate => scheduler_rate
37
+
38
+ for time,events in timeline
39
+ for event in events
40
+ case event
41
+ when Note
42
+ pitch, velocity, duration = event.to_midi
43
+ at time, note_on(pitch,velocity)
44
+ time += duration
45
+ at time, note_off(pitch,velocity)
46
+ end
47
+ end
48
+ end
49
+
50
+ end_time = timeline.times.last + trailing_buffer
51
+ @scheduler.at(end_time) { @scheduler.stop }
52
+
53
+ thread = @scheduler.run
54
+ thread.join if not in_background
55
+ end
56
+
57
+ ######################
58
+ private
59
+
60
+ # It's necessary to generate the events through methods and lambdas like this to create closures.
61
+ # Otherwise when the @generator methods are called, they might not be passed the values you expected.
62
+ # I suspect this may not a problem in MRI ruby, but I'm having trouble in JRuby
63
+ # (pitch and velocity were always the last scheduled values)
64
+
65
+ def note_on(pitch, velocity)
66
+ lambda { @generator.note_on(pitch, velocity) }
67
+ end
68
+
69
+ def note_off(pitch, velocity)
70
+ lambda { @generator.note_off(pitch, velocity) }
71
+ end
72
+
73
+ def at time, block
74
+ @scheduler.at(time) { block.call }
75
+ end
76
+
77
+ end
78
+ end
79
+ end
80
+
data/lib/mtk/note.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module MTK
2
2
 
3
- # A musical #{Event} defined by a {Pitch}, intensity, and duration
3
+ # A musical {Event} defined by a {Pitch}, intensity, and duration
4
4
  class Note < Event
5
5
 
6
6
  # frequency of the note as a Pitch
@@ -15,18 +15,26 @@ module MTK
15
15
  new hash[:pitch], hash[:intensity], hash[:duration]
16
16
  end
17
17
 
18
+ def to_hash
19
+ super.merge({ :pitch => @pitch })
20
+ end
21
+
18
22
  def self.from_midi(pitch, velocity, beats)
19
23
  new Pitches::PITCHES[pitch], velocity/127.0, beats
20
24
  end
21
25
 
22
- def to_hash
23
- super.merge({ :pitch => @pitch })
26
+ def to_midi
27
+ [pitch.to_i, velocity, duration]
24
28
  end
25
29
 
26
30
  def transpose(interval)
27
31
  self.class.new(@pitch+interval, @intensity, @duration)
28
32
  end
29
33
 
34
+ def invert(around_pitch)
35
+ self.class.new(@pitch.invert(around_pitch), @intensity, @duration)
36
+ end
37
+
30
38
  def == other
31
39
  super and other.respond_to? :pitch and @pitch == other.pitch
32
40
  end
@@ -41,4 +49,15 @@ module MTK
41
49
 
42
50
  end
43
51
 
52
+ # Construct a {Note} from any supported type
53
+ def Note(*anything)
54
+ anything = anything.first if anything.size == 1
55
+ case anything
56
+ when Array then Note.new(*anything)
57
+ when Note then anything
58
+ else raise "Note doesn't understand #{anything.class}"
59
+ end
60
+ end
61
+ module_function :Note
62
+
44
63
  end