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
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # MTK
2
+ ## Music ToolKit for Ruby
3
+
4
+ Classes for modeling music with a focus on simplicity. Support for reading/writing MIDI files (and soon, realtime MIDI).
5
+
6
+
7
+
8
+ ## Goals
9
+
10
+ * Build musical generators to assist with composing music
11
+ * Re-implement Cosy (http://compusition.com/web/software/cosy) using these models as the "backend"
12
+
13
+
14
+
15
+ ## Status
16
+
17
+ Pre-alpha, API subject to change. Feedback welcome!
18
+
19
+
20
+
21
+ ## Requirements
22
+ ### Ruby Version
23
+
24
+ Ruby 1.8 or 1.9
25
+
26
+ ### Gem Dependencies
27
+
28
+ * rake (tests & docs)
29
+ * rspec (tests)
30
+ * yard (docs)
31
+ * yard-rspec (docs)
32
+ * rdiscount (docs)
33
+ * midilib (MIDI file I/O -- not strictly required by core lib, but currently needed for tests)
34
+
35
+
36
+
37
+ ## Documentation
38
+
39
+ rake yard
40
+
41
+ then open doc/frames.html
42
+
43
+
44
+
45
+ ## Tests
46
+
47
+ rake spec
48
+
49
+ I test with MRI 1.8.7, MRI 1.9.2, JRuby 1.5.6, and JRuby 1.6.1 on OS X via rvm:
50
+
51
+ rvm 1.8.7,1.9.2,jruby-1.5.6,jruby-1.6.1 rake spec:fast
52
+
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ require 'rspec/core/rake_task'
2
+ require 'rake/clean'
3
+ require 'yard'
4
+
5
+ task :default => :spec
6
+
7
+ CLEAN.include('html','doc') # clean and clobber do the same thing for now
8
+
9
+ desc "Run RSpec tests with full output"
10
+ RSpec::Core::RakeTask.new do |spec|
11
+ spec.rspec_opts = ["--color", "--format", "nested"]
12
+ end
13
+
14
+ namespace :spec do
15
+ desc "Run RSpecs tests with summary output and fast failure"
16
+ RSpec::Core::RakeTask.new(:fast) do |spec|
17
+ spec.rspec_opts = ["--color", "--fail-fast"]
18
+ end
19
+ end
20
+
21
+ YARD::Rake::YardocTask.new do |yard|
22
+ yard.files = ['lib/**/*.rb', 'spec/**/*.rb']
23
+ yard.options = []
24
+ if File.exist? '../yard-spec-plugin/lib/yard-rspec.rb'
25
+ # prefer my local patched copy which can handle my rspec conventions better...
26
+ yard.options.concat ['-e' '../yard-spec-plugin/lib/yard-rspec.rb']
27
+ else
28
+ # use the gem
29
+ yard.options.concat ['-e' 'yard-rspec']
30
+ end
31
+ end
data/lib/mtk/chord.rb ADDED
@@ -0,0 +1,47 @@
1
+ module MTK
2
+
3
+ # A multi-pitch, note-like {Event} defined by a {PitchSet}, intensity, and duration
4
+ class Chord < Event
5
+
6
+ # the {PitchSet} of the chord
7
+ attr_reader :pitch_set
8
+
9
+ def initialize(pitches, intensity, duration)
10
+ @pitch_set = if pitches.is_a? PitchSet
11
+ pitches
12
+ else
13
+ PitchSet.new(pitches)
14
+ end
15
+ super(intensity, duration)
16
+ end
17
+
18
+ def self.from_hash(hash)
19
+ new hash[:pitch_set], hash[:intensity], hash[:duration]
20
+ end
21
+
22
+ def to_hash
23
+ super.merge({ :pitch_set => @pitch_set })
24
+ end
25
+
26
+ def pitches
27
+ @pitch_set.pitches
28
+ end
29
+
30
+ def transpose(interval)
31
+ self.class.new(@pitch_set + interval, @intensity, @duration)
32
+ end
33
+
34
+ def == other
35
+ super and other.respond_to? :pitch_set and @pitch_set == other.pitch_set
36
+ end
37
+
38
+ def to_s
39
+ "Chord(#{pitch_set}, #{super})"
40
+ end
41
+
42
+ def inspect
43
+ "Chord(#{pitch_set}, #{super})"
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,56 @@
1
+ module MTK
2
+
3
+ # Defines values for standard dynamic symbols.
4
+ #
5
+ # These can be thought of like constants, but in order to distinguish 'f' (forte) from the {PitchClass} 'F'
6
+ # it was necessary to use lower-case names and therefore define them as "pseudo constant" methods.
7
+ # The methods are available either throught the module (MTK::Dynamics::f) or via mixin (include MTK::Dynamics; f)
8
+ #
9
+ # These values are intensities in the range 0.0 - 1.0, so they can be easily scaled (unlike MIDI velocities).
10
+ #
11
+ # @note Including this module shadows Ruby's built-in p() method.
12
+ # If you include this module, you can access the built-in p() method via Kernel.p()
13
+ #
14
+ # @see Note
15
+ module Dynamics
16
+ extend MTK::PseudoConstants
17
+
18
+ # NOTE: the yard doc macros here only fill in [$2] with the actual value when generating docs under Ruby 1.9+
19
+
20
+ # pianississimo
21
+ # @macro [attach] dynamics.define_constant
22
+ # @attribute [r]
23
+ # @return [$2] intensity value for $1
24
+ define_constant 'ppp', 0.125
25
+
26
+ # pianissimo
27
+ define_constant 'pp', 0.25
28
+
29
+ # piano
30
+ # @note Including this module shadows Ruby's built-in p() method.
31
+ # If you include this module, you can access the built-in p() method via Kernel.p()
32
+ define_constant 'p', 0.375
33
+
34
+ # mezzo-piano
35
+ define_constant 'mp', 0.5
36
+
37
+ # mezzo-forte
38
+ define_constant 'mf', 0.625
39
+
40
+ # forte
41
+ define_constant 'f', 0.75
42
+
43
+ # fortissimo
44
+ define_constant 'ff', 0.875
45
+
46
+ # fortississimo
47
+ define_constant 'fff', 1.0
48
+
49
+ def self.[](name)
50
+ send name
51
+ rescue
52
+ nil
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,76 @@
1
+ module MTK
2
+
3
+ # Defines a constant for intervals up to an octave using diatonic naming conventions (see http://en.wikipedia.org/wiki/Interval_(music)#Main_intervals)
4
+ #
5
+ # Naming conventions
6
+ # P#: perfect interval
7
+ # M#: major interval
8
+ # m#: minor interval
9
+ # TT: tritone (AKA augmented 4th or diminshed 5th)
10
+ #
11
+ # These can be thought of like constants, but in order to succintly distinguish 'm2' (minor) from 'M2' (major),
12
+ # it was necessary to use lower-case names for some of the values and therefore define them as "pseudo constant" methods.
13
+ # The methods are available either through the module (MTK::Intervals::m2) or via mixin (include MTK::Intervals; m2)
14
+ module Intervals
15
+ extend PseudoConstants
16
+
17
+ # NOTE: the yard doc macros here only fill in [$2] with the actual value when generating docs under Ruby 1.9+
18
+
19
+ # perfect unison
20
+ # @macro [attach] interval.define_constant
21
+ # @attribute [r]
22
+ # @return [$2] number of semitones in the interval $1
23
+ define_constant 'P1', 0
24
+
25
+ # minor second
26
+ # @macro [attach] interval.define_constant
27
+ # @attribute [r]
28
+ # @return [$2] number of semitones in the interval $1
29
+ define_constant 'm2', 1
30
+
31
+ # major second
32
+ define_constant 'M2', 2
33
+
34
+ # minor third
35
+ define_constant 'm3', 3
36
+
37
+ # major third
38
+ define_constant 'M3', 4
39
+
40
+ # pefect fourth
41
+ define_constant 'P4', 5
42
+
43
+ # tritone (AKA augmented fourth or diminished fifth)
44
+ define_constant 'TT', 6
45
+
46
+ # perfect fifth
47
+ define_constant 'P5', 7
48
+
49
+ # minor sixth
50
+ define_constant 'm6', 8
51
+
52
+ # major sixth
53
+ define_constant 'M6', 9
54
+
55
+ # minor seventh
56
+ define_constant 'm7', 10
57
+
58
+ # major seventh
59
+ define_constant 'M7', 11
60
+
61
+ # pefect octave
62
+ define_constant 'P8', 12
63
+
64
+ def self.[](name)
65
+ send name
66
+ rescue
67
+ begin
68
+ const_get name
69
+ rescue
70
+ nil
71
+ end
72
+ end
73
+
74
+ end
75
+
76
+ end
@@ -0,0 +1,18 @@
1
+ module MTK
2
+
3
+ # Defines a constant for each {PitchClass} in the Western chromatic scale.
4
+
5
+ module PitchClasses
6
+
7
+ # An array of all pitch class constants defined in this module
8
+ PITCH_CLASSES = []
9
+
10
+ for name in PitchClass::NAMES
11
+ pc = PitchClass.from_name name
12
+ PITCH_CLASSES << pc
13
+ const_set name, pc
14
+ end
15
+
16
+ end
17
+
18
+ end
@@ -0,0 +1,24 @@
1
+ module MTK
2
+
3
+ # Defines a constants for each {Pitch} in the standard MIDI range using scientific pitch notation.
4
+ #
5
+ # See http://en.wikipedia.org/wiki/Scientific_pitch_notation
6
+ #
7
+ # Note that because the character '#' cannot be used in the name of a constant,
8
+ # The "black key" pitches are all named as flats with 'b' (for example, Gb3 or Cb4)
9
+
10
+ module Pitches
11
+
12
+ # An array of all the pitch constants defined in this module
13
+ PITCHES = []
14
+
15
+ 128.times do |note_number|
16
+ pitch = Pitch.from_i( note_number )
17
+ PITCHES << pitch
18
+ octave_str = pitch.octave.to_s.sub(/-/,'_') # '_1' for -1
19
+ const_set "#{pitch.pitch_class}#{octave_str}", pitch
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,25 @@
1
+ module MTK
2
+
3
+ # Extension for modules that want to define pseudo-constants (constant-like values with lower-case names)
4
+ module PseudoConstants
5
+
6
+ # Define a "constant" as a module method and module function (available both through the module namespace and as a mixin method),
7
+ # in order to facility defining constant-like values with lower-case names.
8
+ #
9
+ # @param name [Symbol] the name of the pseudo-constant
10
+ # @param value [Object] the value of the pseudo-constant
11
+ # @return [nil]
12
+ def define_constant name, value
13
+ if name[0..0] =~ /[A-Z]/
14
+ const_set name, value # it's just a normal constant
15
+ else
16
+ # the pseudo-constant definition is the combination of a method and module_function:
17
+ define_method(name) { value }
18
+ module_function name
19
+ end
20
+ nil
21
+ end
22
+
23
+ end
24
+
25
+ end
data/lib/mtk/event.rb ADDED
@@ -0,0 +1,61 @@
1
+ module MTK
2
+
3
+ # An abstract musical event that has an intensity and a duration
4
+ # @abstract
5
+ class Event
6
+
7
+ # intensity of the note as a value in the range 0.0 - 1.0
8
+ attr_reader :intensity
9
+
10
+ # duration of the note in beats (e.g. 1.0 is a quarter note in 4/4 time signatures)
11
+ attr_reader :duration
12
+
13
+ def initialize(intensity, duration)
14
+ @intensity, @duration = intensity, duration
15
+ end
16
+
17
+ def self.from_hash(hash)
18
+ new hash[:intensity], hash[:duration]
19
+ end
20
+
21
+ def to_hash
22
+ { :intensity => @intensity, :duration => @duration }
23
+ end
24
+
25
+ def clone_with(hash)
26
+ self.class.from_hash(to_hash.merge hash)
27
+ end
28
+
29
+ def scale_intensity(scaling_factor)
30
+ clone_with :intensity => @intensity * scaling_factor.to_f
31
+ end
32
+
33
+ def scale_duration(scaling_factor)
34
+ clone_with :duration => @duration * scaling_factor.to_f
35
+ end
36
+
37
+ # intensity scaled to the MIDI range 0-127
38
+ def velocity
39
+ (127 * @intensity).round
40
+ end
41
+
42
+ def duration_in_pulses(pulses_per_beat)
43
+ (@duration * pulses_per_beat).round
44
+ end
45
+
46
+ def == other
47
+ other.respond_to? :intensity and @intensity == other.intensity and
48
+ other.respond_to? :duration and @duration == other.duration
49
+ end
50
+
51
+ def to_s
52
+ "#{sprintf '%.2f',@intensity}, #{sprintf '%.2f',@duration}"
53
+ end
54
+
55
+ def inspect
56
+ "#@intensity, #@duration"
57
+ end
58
+
59
+ end
60
+
61
+ end
@@ -0,0 +1,179 @@
1
+ require 'rubygems'
2
+ require 'midilib'
3
+
4
+ module MTK
5
+ module MIDI
6
+
7
+ class File
8
+ def initialize file
9
+ if file.respond_to? :path
10
+ @file = file.path
11
+ else
12
+ @file = file.to_s
13
+ end
14
+ end
15
+
16
+ # Read a MIDI file into an Array of {Timeline}s
17
+ #
18
+ # @param filepath [String, #path] path of the file to be written
19
+ # @return [Timeline]
20
+ #
21
+ def to_timelines
22
+ timelines = []
23
+
24
+ ::File.open(@file, 'rb') do |f|
25
+ sequence = ::MIDI::Sequence.new
26
+ sequence.read(f)
27
+ pulses_per_beat = sequence.ppqn.to_f
28
+ track_idx = -1
29
+
30
+ for track in sequence
31
+ track_idx += 1
32
+ timeline = Timeline.new
33
+ note_ons = {}
34
+ #puts "TRACK #{track_idx}"
35
+
36
+ for event in track
37
+ #puts "#{event.class}: #{event} @#{event.time_from_start}"
38
+ case event
39
+ when ::MIDI::NoteOn
40
+ note_ons[event.note] = event
41
+
42
+ when ::MIDI::NoteOff
43
+ if on_event = note_ons.delete(event.note)
44
+ time = (on_event.time_from_start)/pulses_per_beat
45
+ duration = (event.time_from_start - on_event.time_from_start)/pulses_per_beat
46
+ note = Note.from_midi event.note, on_event.velocity, duration
47
+ timeline.add time, note
48
+ end
49
+ end
50
+ end
51
+ timelines << timeline
52
+ end
53
+ end
54
+ timelines
55
+ end
56
+
57
+ def write(anything)
58
+ case anything
59
+ when Timeline then
60
+ write_timeline(anything)
61
+ when Array then
62
+ write_timelines(anything)
63
+ else
64
+ raise "#{self.class}#write doesn't understand #{anything.class}"
65
+ end
66
+ end
67
+
68
+ def write_timelines(timelines, parent_sequence=nil)
69
+ sequence = parent_sequence || ::MIDI::Sequence.new
70
+ for timeline in timelines
71
+ write_timeline(timeline, sequence)
72
+ end
73
+ write_to_disk sequence unless parent_sequence
74
+ end
75
+
76
+ # Write the Timeline as a MIDI file
77
+ #
78
+ # @param [Timeline]
79
+ def write_timeline(timeline, parent_sequence=nil)
80
+ sequence = parent_sequence || ::MIDI::Sequence.new
81
+ clock_rate = sequence.ppqn
82
+ track = add_track sequence
83
+ channel = 1
84
+
85
+ for time, event in timeline
86
+ time *= clock_rate
87
+ case event
88
+ when Note
89
+ pitch, velocity = event.pitch, event.velocity
90
+ add_event track, time => note_on(channel, pitch, velocity)
91
+ duration = event.duration_in_pulses(clock_rate)
92
+ add_event track, time+duration => note_off(channel, pitch, velocity)
93
+
94
+ when Chord
95
+ velocity = event.velocity
96
+ duration = event.duration_in_pulses(clock_rate)
97
+ for pitch in event.pitches
98
+ pitch = pitch.to_i
99
+ add_event track, time => note_on(channel, pitch, velocity)
100
+ add_event track, time+duration => note_off(channel, pitch, velocity)
101
+ end
102
+
103
+ end
104
+ end
105
+ track.recalc_delta_from_times
106
+
107
+ write_to_disk sequence unless parent_sequence
108
+ end
109
+
110
+
111
+ ########################
112
+ private
113
+
114
+ def write_to_disk(sequence)
115
+ ::File.open(@file, 'wb') { |f| sequence.write f }
116
+ end
117
+
118
+ def print_midi sequence
119
+ for track in sequence
120
+ puts "\n*** track \"#{track.name}\""
121
+ puts "#{track.events.length} events"
122
+ for event in track
123
+ puts "#{event.to_s} (#{event.time_from_start})"
124
+ end
125
+ end
126
+ end
127
+
128
+ # Set tempo in terms of Quarter Notes per Minute (aka BPM)
129
+ def tempo(bpm)
130
+ ms_per_quarter_note = ::MIDI::Tempo.bpm_to_mpq(bpm)
131
+ ::MIDI::Tempo.new(ms_per_quarter_note)
132
+ end
133
+
134
+ def program(program_number)
135
+ ::MIDI::ProgramChange.new(channel, program_number)
136
+ end
137
+
138
+ def note_on(channel, pitch, velocity)
139
+ ::MIDI::NoteOn.new(channel, pitch.to_i, velocity)
140
+ end
141
+
142
+ def note_off(channel, pitch, velocity)
143
+ ::MIDI::NoteOff.new(channel, pitch.to_i, velocity)
144
+ end
145
+
146
+ def cc(channel, controller, value)
147
+ ::MIDI::Controller.new(channel, controller.to_i, value.to_i)
148
+ end
149
+
150
+ def pitch_bend(channel, value)
151
+ ::MIDI::PitchBend.new(channel, value)
152
+ end
153
+
154
+ def add_track sequence, opts={}
155
+ track = ::MIDI::Track.new(sequence)
156
+ track.name = opts.fetch :name, ''
157
+ sequence.tracks << track
158
+ track
159
+ end
160
+
161
+ def add_event track, event_hash
162
+ for time, event in event_hash
163
+ event.time_from_start = time
164
+ track.events << event
165
+ event
166
+ end
167
+ end
168
+
169
+ end
170
+
171
+ end
172
+
173
+ def MIDI_File(f)
174
+ MIDI::File.new(f)
175
+ end
176
+ module_function :MIDI_File
177
+
178
+ end
179
+
data/lib/mtk/note.rb ADDED
@@ -0,0 +1,44 @@
1
+ module MTK
2
+
3
+ # A musical #{Event} defined by a {Pitch}, intensity, and duration
4
+ class Note < Event
5
+
6
+ # frequency of the note as a Pitch
7
+ attr_reader :pitch
8
+
9
+ def initialize(pitch, intensity, duration)
10
+ @pitch = pitch
11
+ super(intensity, duration)
12
+ end
13
+
14
+ def self.from_hash(hash)
15
+ new hash[:pitch], hash[:intensity], hash[:duration]
16
+ end
17
+
18
+ def self.from_midi(pitch, velocity, beats)
19
+ new Pitches::PITCHES[pitch], velocity/127.0, beats
20
+ end
21
+
22
+ def to_hash
23
+ super.merge({ :pitch => @pitch })
24
+ end
25
+
26
+ def transpose(interval)
27
+ self.class.new(@pitch+interval, @intensity, @duration)
28
+ end
29
+
30
+ def == other
31
+ super and other.respond_to? :pitch and @pitch == other.pitch
32
+ end
33
+
34
+ def to_s
35
+ "Note(#{pitch}, #{super})"
36
+ end
37
+
38
+ def inspect
39
+ "Note(#{pitch}, #{super})"
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -0,0 +1,61 @@
1
+ class Numeric
2
+
3
+ def semitones
4
+ self
5
+ end
6
+
7
+ def cents
8
+ self/100.0
9
+ end
10
+
11
+ def minor_seconds
12
+ self
13
+ end
14
+
15
+ def major_seconds
16
+ self * 2
17
+ end
18
+
19
+ def minor_thirds
20
+ self * 3
21
+ end
22
+
23
+ def major_thirds
24
+ self * 4
25
+ end
26
+
27
+ def perfect_fourths
28
+ self * 5
29
+ end
30
+
31
+ def tritones
32
+ self * 6
33
+ end
34
+ alias augmented_fourths tritones
35
+ alias diminshed_fifths tritones
36
+
37
+ def perfect_fifths
38
+ self * 7
39
+ end
40
+
41
+ def minor_sixths
42
+ self * 8
43
+ end
44
+
45
+ def major_sixths
46
+ self * 9
47
+ end
48
+
49
+ def minor_sevenths
50
+ self * 10
51
+ end
52
+
53
+ def major_sevenths
54
+ self * 11
55
+ end
56
+
57
+ def octaves
58
+ self * 12
59
+ end
60
+
61
+ end
@@ -0,0 +1,21 @@
1
+ module MTK
2
+ module Pattern
3
+
4
+ # An element enumerator that randomly choices from a list of elements
5
+ class Choice
6
+
7
+ # The element choices
8
+ attr_reader :elements
9
+
10
+ def initialize(elements)
11
+ @elements = elements
12
+ end
13
+
14
+ def next
15
+ @elements[rand(@elements.length)] if @elements and not @elements.empty?
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+ end