mtk 0.0.3.3 → 0.4
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.
- 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
|