mtk 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (147) hide show
  1. data/.yardopts +3 -2
  2. data/DEVELOPMENT_NOTES.md +114 -0
  3. data/INTRO.md +64 -8
  4. data/LICENSE.txt +1 -1
  5. data/README.md +31 -102
  6. data/Rakefile +56 -18
  7. data/bin/mtk +215 -0
  8. data/examples/crescendo.rb +5 -5
  9. data/examples/drum_pattern1.rb +23 -0
  10. data/examples/dynamic_pattern.rb +8 -11
  11. data/examples/gets_and_play.rb +26 -0
  12. data/examples/notation.rb +22 -0
  13. data/examples/play_midi.rb +8 -10
  14. data/examples/random_tone_row.rb +2 -2
  15. data/examples/syntax_to_midi.rb +28 -0
  16. data/examples/test_output.rb +8 -0
  17. data/examples/tone_row_melody.rb +6 -6
  18. data/lib/mtk.rb +52 -40
  19. data/lib/mtk/chord.rb +55 -0
  20. data/lib/mtk/constants/durations.rb +57 -0
  21. data/lib/mtk/constants/intensities.rb +61 -0
  22. data/lib/mtk/constants/intervals.rb +73 -0
  23. data/lib/mtk/constants/pitch_classes.rb +29 -0
  24. data/lib/mtk/constants/pitches.rb +52 -0
  25. data/lib/mtk/duration.rb +211 -0
  26. data/lib/mtk/events/event.rb +119 -0
  27. data/lib/mtk/events/note.rb +112 -0
  28. data/lib/mtk/events/parameter.rb +54 -0
  29. data/lib/mtk/helpers/collection.rb +164 -0
  30. data/lib/mtk/helpers/convert.rb +36 -0
  31. data/lib/mtk/helpers/lilypond.rb +162 -0
  32. data/lib/mtk/helpers/output_selector.rb +67 -0
  33. data/lib/mtk/helpers/pitch_collection.rb +23 -0
  34. data/lib/mtk/helpers/pseudo_constants.rb +26 -0
  35. data/lib/mtk/intensity.rb +156 -0
  36. data/lib/mtk/interval.rb +155 -0
  37. data/lib/mtk/lang/mtk_grammar.citrus +190 -13
  38. data/lib/mtk/lang/parser.rb +29 -0
  39. data/lib/mtk/melody.rb +94 -0
  40. data/lib/mtk/midi/dls_synth_device.rb +144 -0
  41. data/lib/mtk/midi/dls_synth_output.rb +62 -0
  42. data/lib/mtk/midi/file.rb +67 -32
  43. data/lib/mtk/midi/input.rb +97 -0
  44. data/lib/mtk/midi/jsound_input.rb +36 -17
  45. data/lib/mtk/midi/jsound_output.rb +48 -46
  46. data/lib/mtk/midi/output.rb +195 -0
  47. data/lib/mtk/midi/unimidi_input.rb +117 -0
  48. data/lib/mtk/midi/unimidi_output.rb +121 -0
  49. data/lib/mtk/{_numeric_extensions.rb → numeric_extensions.rb} +12 -0
  50. data/lib/mtk/patterns/chain.rb +49 -0
  51. data/lib/mtk/{pattern → patterns}/choice.rb +14 -8
  52. data/lib/mtk/patterns/cycle.rb +18 -0
  53. data/lib/mtk/patterns/for_each.rb +71 -0
  54. data/lib/mtk/patterns/function.rb +39 -0
  55. data/lib/mtk/{pattern → patterns}/lines.rb +11 -17
  56. data/lib/mtk/{pattern → patterns}/palindrome.rb +11 -8
  57. data/lib/mtk/patterns/pattern.rb +171 -0
  58. data/lib/mtk/patterns/sequence.rb +20 -0
  59. data/lib/mtk/pitch.rb +7 -6
  60. data/lib/mtk/pitch_class.rb +124 -46
  61. data/lib/mtk/pitch_class_set.rb +58 -35
  62. data/lib/mtk/sequencers/event_builder.rb +131 -0
  63. data/lib/mtk/sequencers/legato_sequencer.rb +24 -0
  64. data/lib/mtk/sequencers/rhythmic_sequencer.rb +28 -0
  65. data/lib/mtk/{sequencer/abstract_sequencer.rb → sequencers/sequencer.rb} +37 -11
  66. data/lib/mtk/{sequencer → sequencers}/step_sequencer.rb +4 -4
  67. data/lib/mtk/timeline.rb +39 -22
  68. data/lib/mtk/variable.rb +32 -0
  69. data/spec/mtk/chord_spec.rb +83 -0
  70. data/spec/mtk/{_constants → constants}/durations_spec.rb +12 -41
  71. data/spec/mtk/{_constants → constants}/intensities_spec.rb +13 -37
  72. data/spec/mtk/{_constants → constants}/intervals_spec.rb +14 -32
  73. data/spec/mtk/{_constants → constants}/pitch_classes_spec.rb +8 -4
  74. data/spec/mtk/{_constants → constants}/pitches_spec.rb +5 -1
  75. data/spec/mtk/duration_spec.rb +372 -0
  76. data/spec/mtk/events/event_spec.rb +234 -0
  77. data/spec/mtk/events/note_spec.rb +174 -0
  78. data/spec/mtk/events/parameter_spec.rb +220 -0
  79. data/spec/mtk/{helper → helpers}/collection_spec.rb +86 -3
  80. data/spec/mtk/{helper → helpers}/pseudo_constants_spec.rb +2 -2
  81. data/spec/mtk/intensity_spec.rb +289 -0
  82. data/spec/mtk/interval_spec.rb +265 -0
  83. data/spec/mtk/lang/parser_spec.rb +597 -0
  84. data/spec/mtk/melody_spec.rb +223 -0
  85. data/spec/mtk/midi/file_spec.rb +16 -16
  86. data/spec/mtk/midi/jsound_input_spec.rb +11 -0
  87. data/spec/mtk/midi/jsound_output_spec.rb +11 -0
  88. data/spec/mtk/midi/output_spec.rb +102 -0
  89. data/spec/mtk/midi/unimidi_input_spec.rb +11 -0
  90. data/spec/mtk/midi/unimidi_output_spec.rb +11 -0
  91. data/spec/mtk/{_numeric_extensions_spec.rb → numeric_extensions_spec.rb} +1 -0
  92. data/spec/mtk/patterns/chain_spec.rb +110 -0
  93. data/spec/mtk/{pattern → patterns}/choice_spec.rb +20 -30
  94. data/spec/mtk/{pattern → patterns}/cycle_spec.rb +25 -35
  95. data/spec/mtk/patterns/for_each_spec.rb +136 -0
  96. data/spec/mtk/{pattern → patterns}/function_spec.rb +17 -30
  97. data/spec/mtk/{pattern → patterns}/lines_spec.rb +11 -27
  98. data/spec/mtk/{pattern → patterns}/palindrome_spec.rb +13 -29
  99. data/spec/mtk/patterns/pattern_spec.rb +132 -0
  100. data/spec/mtk/patterns/sequence_spec.rb +203 -0
  101. data/spec/mtk/pitch_class_set_spec.rb +23 -21
  102. data/spec/mtk/pitch_class_spec.rb +151 -39
  103. data/spec/mtk/pitch_spec.rb +22 -1
  104. data/spec/mtk/sequencers/event_builder_spec.rb +245 -0
  105. data/spec/mtk/sequencers/legato_sequencer_spec.rb +45 -0
  106. data/spec/mtk/sequencers/rhythmic_sequencer_spec.rb +84 -0
  107. data/spec/mtk/sequencers/sequencer_spec.rb +215 -0
  108. data/spec/mtk/{sequencer → sequencers}/step_sequencer_spec.rb +35 -13
  109. data/spec/mtk/timeline_spec.rb +109 -16
  110. data/spec/mtk/variable_spec.rb +52 -0
  111. data/spec/spec_coverage.rb +2 -0
  112. data/spec/spec_helper.rb +3 -0
  113. metadata +188 -91
  114. data/lib/mtk/_constants/durations.rb +0 -80
  115. data/lib/mtk/_constants/intensities.rb +0 -81
  116. data/lib/mtk/_constants/intervals.rb +0 -85
  117. data/lib/mtk/_constants/pitch_classes.rb +0 -35
  118. data/lib/mtk/_constants/pitches.rb +0 -49
  119. data/lib/mtk/event.rb +0 -70
  120. data/lib/mtk/helper/collection.rb +0 -114
  121. data/lib/mtk/helper/event_builder.rb +0 -85
  122. data/lib/mtk/helper/pseudo_constants.rb +0 -26
  123. data/lib/mtk/lang/grammar.rb +0 -17
  124. data/lib/mtk/note.rb +0 -63
  125. data/lib/mtk/pattern/abstract_pattern.rb +0 -132
  126. data/lib/mtk/pattern/cycle.rb +0 -51
  127. data/lib/mtk/pattern/enumerator.rb +0 -26
  128. data/lib/mtk/pattern/function.rb +0 -46
  129. data/lib/mtk/pattern/sequence.rb +0 -30
  130. data/lib/mtk/pitch_set.rb +0 -84
  131. data/lib/mtk/sequencer/rhythmic_sequencer.rb +0 -29
  132. data/lib/mtk/transform/invertible.rb +0 -15
  133. data/lib/mtk/transform/mappable.rb +0 -18
  134. data/lib/mtk/transform/set_theory_operations.rb +0 -34
  135. data/lib/mtk/transform/transposable.rb +0 -14
  136. data/spec/mtk/event_spec.rb +0 -139
  137. data/spec/mtk/helper/event_builder_spec.rb +0 -92
  138. data/spec/mtk/lang/grammar_spec.rb +0 -100
  139. data/spec/mtk/note_spec.rb +0 -115
  140. data/spec/mtk/pattern/abstract_pattern_spec.rb +0 -45
  141. data/spec/mtk/pattern/note_cycle_spec.rb.bak +0 -116
  142. data/spec/mtk/pattern/pitch_cycle_spec.rb.bak +0 -47
  143. data/spec/mtk/pattern/pitch_sequence_spec.rb.bak +0 -37
  144. data/spec/mtk/pattern/sequence_spec.rb +0 -151
  145. data/spec/mtk/pitch_set_spec.rb +0 -198
  146. data/spec/mtk/sequencer/abstract_sequencer_spec.rb +0 -159
  147. data/spec/mtk/sequencer/rhythmic_sequencer_spec.rb +0 -49
@@ -0,0 +1,164 @@
1
+ module MTK
2
+ module Helpers
3
+
4
+ # Given a method #elements, which returns an Array of elements in the collection,
5
+ # including this module will make the class Enumerable and provide various methods you'd expect from an Array.
6
+ module Collection
7
+ include Enumerable
8
+
9
+ # A mutable array of elements in this collection
10
+ def to_a
11
+ Array.new(elements) # we construct a new array since some including classes make elements be immutable
12
+ end
13
+
14
+ # The number of elements in this collection
15
+ def size
16
+ elements.size
17
+ end
18
+ alias length size
19
+
20
+ def empty?
21
+ elements.nil? or elements.size == 0
22
+ end
23
+
24
+ # The each iterator for providing Enumerable functionality
25
+ def each &block
26
+ elements.each(&block)
27
+ end
28
+
29
+ # the original Enumerable#map implementation, which returns an Array
30
+ alias enumerable_map map
31
+
32
+ # the overriden #map implementation, which returns an object of the same type
33
+ def map &block
34
+ clone_with enumerable_map(&block)
35
+ end
36
+
37
+ # The first element
38
+ def first(n=nil)
39
+ n ? elements.first(n) : elements.first
40
+ end
41
+
42
+ # The last element
43
+ def last(n=nil)
44
+ n ? elements.last(n) : elements.last
45
+ end
46
+
47
+ # The element with the given index
48
+ def [](index)
49
+ elements[index]
50
+ end
51
+
52
+ def repeat(times=2)
53
+ full_repetitions, fractional_repetitions = times.floor, times%1 # split into int and fractional part
54
+ repeated = elements * full_repetitions
55
+ repeated += elements[0...elements.size*fractional_repetitions]
56
+ clone_with repeated
57
+ end
58
+
59
+ def permute
60
+ clone_with elements.shuffle
61
+ end
62
+ alias shuffle permute
63
+
64
+ def rotate(offset=1)
65
+ clone_with elements.rotate(offset)
66
+ end
67
+
68
+ def concat(other)
69
+ other_elements = (other.respond_to? :elements) ? other.elements : other
70
+ clone_with(elements + other_elements)
71
+ end
72
+
73
+ def reverse
74
+ clone_with elements.reverse
75
+ end
76
+ alias retrograde reverse
77
+
78
+
79
+ # Partition the collection into an Array of sub-collections.
80
+ #
81
+ # With a Numeric argument: partition the elements into collections of the given size (plus whatever's left over).
82
+ #
83
+ # With an Array argument: partition the elements into collections of the given sizes.
84
+ #
85
+ # Otherwise if a block is given: partition the elements into collections with the same block return value.
86
+ #
87
+ def partition(arg=nil, &block)
88
+ partitions = nil
89
+ case arg
90
+ when Numeric
91
+ partitions = self.each_slice(arg)
92
+
93
+ when Enumerable
94
+ partitions = []
95
+ items, sizes = self.to_enum, arg.to_enum
96
+ group = []
97
+ size = sizes.next
98
+ loop do
99
+ item = items.next
100
+ if group.size < size
101
+ group << item
102
+ else
103
+ partitions << group
104
+ group = []
105
+ size = sizes.next
106
+ group << item
107
+ end
108
+ end
109
+ partitions << group unless group.empty?
110
+
111
+ else
112
+ if block
113
+ group = Hash.new{|h,k| h[k] = [] }
114
+ if block.arity == 2
115
+ self.each_with_index{|item,index| group[block[item,index]] << item }
116
+ else
117
+ self.each{|item| group[block[item]] << item }
118
+ end
119
+ partitions = group.values
120
+ end
121
+ end
122
+
123
+ if partitions
124
+ partitions.map{|p| self.class.from_a(p) }
125
+ else
126
+ self
127
+ end
128
+ end
129
+
130
+ def ==(other)
131
+ if other.respond_to? :elements
132
+ if other.respond_to? :options
133
+ elements == other.elements and @options == other.options
134
+ else
135
+ elements == other.elements
136
+ end
137
+ else
138
+ elements == other
139
+ end
140
+ end
141
+
142
+ # Create a copy of the collection.
143
+ # In order to use this method, the including class must implement .from_a()
144
+ def clone
145
+ clone_with to_a
146
+ end
147
+
148
+ #################################
149
+ private
150
+
151
+ # "clones" the object with the given elements, attempting to maintain any @options
152
+ # This is designed to work with 2 argument constructors: def initialize(elements, options=default)
153
+ def clone_with elements
154
+ from_a = self.class.method(:from_a)
155
+ if @options and from_a.arity == -2
156
+ from_a[elements, @options]
157
+ else
158
+ from_a[elements]
159
+ end
160
+ end
161
+
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,36 @@
1
+ module MTK
2
+ module Helpers
3
+
4
+ module Convert
5
+
6
+ def to_pitch_classes(*anything)
7
+ anything = anything.first if anything.length == 1
8
+ if anything.respond_to? :to_pitch_classes
9
+ anything.to_pitch_classes
10
+ else
11
+ case anything
12
+ when Enumerable then anything.map{|item| PitchClass(item) }
13
+ else [PitchClass(anything)]
14
+ end
15
+ end
16
+ end
17
+ module_function :to_pitch_classes
18
+
19
+
20
+ def to_pitches(*anything)
21
+ anything = anything.first if anything.length == 1
22
+ if anything.respond_to? :to_pitches
23
+ anything.to_pitches
24
+ else
25
+ case anything
26
+ when Enumerable then anything.map{|item| Pitch(item) }
27
+ else [Pitch(anything)]
28
+ end
29
+ end
30
+ end
31
+ module_function :to_pitches
32
+
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,162 @@
1
+ require 'tempfile'
2
+ require 'tmpdir'
3
+ require 'fileutils'
4
+
5
+ module MTK
6
+ module Helpers
7
+
8
+ # Uses {Timeline}s to generates music notation graphics with {http://lilypond.org/ Lilypond}.
9
+ # @note This class is optional and only available if you require 'mtk/helpers/lilypond'.
10
+ # @note To make notation graphics, {http://lilypond.org/download.html Lilypond} must be installed
11
+ # and you must follow the "Running on the command-line" instructions (found on the download page for
12
+ # your operating system). If the lilypond command is not on your PATH, set the environment variable LILYPOND_PATH
13
+ class Lilypond
14
+
15
+ LILYPOND_PATH = ENV['LILYPOND_PATH'] || 'lilypond'
16
+
17
+ VALID_FORMATS = %w( png pdf ps )
18
+
19
+
20
+ def initialize(file, options={})
21
+ @file = file
22
+ @options = options
23
+
24
+ @format = File.extname(file)[1..-1].downcase
25
+ raise ArgumentError.new("Invalid file format '#{@format}'") unless VALID_FORMATS.include? @format
26
+
27
+ @dpi = options[:dpi]
28
+ end
29
+
30
+ def self.open(file, options={})
31
+ new(file,options)
32
+ end
33
+
34
+
35
+ def write(timeline)
36
+ lilypond_syntax = syntax_for_timeline(timeline)
37
+ puts lilypond_syntax
38
+ puts "_____________________________"
39
+ Tempfile.open('mtk_lilypond') do |lilypond_file|
40
+ Dir.mktmpdir do |tmpdir|
41
+ # use the directory...
42
+ #open("#{dir}/foo", "w") { ... }
43
+
44
+ lilypond_file.write(lilypond_syntax)
45
+ lilypond_file.flush
46
+
47
+ cmd = ['lilypond', '-dbackend=eps', "-f#{@format}", "--output=\"#{tmpdir}\""]
48
+ cmd << "-dresolution=#{@dpi}" if @dpi
49
+ cmd << lilypond_file.path
50
+ cmd = cmd.join(' ')
51
+
52
+ puts cmd if $DEBUG
53
+ lilypond_command_output = `#{cmd}`
54
+ puts lilypond_command_output if $DEBUG
55
+
56
+ output_file = Dir["#{tmpdir}/*.#{@format}"].first
57
+
58
+ FileUtils.cp output_file, @file
59
+
60
+ puts "Wrote #{@file}"
61
+ end
62
+ end
63
+ end
64
+
65
+
66
+ ########################
67
+ private
68
+
69
+ QUANTIZATION_INTERVAL = 0.0625 # 64th note granularity
70
+
71
+ SYNTAX_PREFIX = '
72
+ \language "english"
73
+ \paper{
74
+ oddFooterMarkup=##f
75
+ oddHeaderMarkup=##f
76
+ }
77
+ \new PianoStaff {
78
+ \autochange {
79
+ '
80
+
81
+ SYNTAX_SUFFIX = '
82
+ }
83
+ }
84
+ '
85
+
86
+ def syntax_for_timeline(timeline)
87
+ quantized_timeline = timeline.flatten.quantize(QUANTIZATION_INTERVAL)
88
+ last_time = 0
89
+ last_duration = 0
90
+
91
+ s = ''
92
+ s << SYNTAX_PREFIX
93
+
94
+ for time,events in quantized_timeline
95
+
96
+ # handle rests between notes
97
+ delta = time - last_time
98
+ if delta > last_duration
99
+ s << 'r'+syntax_for_duration(delta - last_duration)
100
+ s << ' '
101
+ end
102
+
103
+ notes = events.find_all{|event| event.type == :note }
104
+
105
+ if notes.length > 1
106
+ # a chord
107
+ s << '<'
108
+ total_duration = 0
109
+ for note in notes
110
+ total_duration += note.duration
111
+ s << syntax_for_pitch(note.pitch)
112
+ s << ' '
113
+ end
114
+ s << '>'
115
+ duration = total_duration.to_f/notes.length
116
+ s << syntax_for_duration(duration)
117
+
118
+ else # a single note
119
+ note = notes.first
120
+ s << syntax_for_pitch(note.pitch)
121
+ duration = note.duration
122
+ s << syntax_for_duration(duration)
123
+ end
124
+
125
+ last_time = time
126
+ last_duration = duration
127
+ s << ' '
128
+ end
129
+
130
+ s << SYNTAX_SUFFIX
131
+ s
132
+ end
133
+
134
+ def syntax_for_pitch(pitch)
135
+ syntax = pitch.pitch_class.name.downcase
136
+ if syntax.length > 0
137
+ syntax = syntax[0] + syntax[1..-1].gsub('b','f') # .gsub('#','s') pitch class names never have '#'
138
+ end
139
+ oct = pitch.octave
140
+ while oct > 3
141
+ syntax << "'"
142
+ oct -= 1
143
+ end
144
+ while oct < 3
145
+ syntax << ","
146
+ oct += 1
147
+ end
148
+ syntax
149
+ end
150
+
151
+ def syntax_for_duration(duration)
152
+ # TODO: handle dots, triplets, and ties of arbitrary durations
153
+ duration = MTK::Timeline.quantize_time(duration.to_f.abs, QUANTIZATION_INTERVAL)
154
+ syntax = (4.0/duration).round
155
+ syntax = 1 if syntax < 1
156
+ syntax.to_s
157
+ end
158
+
159
+ end
160
+
161
+ end
162
+ end
@@ -0,0 +1,67 @@
1
+ require 'mtk/midi/output'
2
+
3
+ module MTK
4
+ module Helpers
5
+
6
+ # Optional class for loading the preferred platform-specific implementation of an output,
7
+ # and methods to assist with selecting an output.
8
+ class OutputSelector
9
+
10
+ class << self
11
+
12
+ def output
13
+ MTK::MIDI::Output
14
+ end
15
+
16
+ # Look for an output by name using case insensitive matching,
17
+ # treating underscore like either an underscore or whitespace
18
+ def search output_name_pattern
19
+ output.find_by_name(/#{output_name_pattern.to_s.sub '_','(_|\\s+)'}/i)
20
+ end
21
+
22
+ # Command line interface to list output choices and select an output.
23
+ def prompt_for_output
24
+ devices_by_name = output.devices_by_name
25
+ names_by_number = {}
26
+
27
+ puts "Available MIDI outputs:"
28
+ devices_by_name.keys.each_with_index do |name,index|
29
+ number = index+1
30
+ names_by_number[number] = name
31
+ puts " #{number} => #{name}"
32
+ end
33
+
34
+ print "Enter the number of the output to test: "
35
+ device = nil
36
+ loop do
37
+ begin
38
+ number = STDIN.gets.to_i
39
+ name = names_by_number[number]
40
+ device = devices_by_name[name]
41
+ return output.open(device) if device
42
+ rescue
43
+ if $DEBUG
44
+ puts $!
45
+ puts $!.backtrace
46
+ end
47
+ # keep looping
48
+ end
49
+ print "Invalid input. Enter a number listed above: "
50
+ end
51
+ end
52
+
53
+
54
+ def ensure_output name=nil
55
+ output = nil
56
+ if name
57
+ output = search name
58
+ puts "Output '#{name}' not found." unless output
59
+ end
60
+ output || prompt_for_output
61
+ end
62
+
63
+ end
64
+
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,23 @@
1
+ module MTK
2
+ module Helpers
3
+
4
+ # An extension to {Collection}, which provides additional transformations for pitch-like collections.
5
+ #
6
+ module PitchCollection
7
+ include Collection
8
+
9
+ # Transpose all elements upward by the given interval
10
+ # @param interval_in_semitones [Numeric] an interval in semitones
11
+ def transpose interval_in_semitones
12
+ map{|elem| elem + interval_in_semitones }
13
+ end
14
+
15
+ # Invert all elements around the given inversion point
16
+ # @param inversion_point [Numeric] the value around which all elements will be inverted (defaults to the first element in the collection)
17
+ def invert(inversion_point=first)
18
+ map{|elem| elem.invert(inversion_point) }
19
+ end
20
+
21
+ end
22
+ end
23
+ end