mtk 0.0.1

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 (45) hide show
  1. data/README.md +52 -0
  2. data/Rakefile +31 -0
  3. data/lib/mtk/chord.rb +47 -0
  4. data/lib/mtk/constants/dynamics.rb +56 -0
  5. data/lib/mtk/constants/intervals.rb +76 -0
  6. data/lib/mtk/constants/pitch_classes.rb +18 -0
  7. data/lib/mtk/constants/pitches.rb +24 -0
  8. data/lib/mtk/constants/pseudo_constants.rb +25 -0
  9. data/lib/mtk/event.rb +61 -0
  10. data/lib/mtk/midi/file.rb +179 -0
  11. data/lib/mtk/note.rb +44 -0
  12. data/lib/mtk/numeric_extensions.rb +61 -0
  13. data/lib/mtk/pattern/choice.rb +21 -0
  14. data/lib/mtk/pattern/note_sequence.rb +60 -0
  15. data/lib/mtk/pattern/pitch_sequence.rb +22 -0
  16. data/lib/mtk/pattern/sequence.rb +65 -0
  17. data/lib/mtk/patterns.rb +4 -0
  18. data/lib/mtk/pitch.rb +112 -0
  19. data/lib/mtk/pitch_class.rb +113 -0
  20. data/lib/mtk/pitch_class_set.rb +106 -0
  21. data/lib/mtk/pitch_set.rb +95 -0
  22. data/lib/mtk/timeline.rb +160 -0
  23. data/lib/mtk/util/mappable.rb +14 -0
  24. data/lib/mtk.rb +36 -0
  25. data/spec/mtk/chord_spec.rb +74 -0
  26. data/spec/mtk/constants/dynamics_spec.rb +94 -0
  27. data/spec/mtk/constants/intervals_spec.rb +140 -0
  28. data/spec/mtk/constants/pitch_classes_spec.rb +35 -0
  29. data/spec/mtk/constants/pitches_spec.rb +23 -0
  30. data/spec/mtk/event_spec.rb +120 -0
  31. data/spec/mtk/midi/file_spec.rb +208 -0
  32. data/spec/mtk/note_spec.rb +65 -0
  33. data/spec/mtk/numeric_extensions_spec.rb +102 -0
  34. data/spec/mtk/pattern/choice_spec.rb +21 -0
  35. data/spec/mtk/pattern/note_sequence_spec.rb +121 -0
  36. data/spec/mtk/pattern/pitch_sequence_spec.rb +47 -0
  37. data/spec/mtk/pattern/sequence_spec.rb +54 -0
  38. data/spec/mtk/pitch_class_set_spec.rb +103 -0
  39. data/spec/mtk/pitch_class_spec.rb +165 -0
  40. data/spec/mtk/pitch_set_spec.rb +163 -0
  41. data/spec/mtk/pitch_spec.rb +217 -0
  42. data/spec/mtk/timeline_spec.rb +234 -0
  43. data/spec/spec_helper.rb +7 -0
  44. data/spec/test.mid +0 -0
  45. metadata +97 -0
@@ -0,0 +1,60 @@
1
+ module MTK
2
+ module Pattern
3
+
4
+ # A {Sequence} of {Note}s
5
+ class NoteSequence
6
+
7
+ attr_reader :pitch_sequence, :intensity_sequence, :duration_sequence
8
+
9
+ attr_accessor :pitch, :intensity, :duration
10
+
11
+ def initialize(pitches, intensities=nil, durations=nil, defaults={})
12
+ @pitch_sequence = PitchSequence.new(pitches)
13
+ @intensity_sequence = Sequence.new(intensities)
14
+ @duration_sequence = Sequence.new(durations)
15
+ @default = {:pitch => Pitches::C4, :intensity => Dynamics::mf, :duration => 1}.merge defaults
16
+ reset
17
+ end
18
+
19
+ def pitches
20
+ @pitch_sequence.elements
21
+ end
22
+
23
+ def intensities
24
+ @intensity_sequence.elements
25
+ end
26
+
27
+ def durations
28
+ @duration_sequence.elements
29
+ end
30
+
31
+ # reset the Sequence to the beginning
32
+ def reset
33
+ @pitch_sequence.reset
34
+ @intensity_sequence.reset
35
+ @duration_sequence.reset
36
+
37
+ @pitch = @default[:pitch]
38
+ @intensity = @default[:intensity]
39
+ @duration = @default[:duration]
40
+ end
41
+
42
+ # return next {Note} in sequence
43
+ def next
44
+ @pitch = @pitch_sequence.next || @pitch
45
+ @intensity = @intensity_sequence.next || @intensity
46
+ @duration = @duration_sequence.next || @duration
47
+
48
+
49
+ case @pitch
50
+ when PitchSet,Array then Chord.new(@pitch, @intensity, @duration)
51
+ else Note.new(@pitch, @intensity, @duration)
52
+ end
53
+ end
54
+
55
+ end
56
+
57
+ end
58
+ end
59
+
60
+
@@ -0,0 +1,22 @@
1
+ module MTK
2
+ module Pattern
3
+
4
+ # A {Sequence} of pitch-related elements, which may be {Pitch}es, {PitchClass}es, {PitchSet}s, or {Intervals} (Numeric)
5
+ class PitchSequence < Sequence
6
+
7
+ protected
8
+
9
+ # extend value_of to handle intervals and PitchClasses
10
+ def value_of element
11
+ element = super # eval Procs
12
+ case element
13
+ when Numeric then @value + element if @value # add interval
14
+ when PitchClass then @value.nearest(element) if @value
15
+ else element
16
+ end
17
+ end
18
+
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,65 @@
1
+ module MTK
2
+ module Pattern
3
+
4
+ # An endless enumerator that outputs an element one at a time from a list of elements,
5
+ # looping back to the beginning when elements run out.
6
+ class Sequence
7
+
8
+ # The list of elements enumerated by this Sequence
9
+ attr_reader :elements
10
+
11
+ # @param elements [Array] the list of {#elements}
12
+ # @param default [Object] the default value returned by {#next} in place of nil
13
+ def initialize(elements)
14
+ if elements.respond_to? :elements
15
+ @elements = elements.elements
16
+ else
17
+ @elements = elements.to_a
18
+ end
19
+ reset
20
+ end
21
+
22
+ # reset the Sequence to its initial state
23
+ def reset
24
+ @index = -1
25
+ @element = nil
26
+ @value = nil
27
+ end
28
+
29
+ # The value of the next element in the Sequence.
30
+ # The sequence goes back to the first element if there are no more elements.
31
+ #
32
+ # @return if an element is a Proc, it is called (depending on the arity of the Proc: with either no arguments,
33
+ # the last value of #next, or the last value and last element) and its return value is returned.
34
+ # @return if an element is nil, previous value of #next is returned. If there is no previous value the @default is returned.
35
+ # @return otherwise the element itself is returned
36
+ #
37
+ def next
38
+ if @elements and not @elements.empty?
39
+ @index = (@index + 1) % @elements.length
40
+ element = @elements[@index]
41
+ value = value_of(element)
42
+ @element, @value = element, value
43
+ end
44
+ @value
45
+ end
46
+
47
+ ####################
48
+ protected
49
+
50
+ def value_of element
51
+ case element
52
+ when Proc
53
+ case element.arity
54
+ when 0 then element.call
55
+ when 1 then element.call(@value)
56
+ else element.call(@value, @element)
57
+ end
58
+ else element
59
+ end
60
+ end
61
+
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,4 @@
1
+ require 'mtk/pattern/sequence'
2
+ require 'mtk/pattern/pitch_sequence'
3
+ require 'mtk/pattern/note_sequence'
4
+ require 'mtk/pattern/choice'
data/lib/mtk/pitch.rb ADDED
@@ -0,0 +1,112 @@
1
+ module MTK
2
+
3
+ # A frequency represented by a {PitchClass}, an integer octave, and an offset in semitones.
4
+
5
+ class Pitch
6
+
7
+ include Comparable
8
+
9
+ attr_reader :pitch_class, :octave, :offset
10
+
11
+ def initialize( pitch_class, octave, offset=0 )
12
+ @pitch_class, @octave, @offset = pitch_class, octave, offset
13
+ @value = @pitch_class.to_i + 12*(@octave+1) + @offset
14
+ end
15
+
16
+ @flyweight = {}
17
+
18
+ def self.[](pitch_class, octave)
19
+ @flyweight[[pitch_class,octave]] ||= new(pitch_class, octave)
20
+ end
21
+
22
+ def self.from_s( s )
23
+ # TODO: update to handle offset
24
+ s = s[0..0].upcase + s[1..-1].downcase # normalize name
25
+ if s =~ /^([A-G](#|##|b|bb)?)(-?\d+)$/
26
+ pitch_class = PitchClass.from_s($1)
27
+ if pitch_class
28
+ octave = $3.to_i
29
+ new( pitch_class, octave )
30
+ end
31
+ end
32
+ end
33
+
34
+ # Convert a Numeric semitones value into a Pitch
35
+ def self.from_f( f )
36
+ i, offset = f.floor, f%1 # split into int and fractional part
37
+ pitch_class = PitchClass.from_i(i)
38
+ octave = i/12 - 1
39
+ new( pitch_class, octave, offset )
40
+ end
41
+
42
+ def self.from_hash(hash)
43
+ new hash[:pitch_class], hash[:octave], hash.fetch(:offset,0)
44
+ end
45
+
46
+ # Convert a Numeric semitones value into a Pitch
47
+ def self.from_i( i )
48
+ from_f( i )
49
+ end
50
+
51
+ # The numerical value of this pitch
52
+ def to_f
53
+ @value
54
+ end
55
+
56
+ # The numerical value for the nearest semitone
57
+ def to_i
58
+ @value.round
59
+ end
60
+
61
+ def offset_in_cents
62
+ @offset * 100
63
+ end
64
+
65
+ def to_hash
66
+ {:pitch_class => @pitch_class, :octave => @octave, :offset => @offset}
67
+ end
68
+
69
+ def to_s
70
+ "#{@pitch_class}#{@octave}" + (@offset.zero? ? '' : "+#{offset_in_cents.round}cents")
71
+ end
72
+
73
+ def inspect
74
+ "#{@pitch_class}#{@octave}" + (@offset.zero? ? '' : "+#{offset_in_cents}cents")
75
+ end
76
+
77
+ def ==( other )
78
+ other.respond_to? :pitch_class and other.respond_to? :octave and other.respond_to? :offset and
79
+ other.pitch_class == @pitch_class and other.octave == @octave and other.offset == @offset
80
+ end
81
+
82
+ def <=> other
83
+ @value <=> other.to_f
84
+ end
85
+
86
+ def + interval_in_semitones
87
+ self.class.from_f( @value + interval_in_semitones.to_f )
88
+ end
89
+
90
+ def - interval_in_semitones
91
+ self.class.from_f( @value - interval_in_semitones.to_f )
92
+ end
93
+
94
+ def invert(center_pitch)
95
+ self + 2*(center_pitch.to_f - to_f)
96
+ end
97
+
98
+ def nearest(pitch_class)
99
+ self + self.pitch_class.distance_to(pitch_class)
100
+ end
101
+
102
+ def coerce(other)
103
+ return self.class.from_f(other.to_f), self
104
+ end
105
+
106
+ def clone_with(hash)
107
+ self.class.from_hash(to_hash.merge hash)
108
+ end
109
+
110
+ end
111
+
112
+ end
@@ -0,0 +1,113 @@
1
+ module MTK
2
+
3
+ # A class of pitches under octave equivalence.
4
+ #
5
+ # A {Pitch} has the same PitchClass as the {Pitches} one or more octaves away.
6
+
7
+ class PitchClass
8
+
9
+ NAMES = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']
10
+
11
+ VALID_NAMES = [
12
+ ['B#', 'C', 'Dbb'],
13
+ ['B##', 'C#', 'Db'],
14
+ ['C##', 'D', 'Ebb'],
15
+ ['D#', 'Eb', 'Fbb'],
16
+ ['D##', 'E', 'Fb'],
17
+ ['E#', 'F', 'Gbb'],
18
+ ['E##', 'F#', 'Gb'],
19
+ ['F##', 'G', 'Abb'],
20
+ ['G#', 'Ab'],
21
+ ['G##', 'A', 'Bbb'],
22
+ ['A#', 'Bb', 'Cbb'],
23
+ ['A##', 'B', 'Cb']
24
+ ]
25
+
26
+ attr_reader :name
27
+
28
+ ##########################################
29
+ private
30
+
31
+ def initialize(name, int_value)
32
+ @name, @int_value = name, int_value
33
+ end
34
+
35
+ private_class_method :new
36
+
37
+ @flyweight = {}
38
+
39
+ def self.get(name, int_value)
40
+ @flyweight[name] ||= new(name, int_value)
41
+ end
42
+
43
+ private_class_method :get
44
+
45
+ ##########################################
46
+ public
47
+
48
+ def self.from_s(s)
49
+ s = s.to_s
50
+ s = s[0..0].upcase + s[1..-1].downcase # normalize the name
51
+ VALID_NAMES.each_with_index do |names, index|
52
+ return get(s, index) if names.include? s
53
+ end
54
+ nil
55
+ end
56
+
57
+ class << self
58
+ alias from_name from_s # alias self.from_name to self.from_s
59
+ end
60
+
61
+ def self.from_i(value)
62
+ value = value.to_i % 12
63
+ name = NAMES[value]
64
+ get(name, value)
65
+ end
66
+
67
+ def self.[](name_or_value)
68
+ if name_or_value.kind_of? Numeric
69
+ from_i(name_or_value)
70
+ else
71
+ from_s(name_or_value)
72
+ end
73
+ end
74
+
75
+ def == other
76
+ other.is_a? PitchClass and other.to_i == @int_value
77
+ end
78
+
79
+ def <=> other
80
+ @int_value <=> other.to_i
81
+ end
82
+
83
+ def to_s
84
+ @name
85
+ end
86
+
87
+ def to_i
88
+ @int_value
89
+ end
90
+
91
+ def +(interval)
92
+ self.class.from_i(to_i + interval.to_i)
93
+ end
94
+
95
+ def -(interval)
96
+ self.class.from_i(to_i - interval.to_i)
97
+ end
98
+
99
+ # the smallest interval in semitones that needs to be added to this PitchClass to reach the given PitchClass
100
+ def distance_to(pitch_class)
101
+ delta = (pitch_class.to_i - to_i) % 12
102
+ if delta > 6
103
+ delta -= 12
104
+ elsif delta == 6 and to_i >= 6
105
+ # this is a special edge case to prevent endlessly ascending pitch sequences when alternating between two pitch classes a tritone apart
106
+ delta = -6
107
+ end
108
+ delta
109
+ end
110
+
111
+ end
112
+
113
+ end
@@ -0,0 +1,106 @@
1
+ module MTK
2
+
3
+ # A Set of PitchClasses, for 12-tone set-theory pitch analysis and manipulations
4
+ #
5
+ class PitchClassSet
6
+
7
+ include Mappable
8
+
9
+ attr_reader :pitch_classes
10
+
11
+ def initialize(pitch_classes)
12
+ @pitch_classes = pitch_classes.to_a.uniq.sort.freeze
13
+ end
14
+
15
+ def self.from_a enumerable
16
+ new enumerable
17
+ end
18
+
19
+ def to_a
20
+ Array.new(@pitch_classes)
21
+ end
22
+
23
+ def each &block
24
+ @pitch_classes.each &block
25
+ end
26
+
27
+ def size
28
+ @pitch_classes.size
29
+ end
30
+
31
+ def normal_order
32
+ ordering = Array.new(@pitch_classes)
33
+ min_span, start_index_for_normal_order = nil, nil
34
+
35
+ # check every rotation for the minimal span:
36
+ size.times do |index|
37
+ span = self.class.span_for ordering
38
+
39
+ if min_span.nil? or span < min_span
40
+ # best so far
41
+ min_span = span
42
+ start_index_for_normal_order = index
43
+
44
+ elsif span == min_span
45
+ # handle ties, minimize distance between first and second-to-last note, then first and third-to-last, etc
46
+ span1, span2 = nil, nil
47
+ tie_breaker = 1
48
+ while span1 == span2 and tie_breaker < size
49
+ span1 = self.class.span_between( ordering[0], ordering[-1 - tie_breaker] )
50
+ span2 = self.class.span_between( ordering[start_index_for_normal_order], ordering[start_index_for_normal_order - tie_breaker] )
51
+ tie_breaker -= 1
52
+ end
53
+ if span1 != span2
54
+ # tie cannot be broken, pick the one starting with the lowest pitch class
55
+ if ordering[0].to_i < ordering[start_index_for_normal_order].to_i
56
+ start_index_for_normal_order = index
57
+ end
58
+ elsif span1 < span2
59
+ start_index_for_normal_order = index
60
+ end
61
+
62
+ end
63
+ ordering << ordering.shift # rotate
64
+ end
65
+
66
+ # we've rotated all the way around, so we now need to rotate back to the start index we just found:
67
+ start_index_for_normal_order.times{ ordering << ordering.shift }
68
+
69
+ ordering
70
+ end
71
+
72
+ def normal_form
73
+ norder = normal_order
74
+ first_pc_val = norder.first.to_i
75
+ norder.map{|pitch_class| (pitch_class.to_i - first_pc_val) % 12 }
76
+ end
77
+
78
+ # @param other [#pitch_classes, #to_a, Array]
79
+ def == other
80
+ if other.respond_to? :pitch_classes
81
+ @pitch_classes == other.pitch_classes
82
+ elsif other.respond_to? :to_a
83
+ @pitch_classes == other.to_a
84
+ else
85
+ @pitch_classes == other
86
+ end
87
+ end
88
+
89
+ def to_s
90
+ @pitch_classes.join(' ')
91
+ end
92
+
93
+ def inspect
94
+ @pitch_classes.inspect
95
+ end
96
+
97
+ def self.span_for(pitch_classes)
98
+ span_between pitch_classes.first, pitch_classes.last
99
+ end
100
+
101
+ def self.span_between(pc1, pc2)
102
+ (pc2.to_i - pc1.to_i) % 12
103
+ end
104
+
105
+ end
106
+ end
@@ -0,0 +1,95 @@
1
+ module MTK
2
+
3
+ # A set of {Pitch}es
4
+ #
5
+ class PitchSet
6
+
7
+ include Mappable
8
+
9
+ attr_reader :pitches
10
+
11
+ def initialize(pitches)
12
+ @pitches = pitches.to_a.uniq.sort.freeze
13
+ end
14
+
15
+ def self.from_a enumerable
16
+ new enumerable
17
+ end
18
+
19
+ def to_a
20
+ Array.new(@pitches)
21
+ end
22
+
23
+ def each &block
24
+ @pitches.each &block
25
+ end
26
+
27
+ def to_pitch_class_set
28
+ PitchClassSet.new @pitches.map{|p| p.pitch_class }
29
+ end
30
+
31
+ def pitch_classes
32
+ @pitch_classes ||= @pitches.map{|p| p.pitch_class }.uniq
33
+ end
34
+
35
+ def + semitones
36
+ each_pitch_apply :+, semitones
37
+ end
38
+
39
+ def - semitones
40
+ each_pitch_apply :-, semitones
41
+ end
42
+
43
+ def invert(center_pitch=@pitches.first)
44
+ each_pitch_apply :invert, center_pitch
45
+ end
46
+
47
+ def inversion(number)
48
+ number = number.to_i
49
+ pitch_set = Array.new(@pitches)
50
+ if number > 0
51
+ number.times do |count|
52
+ index = count % pitch_set.length
53
+ pitch_set[index] += 12
54
+ end
55
+ else
56
+ number.abs.times do |count|
57
+ index = -(count + 1) % pitch_set.length # count from -1 downward to go backwards through the list starting at the end
58
+ pitch_set[index] -= 12
59
+ end
60
+ end
61
+ self.class.new pitch_set
62
+ end
63
+
64
+ def include? pitch
65
+ @pitches.include? pitch
66
+ end
67
+
68
+ def nearest(pitch_class)
69
+ self + @pitches.first.pitch_class.distance_to(pitch_class)
70
+ end
71
+
72
+ # @param other [#pitches, #to_a, Array]
73
+ def == other
74
+ if other.respond_to? :pitches
75
+ @pitches == other.pitches
76
+ elsif other.respond_to? :to_a
77
+ @pitches == other.to_a
78
+ else
79
+ @pitches == other
80
+ end
81
+ end
82
+
83
+ def to_s
84
+ @pitches.inspect
85
+ end
86
+
87
+ #######################################
88
+ protected
89
+
90
+ def each_pitch_apply(method_name, *args, &block)
91
+ self.class.new @pitches.map{|pitch| pitch.send(method_name, *args, &block) }
92
+ end
93
+
94
+ end
95
+ end