mtk 0.0.3.2 → 0.0.3.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. data/.yardopts +2 -2
  2. data/DEVELOPMENT_NOTES.md +20 -0
  3. data/README.md +9 -3
  4. data/Rakefile +47 -13
  5. data/bin/mtk +55 -20
  6. data/examples/crescendo.rb +4 -4
  7. data/examples/{drum_pattern1.rb → drum_pattern.rb} +8 -8
  8. data/examples/dynamic_pattern.rb +5 -5
  9. data/examples/gets_and_play.rb +3 -2
  10. data/examples/notation.rb +3 -3
  11. data/examples/play_midi.rb +4 -4
  12. data/examples/print_midi.rb +2 -2
  13. data/examples/random_tone_row.rb +3 -3
  14. data/examples/syntax_to_midi.rb +2 -2
  15. data/examples/test_output.rb +4 -5
  16. data/examples/tone_row_melody.rb +7 -5
  17. data/lib/mtk/core/duration.rb +213 -0
  18. data/lib/mtk/core/intensity.rb +158 -0
  19. data/lib/mtk/core/interval.rb +157 -0
  20. data/lib/mtk/core/pitch.rb +154 -0
  21. data/lib/mtk/core/pitch_class.rb +194 -0
  22. data/lib/mtk/events/event.rb +4 -4
  23. data/lib/mtk/events/note.rb +12 -12
  24. data/lib/mtk/events/timeline.rb +232 -0
  25. data/lib/mtk/groups/chord.rb +56 -0
  26. data/lib/mtk/{helpers → groups}/collection.rb +33 -1
  27. data/lib/mtk/groups/melody.rb +96 -0
  28. data/lib/mtk/groups/pitch_class_set.rb +163 -0
  29. data/lib/mtk/{helpers → groups}/pitch_collection.rb +1 -1
  30. data/lib/mtk/{midi → io}/dls_synth_device.rb +3 -1
  31. data/lib/mtk/{midi → io}/dls_synth_output.rb +10 -10
  32. data/lib/mtk/{midi → io}/jsound_input.rb +2 -2
  33. data/lib/mtk/{midi → io}/jsound_output.rb +9 -9
  34. data/lib/mtk/{midi/file.rb → io/midi_file.rb} +13 -13
  35. data/lib/mtk/{midi/input.rb → io/midi_input.rb} +4 -4
  36. data/lib/mtk/{midi/output.rb → io/midi_output.rb} +8 -8
  37. data/lib/mtk/{helpers/lilypond.rb → io/notation.rb} +5 -5
  38. data/lib/mtk/{midi → io}/unimidi_input.rb +2 -2
  39. data/lib/mtk/{midi → io}/unimidi_output.rb +14 -9
  40. data/lib/mtk/{constants → lang}/durations.rb +11 -11
  41. data/lib/mtk/{constants → lang}/intensities.rb +11 -11
  42. data/lib/mtk/{constants → lang}/intervals.rb +17 -17
  43. data/lib/mtk/lang/mtk_grammar.citrus +9 -9
  44. data/lib/mtk/{constants → lang}/pitch_classes.rb +5 -5
  45. data/lib/mtk/{constants → lang}/pitches.rb +7 -7
  46. data/lib/mtk/{helpers → lang}/pseudo_constants.rb +1 -1
  47. data/lib/mtk/{variable.rb → lang/variable.rb} +1 -1
  48. data/lib/mtk/numeric_extensions.rb +40 -47
  49. data/lib/mtk/patterns/for_each.rb +1 -1
  50. data/lib/mtk/patterns/pattern.rb +3 -3
  51. data/lib/mtk/sequencers/event_builder.rb +16 -15
  52. data/lib/mtk/sequencers/legato_sequencer.rb +1 -1
  53. data/lib/mtk/sequencers/rhythmic_sequencer.rb +1 -1
  54. data/lib/mtk/sequencers/sequencer.rb +8 -8
  55. data/lib/mtk/sequencers/step_sequencer.rb +2 -2
  56. data/lib/mtk.rb +33 -39
  57. data/spec/mtk/{duration_spec.rb → core/duration_spec.rb} +3 -3
  58. data/spec/mtk/{intensity_spec.rb → core/intensity_spec.rb} +3 -3
  59. data/spec/mtk/{interval_spec.rb → core/interval_spec.rb} +1 -1
  60. data/spec/mtk/{pitch_class_spec.rb → core/pitch_class_spec.rb} +1 -1
  61. data/spec/mtk/{pitch_spec.rb → core/pitch_spec.rb} +8 -8
  62. data/spec/mtk/events/event_spec.rb +4 -4
  63. data/spec/mtk/events/note_spec.rb +8 -8
  64. data/spec/mtk/{timeline_spec.rb → events/timeline_spec.rb} +47 -47
  65. data/spec/mtk/{chord_spec.rb → groups/chord_spec.rb} +18 -16
  66. data/spec/mtk/{helpers → groups}/collection_spec.rb +3 -3
  67. data/spec/mtk/{melody_spec.rb → groups/melody_spec.rb} +36 -34
  68. data/spec/mtk/{pitch_class_set_spec.rb → groups/pitch_class_set_spec.rb} +57 -55
  69. data/spec/mtk/{midi/file_spec.rb → io/midi_file_spec.rb} +17 -17
  70. data/spec/mtk/{midi/output_spec.rb → io/midi_output_spec.rb} +6 -6
  71. data/spec/mtk/{constants → lang}/durations_spec.rb +1 -1
  72. data/spec/mtk/{constants → lang}/intensities_spec.rb +1 -1
  73. data/spec/mtk/{constants → lang}/intervals_spec.rb +1 -1
  74. data/spec/mtk/lang/parser_spec.rb +12 -6
  75. data/spec/mtk/{constants → lang}/pitch_classes_spec.rb +1 -1
  76. data/spec/mtk/{constants → lang}/pitches_spec.rb +1 -1
  77. data/spec/mtk/{helpers → lang}/pseudo_constants_spec.rb +2 -2
  78. data/spec/mtk/{variable_spec.rb → lang/variable_spec.rb} +4 -4
  79. data/spec/mtk/numeric_extensions_spec.rb +35 -55
  80. data/spec/mtk/patterns/for_each_spec.rb +1 -1
  81. data/spec/mtk/patterns/sequence_spec.rb +1 -1
  82. data/spec/mtk/sequencers/legato_sequencer_spec.rb +2 -2
  83. data/spec/mtk/sequencers/rhythmic_sequencer_spec.rb +4 -4
  84. data/spec/mtk/sequencers/step_sequencer_spec.rb +5 -5
  85. data/spec/spec_helper.rb +7 -6
  86. metadata +75 -61
  87. data/ext/mkrf_conf.rb +0 -25
  88. data/lib/mtk/chord.rb +0 -55
  89. data/lib/mtk/duration.rb +0 -211
  90. data/lib/mtk/helpers/convert.rb +0 -36
  91. data/lib/mtk/helpers/output_selector.rb +0 -67
  92. data/lib/mtk/intensity.rb +0 -156
  93. data/lib/mtk/interval.rb +0 -155
  94. data/lib/mtk/melody.rb +0 -94
  95. data/lib/mtk/pitch.rb +0 -152
  96. data/lib/mtk/pitch_class.rb +0 -192
  97. data/lib/mtk/pitch_class_set.rb +0 -161
  98. data/lib/mtk/timeline.rb +0 -230
  99. data/spec/mtk/midi/jsound_input_spec.rb +0 -11
  100. data/spec/mtk/midi/jsound_output_spec.rb +0 -11
  101. data/spec/mtk/midi/unimidi_input_spec.rb +0 -11
  102. data/spec/mtk/midi/unimidi_output_spec.rb +0 -11
data/lib/mtk/intensity.rb DELETED
@@ -1,156 +0,0 @@
1
- module MTK
2
-
3
- # A measure of intensity, using an underlying value in the range 0.0-1.0
4
- class Intensity
5
-
6
- include Comparable
7
-
8
- # The names of the base intensities. See {}MTK::Constants::Intensities} for more info.
9
- NAMES = %w[ppp pp p mp mf o ff fff].freeze
10
-
11
- VALUES_BY_NAME = {
12
- 'ppp' => 0.125,
13
- 'pp' => 0.25,
14
- 'p' => 0.375,
15
- 'mp' => 0.5,
16
- 'mf' => 0.625,
17
- 'o' => 0.75,
18
- 'ff' => 0.875,
19
- 'fff' => 1.0
20
- }
21
-
22
- @flyweight = {}
23
-
24
- # The number of beats, typically representation as a Rational
25
- attr_reader :value
26
-
27
- def initialize(value)
28
- @value = value
29
- end
30
-
31
- # Return an Intensity, only constructing a new instance when not already in the flyweight cache
32
- def self.[](value)
33
- value = value.to_f
34
- @flyweight[value] ||= new(value)
35
- end
36
-
37
- class << self
38
- alias :from_f :[]
39
- alias :from_i :[]
40
- end
41
-
42
- # Lookup an intensity by name.
43
- # This method supports appending '-' or '+' for more fine-grained values.
44
- def self.from_s(s)
45
- return self[1.0] if s == 'fff+' # special case because "fff" is already the maximum
46
-
47
- name = nil
48
- modifier = nil
49
- if s =~ /^(\w+)([+-])?$/
50
- name = $1
51
- modifier = $2
52
- end
53
-
54
- value = VALUES_BY_NAME[name]
55
- raise ArgumentError.new("Invalid Intensity string '#{s}'") unless value
56
-
57
- value += 1.0/24 if modifier == '+'
58
- value -= 1.0/24 if modifier == '-'
59
-
60
- self[value]
61
- end
62
-
63
- class << self
64
- alias :from_name :from_s
65
- end
66
-
67
- # The number of beats as a floating point number
68
- def to_f
69
- @value.to_f
70
- end
71
-
72
- # The numerical value for the nearest whole number of beats
73
- def to_i
74
- @value.round
75
- end
76
-
77
- def to_midi
78
- (to_f * 127).round
79
- end
80
-
81
- def to_percent
82
- (@value * 100).round
83
- end
84
-
85
- def to_s
86
- "#{to_percent}% intensity"
87
- end
88
-
89
- def inspect
90
- "#<#{self.class}:#{object_id} @value=#{@value}>"
91
- end
92
-
93
- def ==( other )
94
- other.is_a? MTK::Intensity and other.value == @value
95
- end
96
-
97
- def <=> other
98
- if other.respond_to? :value
99
- @value <=> other.value
100
- else
101
- @value <=> other
102
- end
103
-
104
- end
105
-
106
- def + intensity
107
- if intensity.is_a? MTK::Intensity
108
- MTK::Intensity[@value + intensity.value]
109
- else
110
- MTK::Intensity[@value + intensity]
111
- end
112
- end
113
-
114
- def -intensity
115
- if intensity.is_a? MTK::Intensity
116
- MTK::Intensity[@value - intensity.value]
117
- else
118
- MTK::Intensity[@value - intensity]
119
- end
120
- end
121
-
122
- def * intensity
123
- if intensity.is_a? MTK::Intensity
124
- MTK::Intensity[@value * intensity.value]
125
- else
126
- MTK::Intensity[@value * intensity]
127
- end
128
- end
129
-
130
- def / intensity
131
- if intensity.is_a? MTK::Intensity
132
- MTK::Intensity[to_f / intensity.value]
133
- else
134
- MTK::Intensity[to_f / intensity]
135
- end
136
- end
137
-
138
- def coerce(other)
139
- return MTK::Intensity[other], self
140
- end
141
-
142
- end
143
-
144
- # Construct a {Duration} from any supported type
145
- def Intensity(*anything)
146
- anything = anything.first if anything.length == 1
147
- case anything
148
- when Numeric then MTK::Intensity[anything]
149
- when String, Symbol then MTK::Intensity.from_s(anything)
150
- when Intensity then anything
151
- else raise "Intensity doesn't understand #{anything.class}"
152
- end
153
- end
154
- module_function :Intensity
155
-
156
- end
data/lib/mtk/interval.rb DELETED
@@ -1,155 +0,0 @@
1
- module MTK
2
-
3
- # A measure of intensity, using an underlying value in the range 0.0-1.0
4
- class Interval
5
-
6
- include Comparable
7
-
8
- # The preferred names of all pre-defined intervals
9
- NAMES = %w[P1 m2 M2 m3 M3 P4 TT P5 m6 M6 m7 M7 P8].freeze
10
-
11
- # All valid names of pre-defined intervals, indexed by their value.
12
- NAMES_BY_VALUE =
13
- [ # names # value # description
14
- %w( P1 p1 ), # 0 # unison
15
- %w( m2 min2 ), # 1 # minor second
16
- %w( M2 maj2 ), # 2 # major second
17
- %w( m3 min3 ), # 3 # minor third
18
- %w( M3 maj3 ), # 4 # major third
19
- %w( P4 p4 ), # 5 # perfect fourth
20
- %w( TT tt ), # 6 # tritone (AKA augmented fourth, diminished fifth)
21
- %w( P5 p5 ), # 7 # perfect fifth
22
- %w( m6 min6 ), # 8 # minor sixth
23
- %w( M6 maj6 ), # 9 # major sixth
24
- %w( m7 min7 ), # 10 # minor seventh
25
- %w( M7 maj7 ), # 11 # major seventh
26
- %w( P8 p8 ) # 12 # octave
27
- ].freeze
28
-
29
- # A mapping from intervals names to their value
30
- VALUES_BY_NAME = Hash[ # a map from a list of name,value pairs
31
- NAMES_BY_VALUE.map.with_index do |names,value|
32
- names.map{|name| [name,value] }
33
- end.flatten(1)
34
- ].freeze
35
-
36
- # All valid interval names
37
- ALL_NAMES = NAMES_BY_VALUE.flatten.freeze
38
-
39
-
40
- @flyweight = {}
41
-
42
- # The number of semitones represented by this interval
43
- attr_reader :value
44
-
45
- def initialize(value)
46
- @value = value
47
- end
48
-
49
- # Return an {Interval}, only constructing a new instance when not already in the flyweight cache
50
- def self.[](value)
51
- value = value.to_f unless value.is_a? Fixnum
52
- @flyweight[value] ||= new(value)
53
- end
54
-
55
- class << self
56
- alias :from_f :[]
57
- alias :from_i :[]
58
- end
59
-
60
- # Lookup an interval duration by name.
61
- def self.from_s(s)
62
- value = VALUES_BY_NAME[s.to_s]
63
- raise ArgumentError.new("Invalid Interval string '#{s}'") unless value
64
- self[value]
65
- end
66
-
67
- class << self
68
- alias :from_name :from_s
69
- end
70
-
71
- # The number of semitones as a floating point number
72
- def to_f
73
- @value.to_f
74
- end
75
-
76
- # The numerical value for the nearest whole number of semitones in this interval
77
- def to_i
78
- @value.round
79
- end
80
-
81
- def to_s
82
- @value.to_s
83
- end
84
-
85
- def inspect
86
- "#{self.class}<#{to_s} semitones>"
87
- end
88
-
89
- def ==( other )
90
- other.is_a? MTK::Interval and other.value == @value
91
- end
92
-
93
- def <=> other
94
- if other.respond_to? :value
95
- @value <=> other.value
96
- else
97
- @value <=> other
98
- end
99
- end
100
-
101
- def + interval
102
- if interval.is_a? MTK::Interval
103
- MTK::Interval[@value + interval.value]
104
- else
105
- MTK::Interval[@value + interval]
106
- end
107
- end
108
-
109
- def -interval
110
- if interval.is_a? MTK::Interval
111
- MTK::Interval[@value - interval.value]
112
- else
113
- MTK::Interval[@value - interval]
114
- end
115
- end
116
-
117
- def * interval
118
- if interval.is_a? MTK::Interval
119
- MTK::Interval[@value * interval.value]
120
- else
121
- MTK::Interval[@value * interval]
122
- end
123
- end
124
-
125
- def / interval
126
- if interval.is_a? MTK::Interval
127
- MTK::Interval[to_f / interval.value]
128
- else
129
- MTK::Interval[to_f / interval]
130
- end
131
- end
132
-
133
- def -@
134
- MTK::Interval[@value * -1]
135
- end
136
-
137
- def coerce(other)
138
- return MTK::Interval[other], self
139
- end
140
-
141
- end
142
-
143
- # Construct a {Duration} from any supported type
144
- def Interval(*anything)
145
- anything = anything.first if anything.length == 1
146
- case anything
147
- when Numeric then MTK::Interval[anything]
148
- when String, Symbol then MTK::Interval.from_s(anything)
149
- when Interval then anything
150
- else raise "Interval doesn't understand #{anything.class}"
151
- end
152
- end
153
- module_function :Interval
154
-
155
- end
data/lib/mtk/melody.rb DELETED
@@ -1,94 +0,0 @@
1
- module MTK
2
-
3
- # An ordered collection of {Pitch}es.
4
- #
5
- # The "horizontal" (sequential) pitch collection.
6
- #
7
- # Unlike the strict definition of melody, this class is fairly abstract and only models a succession of pitches.
8
- # To create a true, playable melody one must combine an MTK::Melody and rhythms into a {Timeline}.
9
- #
10
- # @see Chord
11
- #
12
- class Melody
13
- include Helpers::PitchCollection
14
-
15
- attr_reader :pitches
16
-
17
- # @param pitches [#to_a] the collection of pitches
18
- # @see MTK#Melody
19
- #
20
- def initialize(pitches)
21
- @pitches = pitches.to_a.clone.freeze
22
- end
23
-
24
- def self.from_pitch_classes(pitch_classes, start=Constants::Pitches::C4, max_distance=12)
25
- pitch = start
26
- pitches = []
27
- pitch_classes.each do |pitch_class|
28
- pitch = pitch.nearest(pitch_class)
29
- pitch -= 12 if pitch > start+max_distance # keep within max_distance of start (default is one octave)
30
- pitch += 12 if pitch < start-max_distance
31
- pitches << pitch
32
- end
33
- new pitches
34
- end
35
-
36
- # @see Helper::Collection
37
- def elements
38
- @pitches
39
- end
40
-
41
- # Convert to an Array of pitches.
42
- # @note this returns a mutable copy the underlying @pitches attribute, which is otherwise unmutable
43
- alias :to_pitches :to_a
44
-
45
- def self.from_a enumerable
46
- new enumerable
47
- end
48
-
49
- def to_pitch_class_set(remove_duplicates=true)
50
- PitchClassSet.new(remove_duplicates ? pitch_classes.uniq : pitch_classes)
51
- end
52
-
53
- def pitch_classes
54
- @pitch_classes ||= @pitches.map{|p| p.pitch_class }
55
- end
56
-
57
- # @param other [#pitches, Enumerable]
58
- def == other
59
- if other.respond_to? :pitches
60
- @pitches == other.pitches
61
- elsif other.is_a? Enumerable
62
- @pitches == other.to_a
63
- else
64
- @pitches == other
65
- end
66
- end
67
-
68
- # Compare for equality, ignoring order and duplicates
69
- # @param other [#pitches, Array, #to_a]
70
- def =~ other
71
- @normalized_pitches ||= @pitches.uniq.sort
72
- @normalized_pitches == case
73
- when other.respond_to?(:pitches) then other.pitches.uniq.sort
74
- when (other.is_a? Array and other.frozen?) then other
75
- when other.respond_to?(:to_a) then other.to_a.uniq.sort
76
- else other
77
- end
78
- end
79
-
80
- def to_s
81
- '[' + @pitches.map{|pitch| pitch.to_s}.join(', ') + ']'
82
- end
83
-
84
- end
85
-
86
- # Construct an ordered {Melody} that allows duplicates
87
- # @see #Melody
88
- # @see #Chord
89
- def Melody(*anything)
90
- Melody.new Helpers::Convert.to_pitches(*anything)
91
- end
92
- module_function :Melody
93
-
94
- end
data/lib/mtk/pitch.rb DELETED
@@ -1,152 +0,0 @@
1
- module MTK
2
-
3
- # A frequency represented by a {PitchClass}, an integer octave, and an offset in semitones.
4
- class Pitch
5
-
6
- include Comparable
7
-
8
- attr_reader :pitch_class, :octave, :offset
9
-
10
- def initialize( pitch_class, octave, offset=0 )
11
- @pitch_class, @octave, @offset = pitch_class, octave, offset
12
- @value = @pitch_class.to_i + 12*(@octave+1) + @offset
13
- end
14
-
15
- @flyweight = {}
16
-
17
- # Return a pitch with no offset, only constructing a new instance when not already in the flyweight cache
18
- def self.[](pitch_class, octave)
19
- pitch_class = MTK.PitchClass(pitch_class)
20
- @flyweight[[pitch_class,octave]] ||= new(pitch_class, octave)
21
- end
22
-
23
- # Lookup a pitch by name, which consists of any {PitchClass::VALID_NAMES} and an octave number.
24
- # The name may also be optionally suffixed by +/-###cents (where ### is any number).
25
- # @example get the Pitch for middle C :
26
- # Pitch.from_s('C4')
27
- # @example get the Pitch for middle C + 50 cents:
28
- # Pitch.from_s('C4+50cents')
29
- def self.from_s( name )
30
- s = name.to_s
31
- s = s[0..0].upcase + s[1..-1].downcase # normalize name
32
- if s =~ /^([A-G](#|##|b|bb)?)(-?\d+)(\+(\d+(\.\d+)?)cents)?$/
33
- pitch_class = PitchClass.from_s($1)
34
- if pitch_class
35
- octave = $3.to_i
36
- offset_in_cents = $5.to_f
37
- if offset_in_cents.nil? or offset_in_cents.zero?
38
- return self[pitch_class, octave]
39
- else
40
- return new( pitch_class, octave, offset_in_cents/100.0 )
41
- end
42
- end
43
- end
44
- raise ArgumentError.new("Invalid pitch name: #{name.inspect}")
45
- end
46
-
47
- class << self
48
- alias :from_name :from_s
49
- end
50
-
51
- # Convert a Numeric semitones value into a Pitch
52
- def self.from_f( f )
53
- i, offset = f.floor, f%1 # split into int and fractional part
54
- pitch_class = PitchClass.from_i(i)
55
- octave = i/12 - 1
56
- if offset == 0
57
- self[pitch_class, octave]
58
- else
59
- new( pitch_class, octave, offset )
60
- end
61
- end
62
-
63
- def self.from_hash(hash)
64
- new hash[:pitch_class], hash[:octave], hash.fetch(:offset,0)
65
- end
66
-
67
- # Convert a Numeric semitones value into a Pitch
68
- def self.from_i( i )
69
- from_f( i )
70
- end
71
-
72
- # The numerical value of this pitch
73
- def to_f
74
- @value
75
- end
76
-
77
- # The numerical value for the nearest semitone
78
- def to_i
79
- @value.round
80
- end
81
-
82
- def offset_in_cents
83
- @offset * 100
84
- end
85
-
86
- def to_hash
87
- {:pitch_class => @pitch_class, :octave => @octave, :offset => @offset}
88
- end
89
-
90
- def to_s
91
- "#{@pitch_class}#{@octave}" + (@offset.zero? ? '' : "+#{offset_in_cents.round}cents")
92
- end
93
-
94
- def inspect
95
- "#<#{self.class}:#{object_id} @value=#{@value}>"
96
- end
97
-
98
- def ==( other )
99
- other.respond_to? :pitch_class and other.respond_to? :octave and other.respond_to? :offset and
100
- other.pitch_class == @pitch_class and other.octave == @octave and other.offset == @offset
101
- end
102
-
103
- def <=> other
104
- @value <=> other.to_f
105
- end
106
-
107
- def + interval_in_semitones
108
- self.class.from_f( @value + interval_in_semitones.to_f )
109
- end
110
- alias transpose +
111
-
112
- def - interval_in_semitones
113
- self.class.from_f( @value - interval_in_semitones.to_f )
114
- end
115
-
116
- def invert(center_pitch)
117
- self + 2*(center_pitch.to_f - to_f)
118
- end
119
-
120
- def nearest(pitch_class)
121
- self + self.pitch_class.distance_to(pitch_class)
122
- end
123
-
124
- def coerce(other)
125
- return self.class.from_f(other.to_f), self
126
- end
127
-
128
- def clone_with(hash)
129
- self.class.from_hash(to_hash.merge hash)
130
- end
131
-
132
- end
133
-
134
- # Construct a {Pitch} from any supported type
135
- def Pitch(*anything)
136
- anything = anything.first if anything.length == 1
137
- case anything
138
- when Numeric then Pitch.from_f(anything)
139
- when String, Symbol then Pitch.from_s(anything)
140
- when Pitch then anything
141
- when Array
142
- if anything.length == 2
143
- Pitch[*anything]
144
- else
145
- Pitch.new(*anything)
146
- end
147
- else raise ArgumentError.new("Pitch doesn't understand #{anything.class}")
148
- end
149
- end
150
- module_function :Pitch
151
-
152
- end