mtk 0.0.3.3 → 0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/INTRO.md +63 -31
- data/Rakefile +3 -1
- data/bin/mtk +75 -32
- data/examples/drum_pattern.rb +2 -2
- data/examples/dynamic_pattern.rb +1 -1
- data/examples/helpers/output_selector.rb +71 -0
- data/examples/notation.rb +5 -1
- data/examples/tone_row_melody.rb +1 -1
- data/lib/mtk.rb +1 -0
- data/lib/mtk/core/duration.rb +18 -3
- data/lib/mtk/core/intensity.rb +5 -3
- data/lib/mtk/core/interval.rb +21 -14
- data/lib/mtk/core/pitch.rb +2 -0
- data/lib/mtk/core/pitch_class.rb +6 -3
- data/lib/mtk/events/event.rb +2 -1
- data/lib/mtk/events/note.rb +1 -1
- data/lib/mtk/events/parameter.rb +1 -0
- data/lib/mtk/events/rest.rb +85 -0
- data/lib/mtk/events/timeline.rb +6 -2
- data/lib/mtk/io/jsound_input.rb +9 -3
- data/lib/mtk/io/midi_file.rb +38 -2
- data/lib/mtk/io/midi_input.rb +1 -1
- data/lib/mtk/io/midi_output.rb +95 -4
- data/lib/mtk/io/unimidi_input.rb +7 -3
- data/lib/mtk/lang/durations.rb +31 -26
- data/lib/mtk/lang/intensities.rb +29 -30
- data/lib/mtk/lang/intervals.rb +108 -41
- data/lib/mtk/lang/mtk_grammar.citrus +14 -4
- data/lib/mtk/lang/parser.rb +10 -5
- data/lib/mtk/lang/pitch_classes.rb +45 -17
- data/lib/mtk/lang/pitches.rb +169 -32
- data/lib/mtk/lang/tutorial.rb +279 -0
- data/lib/mtk/lang/tutorial_lesson.rb +87 -0
- data/lib/mtk/sequencers/event_builder.rb +29 -8
- data/spec/mtk/core/duration_spec.rb +14 -1
- data/spec/mtk/core/intensity_spec.rb +1 -1
- data/spec/mtk/events/event_spec.rb +10 -16
- data/spec/mtk/events/note_spec.rb +3 -3
- data/spec/mtk/events/rest_spec.rb +184 -0
- data/spec/mtk/events/timeline_spec.rb +5 -1
- data/spec/mtk/io/midi_file_spec.rb +13 -2
- data/spec/mtk/io/midi_output_spec.rb +42 -9
- data/spec/mtk/lang/durations_spec.rb +5 -5
- data/spec/mtk/lang/intensities_spec.rb +5 -5
- data/spec/mtk/lang/intervals_spec.rb +139 -13
- data/spec/mtk/lang/parser_spec.rb +65 -25
- data/spec/mtk/lang/pitch_classes_spec.rb +0 -11
- data/spec/mtk/lang/pitches_spec.rb +0 -15
- data/spec/mtk/patterns/chain_spec.rb +7 -7
- data/spec/mtk/patterns/for_each_spec.rb +2 -2
- data/spec/mtk/sequencers/event_builder_spec.rb +49 -17
- metadata +12 -22
data/examples/notation.rb
CHANGED
@@ -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)
|
data/examples/tone_row_melody.rb
CHANGED
@@ -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,
|
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
data/lib/mtk/core/duration.rb
CHANGED
@@ -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
|
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
|
-
'
|
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+)?)?([
|
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
|
data/lib/mtk/core/intensity.rb
CHANGED
@@ -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 {
|
10
|
-
NAMES = %w[ppp pp p mp mf
|
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
|
-
'
|
20
|
+
'f' => 0.75,
|
19
21
|
'ff' => 0.875,
|
20
22
|
'fff' => 1.0
|
21
23
|
}
|
data/lib/mtk/core/interval.rb
CHANGED
@@ -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
|
16
|
-
%w( m2
|
17
|
-
%w( M2
|
18
|
-
%w( m3
|
19
|
-
%w( M3
|
20
|
-
%w( P4
|
21
|
-
%w( TT
|
22
|
-
%w( P5
|
23
|
-
%w( m6
|
24
|
-
%w( M6
|
25
|
-
%w( m7
|
26
|
-
%w( M7
|
27
|
-
%w( P8
|
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
|
|
data/lib/mtk/core/pitch.rb
CHANGED
data/lib/mtk/core/pitch_class.rb
CHANGED
@@ -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
|
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 #
|
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
|
|
data/lib/mtk/events/event.rb
CHANGED
@@ -51,7 +51,7 @@ module MTK
|
|
51
51
|
end
|
52
52
|
|
53
53
|
def to_h
|
54
|
-
hash = {:
|
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?
|
data/lib/mtk/events/note.rb
CHANGED
data/lib/mtk/events/parameter.rb
CHANGED
@@ -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
|
data/lib/mtk/events/timeline.rb
CHANGED
@@ -212,8 +212,12 @@ module MTK
|
|
212
212
|
def to_s
|
213
213
|
times = self.times
|
214
214
|
last = times.last
|
215
|
-
|
216
|
-
|
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
|
data/lib/mtk/io/jsound_input.rb
CHANGED
@@ -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
|
-
|
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([
|
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
|
|
data/lib/mtk/io/midi_file.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|