mtk 0.0.2 → 0.0.3

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