sound 0.0.9 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- NzdkMGRkZDM3ODkxZTM4NDNkY2FhMjU3M2JiYTE2Y2MwM2M2Y2MzOQ==
4
+ YjA0NGNiZGY0MzRkN2IzMjMyY2JhZDhhMzM0OWI2ZDFiN2QzN2EyMQ==
5
5
  data.tar.gz: !binary |-
6
- NmUxZmE3NGMyZjQ1ZTExZjhmMmMyMTNhYmYyMTQ3ZDJmY2I3NjlmZg==
6
+ MmRkM2IyYjY4NTlhZjRkNDZiODc4ODhiN2RiN2ZiY2I1YjIxNDVmNw==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- N2MyNDZkYzE4Y2U4YjU4ZTBiN2YwOTRmNjk5OTFmMTMxMmZmNjI3NzljMTBk
10
- YmIxNmU1NzU0MDgxNzQ5NGQwZTQxNTQ4NjE0ODE4MmY4MDFjMzgyMjVlYWVl
11
- NjgyZjY3ZDQzZGQxYTZkZGUwYThiZmEwMWIzNTZlNzkxNmZjZTM=
9
+ Y2ExM2QwNDg5NjI2MjM1NmE2MzE0YTVmODdjMGFiMjNiZjM4ZDRiMzRlNTA4
10
+ ZTQwNjBmZTE1OGJiZmExZWQyNjA1Nzc1ZDk1YzM2NzViNWQ0NzM1NDdkMWMz
11
+ M2ExNDM3ODlhZjAyZWMwY2JiZmE1ZWFmYWZmZWFkYTAxNzQ2ODM=
12
12
  data.tar.gz: !binary |-
13
- ODJlYzA5MzQyYjljYzZjODY5OGM2NGUzMTBlODNjZTE0NDZmOWZmNzEwNDU0
14
- MWM3OWU3MDI3YzU3NzA3ODc2NzMwODJlMTE1NGExYWMyMGU3M2M5MmM4ZmU0
15
- NTFhMzEyNTM4ZGE3YTdkODVmNzkxOTgyYzY0NjU5ZDEwMzBmYmU=
13
+ Njk3N2I3NjY1Y2Q4ODk4ODYxMzU3NjA2ODIwZWI2ZmJhMWUwM2ZlMTA4Zjgz
14
+ NjdhNjQzZjA0OWY1YjdkZjc4ODk4YTEwM2RkZTM4N2ZlOWJmOTk1ZmZhOWNl
15
+ ZTA4NjllZGQ3ZmE0YTI2MTE2OTJhMjZjNWZmMTU2MTA3Yjg5MWY=
@@ -12,7 +12,7 @@ Sound.verbose = true
12
12
  device = Sound::Device.new("w")
13
13
 
14
14
  # creates a PCM format object
15
- format = Sound::Format.new(Sound::Format::PCM)
15
+ format = Sound::Format.new#(Sound::Format::PCM)
16
16
 
17
17
  # creates a new Sound Data object with a PCM format
18
18
  data = Sound::Data.new(format)
@@ -1,5 +1,9 @@
1
1
  module OS
2
2
 
3
+ class << self
4
+ attr_accessor :os
5
+ end
6
+
3
7
  def OS.windows?
4
8
  os == :windows
5
9
  end
@@ -17,21 +21,22 @@ module OS
17
21
  end
18
22
 
19
23
  def OS.os
20
- @os ||= (
21
- host_os = RbConfig::CONFIG['host_os']
22
- case host_os
23
- when /mswin|msys|mingw|cygwin|bccwin|wince|emc|emx/
24
- :windows
25
- when /darwin|mac os/
26
- :mac
27
- when /linux/
28
- :linux
29
- when /solaris|bsd/
30
- :unix
31
- else
32
- raise Error::WebDriverError, "unknown os: #{host_os.inspect}"
33
- end
34
- )
24
+ @os ||= OS.host_os
25
+ end
26
+
27
+ def OS.host_os
28
+ case RbConfig::CONFIG['host_os']
29
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc|emx/
30
+ :windows
31
+ when /darwin|mac os/
32
+ :mac
33
+ when /linux/
34
+ :linux
35
+ when /solaris|bsd/
36
+ :unix
37
+ else
38
+ raise Error::WebDriverError, "unknown os: #{RbConfig::CONFIG['host_os'].inspect}"
39
+ end
35
40
  end
36
41
 
37
42
  end
@@ -1,5 +1,3 @@
1
-
2
- require 'ffi'
3
1
  require 'pry'
4
2
  require 'os/os'
5
3
 
@@ -7,10 +5,10 @@ module Sound
7
5
 
8
6
  @verbose = false
9
7
  @no_device = false
10
- @platform_supported = false
8
+ @platform_supported = true
11
9
 
12
10
  class << self
13
- attr_accessor :verbose, :no_device, :platform_supported
11
+ attr_accessor :verbose, :no_device, :platform_supported, :device_library, :format_library
14
12
  end
15
13
 
16
14
  class NoDeviceError < RuntimeError; end
@@ -19,29 +17,29 @@ module Sound
19
17
  end
20
18
 
21
19
  if OS.windows?
22
- require 'sound/win32'
23
- module Sound
24
- class Device
25
- include Win32
26
- end
27
- end
28
- Sound.platform_supported = true
20
+ require 'sound/device_library/mmlib'
21
+ require 'sound/format_library/mmlib'
22
+ Sound.device_library = Sound::DeviceLibrary::MMLib
23
+ Sound.format_library = Sound::FormatLibrary::MMLib
29
24
  elsif OS.linux?
30
25
  libasound_present = !(`which aplay`.eql? "")
31
- unless libasound_present
26
+ if libasound_present
27
+ require 'sound/device_library/alsa'
28
+ require 'sound/format_library/alsa'
29
+ Sound.device_library = Sound::DeviceLibrary::ALSA
30
+ Sound.format_library = Sound::FormatLibrary::ALSA
31
+ else
32
32
  warn("warning: sound output requires libasound2, libasound2-dev, and alsa-utils packages")
33
33
  end
34
- require 'sound/alsa'
35
- module Sound
36
- class Device
37
- include ALSA
38
- end
39
- end
40
- Sound.platform_supported = true
41
34
  else
35
+ Sound.device_library = Sound::DeviceLibrary::Base
36
+ Sound.format_library = Sound::FormatLibrary::Base
42
37
  warn("warning: Sound output not yet implemented for this platform: #{OS.os}")
38
+ Sound.platform_supported = false
43
39
  end
44
40
 
41
+ require 'sound/device_library'
42
+ require 'sound/format_library'
45
43
  require 'sound/device'
46
44
  require 'sound/data'
47
45
  require 'sound/format'
@@ -7,9 +7,6 @@ module Sound
7
7
  @format = format
8
8
  @pcm_data = []
9
9
  end
10
- def to_s
11
- puts "#<Sound::Data:#{object_id}>"
12
- end
13
10
  def generate_sine_wave(freq, duration, volume)
14
11
  @pcm_data = []
15
12
  @duration = duration
@@ -2,8 +2,14 @@
2
2
  module Sound
3
3
 
4
4
  class Device
5
+ include DeviceLibrary
5
6
 
6
- class Buffer < Array; end
7
+ class Buffer < Array
8
+ attr_accessor :force
9
+ def new_block
10
+ self.force || self.empty? || self.last.kind_of?(Thread)
11
+ end
12
+ end
7
13
 
8
14
  attr_reader :status, :id
9
15
 
@@ -12,7 +18,7 @@ module Sound
12
18
  # if a block is passed, it executes the code in the block, passing
13
19
  # the newly created device, and then closes the device.
14
20
  #
15
- def initialize(direction = "w", id = DEFAULT_DEVICE_ID, &block)
21
+ def initialize(direction = "w", id = DeviceLibrary::DEFAULT_DEVICE_ID, &block)
16
22
 
17
23
  @id = id
18
24
  @status = :open
@@ -87,7 +93,7 @@ module Sound
87
93
  #
88
94
  def write(data = Sound::Data.new)
89
95
  if closed?
90
- puts "cannot write to a closed device"
96
+ warn("warning: cannot write to a closed device")
91
97
  else
92
98
  @mutex.lock
93
99
  @queue << Thread.new do
@@ -104,42 +110,49 @@ module Sound
104
110
  # starts up data block threads that get played back at the same time. Need
105
111
  # to make all threads wait until others are finished preparing the buffer.
106
112
  #
107
- def write_async(data = Sound::Data.new, new_queue_elem = false)
113
+ def write_async(data = Sound::Data.new, force_new = false)
108
114
  if closed?
109
- puts "cannot write to a closed device"
115
+ warn("warning: cannot write to a closed device")
110
116
  else
111
117
  @mutex.lock
112
- if new_queue_elem || @queue.empty? || @queue.last.kind_of?(Thread)
113
- threads = []
114
- threads << Thread.new do
115
- Thread.current[:async] = true
116
- Thread.current[:data] = data
117
- write_thread
118
- end
119
- @queue << threads
118
+ @queue.force = force_new
119
+ if @queue.new_block
120
+ @queue << [new_async_thread_for(data)]
120
121
  else
121
- @queue.last << Thread.new do
122
- Thread.current[:async] = true
123
- Thread.current[:data] = data
124
- write_thread
125
- end
122
+ @queue.last << new_async_thread_for(data)
126
123
  end
127
124
  @mutex.unlock
128
125
  puts "writing async to queue of device '#{id}': #{data}" if Sound.verbose
129
126
  end
127
+ self
130
128
  end
131
129
 
132
- # should make a close! method that ignores any pending queue data blocks,
133
- # but still safely closes the device as quickly as possible.
130
+ # flushes any pending queue data blocks, waits for them to finish playing,
131
+ # and then closes the device.
134
132
  #
135
133
  def close
136
134
  if closed?
137
- puts "cannot close a closed device"
135
+ warn("warning: device is already closed")
138
136
  else
139
137
  flush
140
138
  puts "device '#{id}' is closing now" if Sound.verbose
141
139
  @status = :closed
142
140
  end
141
+ self
142
+ end
143
+
144
+ # closes the device as quickly as possible without flushing the
145
+ # data buffer.
146
+ #
147
+ def close!
148
+ if closed?
149
+ warn("warning: device is already closed")
150
+ else
151
+ flush!
152
+ puts "device '#{id}' is closing immediately now" if Sound.verbose
153
+ @status = :closed
154
+ end
155
+ self
143
156
  end
144
157
 
145
158
  # flushes each block after previous finishes. Should make other options,
@@ -149,36 +162,62 @@ module Sound
149
162
  until @queue.empty?
150
163
  output = @queue.shift
151
164
  if output.kind_of? Thread
152
- output[:stop] = false
153
- puts "writing to device '#{id}': #{output[:data].class}" if Sound.verbose #this may be NilClass if parent thread is too fast
154
- output.run.join
165
+ finish_up(output).join
155
166
  else
156
167
  output.each do |thread|
157
- thread[:stop] = false
158
- puts "writing to device '#{id}': #{thread[:data].class}" if Sound.verbose
159
- thread.run
168
+ finish_up(thread)
160
169
  end
161
170
  output.last.join if output.last.alive?
162
171
  end
163
172
  end
164
173
  end
165
174
 
175
+ # works like #flush, but empties the buffer without playing any sounds and
176
+ # closes the physical device as quickly as possible for its platform.
177
+ #
178
+ def flush!
179
+ @queue = Device::Buffer.new
180
+ self
181
+ end
182
+
166
183
  private
167
184
 
168
185
  def write_thread
169
186
  Thread.current[:stop] = true if Thread.current[:stop].nil?
170
187
  if Sound.platform_supported
171
- open_device
172
- prepare_buffer
173
- Thread.stop if Thread.current[:stop]
174
- Thread.pass if Thread.current[:async]
188
+ set_up
175
189
  write_to_device
176
- unprepare_buffer
177
- close_device
190
+ tear_down
178
191
  else
179
192
  warn("warning: playback is not yet supported on this platform")
180
193
  end
181
194
  end
195
+
196
+ def set_up
197
+ open_device(self)
198
+ prepare_buffer
199
+ Thread.stop if Thread.current[:stop]
200
+ Thread.pass if Thread.current[:async]
201
+ end
202
+
203
+ def tear_down
204
+ unprepare_buffer
205
+ close_device
206
+ end
207
+
208
+ def new_async_thread_for(data)
209
+ Thread.new do
210
+ Thread.current[:async] = true
211
+ Thread.current[:data] = data
212
+ write_thread
213
+ end
214
+ end
215
+
216
+ def finish_up(thread)
217
+ thread[:stop] = false
218
+ puts "writing to device '#{id}': #{thread[:data].class}" if Sound.verbose #this may be NilClass if parent thread is too fast
219
+ thread.run
220
+ end
182
221
  end
183
222
 
184
223
  end
@@ -0,0 +1,19 @@
1
+ require 'forwardable'
2
+
3
+ module Sound
4
+ module DeviceLibrary
5
+ extend Forwardable
6
+ DEFAULT_DEVICE_ID = Sound.device_library::DEFAULT_DEVICE_ID
7
+ duties = [
8
+ :open_device,
9
+ :prepare_buffer,
10
+ :write_to_device,
11
+ :unprepare_buffer,
12
+ :close_device
13
+ ]
14
+ duties.each do |duty|
15
+ delegate duty => Sound.device_library
16
+ private duty
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,240 @@
1
+ require 'ffi'
2
+
3
+ module Sound
4
+ module DeviceLibrary
5
+ module ALSA
6
+ extend self
7
+
8
+ class Handle
9
+ def initialize
10
+ @handle = FFI::MemoryPointer.new(:pointer)
11
+ end
12
+ def pointer
13
+ @handle
14
+ end
15
+ def id
16
+ @handle.read_pointer
17
+ end
18
+ end
19
+
20
+ SND_PCM_STREAM_PLAYBACK = 0
21
+ SND_PCM_STREAM_CAPTURE = 1
22
+ DEFAULT_DEVICE_ID = "default"
23
+
24
+ extend FFI::Library
25
+ ffi_lib 'asound'
26
+ ffi_convention :stdcall
27
+
28
+ attach_function :snd_pcm_open, [:pointer, :string, :int, :int], :int
29
+ attach_function :snd_pcm_close, [:pointer], :int
30
+ attach_function :snd_pcm_drain, [:pointer], :int
31
+ attach_function :snd_pcm_prepare, [:pointer], :int
32
+ attach_function :write_noninterleaved, :snd_pcm_writen, [:pointer, :pointer, :ulong], :long
33
+ attach_function :snd_pcm_writei, [:pointer, :pointer, :ulong], :long
34
+ attach_function :snd_pcm_hw_params_malloc, [:pointer], :int
35
+ attach_function :snd_pcm_hw_params_any, [:pointer, :pointer], :int
36
+ attach_function :snd_pcm_hw_params_set_access, [:pointer, :pointer, :int], :int
37
+ attach_function :snd_pcm_hw_params_set_format, [:pointer, :pointer, :int], :int
38
+ attach_function :snd_pcm_hw_params_set_rate, [:pointer, :pointer, :uint, :int], :int
39
+ attach_function :snd_pcm_hw_params_set_channels, [:pointer, :pointer, :int], :int
40
+ attach_function :snd_pcm_hw_params, [:pointer, :pointer], :int
41
+ attach_function :snd_pcm_hw_params_free, [:pointer], :void
42
+
43
+ def self.snd_pcm_open(*args)
44
+ output = `aplay -l 2>&1`
45
+ if output.match(/no soundcard/m)
46
+ raise NoDeviceError, "No sound devices present"
47
+ elsif output.match(/not found/m)
48
+ raise NoDependencyError,
49
+ "aplay is not present in your environment. Install alsa-utils package for audio playback."
50
+ else
51
+ snd_pcm_open(*args)
52
+ end
53
+ end
54
+
55
+ def open_device(device)
56
+ begin
57
+ self.snd_pcm_open(handle.pointer, device.id, 0, ASYNC)
58
+ rescue NoDeviceError
59
+ Sound.no_device = true
60
+ end
61
+ end
62
+
63
+ def prepare_buffer
64
+
65
+ unless Sound.no_device
66
+ buffer_length
67
+ set_params
68
+ snd_pcm_prepare(handle.id)
69
+ end
70
+
71
+ end
72
+
73
+ def write_to_device
74
+ snd_pcm_writei(handle.id, data_buffer, buffer_length) unless Sound.no_device
75
+ end
76
+
77
+ def unprepare_buffer
78
+ snd_pcm_drain(handle.id) unless Sound.no_device
79
+ end
80
+
81
+ def close_device
82
+ snd_pcm_close(handle.id) unless Sound.no_device
83
+ end
84
+
85
+ def params_handle
86
+ Thread.current[:params_handle] ||= Handle.new
87
+ end
88
+
89
+ def buffer_length
90
+ Thread.current[:buffer_length] ||= data_buffer.size/2
91
+ end
92
+
93
+ def handle
94
+ Thread.current[:handle] ||= Handle.new
95
+ end
96
+
97
+ def data
98
+ Thread.current[:data] ||= Sound::Data.new
99
+ end
100
+
101
+ def data_buffer
102
+ Thread.current[:data_buffer] ||= FFI::MemoryPointer.new(:int, data.pcm_data.size).write_array_of_int data.pcm_data
103
+ end
104
+
105
+ def set_params
106
+ allocate_param_memory
107
+ alter_allocated_param_memory
108
+ free_param_memory
109
+ end
110
+
111
+ def allocate_param_memory
112
+ snd_pcm_hw_params_malloc(params_handle.pointer)
113
+ end
114
+
115
+ def free_param_memory
116
+ snd_pcm_hw_params_free(params_handle.id)
117
+ end
118
+
119
+ def alter_allocated_param_memory
120
+ set_up_to_change_any_params
121
+ modify_desired_params
122
+ set_params_on_hw_buffer
123
+ end
124
+
125
+ def set_up_to_change_any_params
126
+ snd_pcm_hw_params_any(handle.id, params_handle.id)
127
+ end
128
+
129
+ def modify_desired_params
130
+ # I think these can happen in any order
131
+ snd_pcm_hw_params_set_access(handle.id, params_handle.id, SND_PCM_ACCESS_RW_INTERLEAVED)
132
+ snd_pcm_hw_params_set_format(handle.id, params_handle.id, SND_PCM_FORMAT_S16_LE)
133
+ # need to change this to set_rate_near at some point
134
+ snd_pcm_hw_params_set_rate(handle.id, params_handle.id, data.format.sample_rate, 0)
135
+ snd_pcm_hw_params_set_channels(handle.id, params_handle.id, data.format.channels)
136
+ end
137
+
138
+ def set_params_on_hw_buffer
139
+ snd_pcm_hw_params(handle.id, params_handle.id)
140
+ end
141
+
142
+ SND_PCM_ASYNC = 2
143
+ ASYNC = SND_PCM_ASYNC
144
+ #snd_pcm formats
145
+ # Unknown
146
+ SND_PCM_FORMAT_UNKNOWN = -1
147
+ # Signed 8 bit
148
+ SND_PCM_FORMAT_S8 = 0
149
+ # Unsigned 8 bit
150
+ SND_PCM_FORMAT_U8 = 1
151
+ # Signed 16 bit Little Endian
152
+ SND_PCM_FORMAT_S16_LE = 2
153
+ # Signed 16 bit Big Endian
154
+ SND_PCM_FORMAT_S16_BE = 3
155
+ # Unsigned 16 bit Little Endian
156
+ SND_PCM_FORMAT_U16_LE = 4
157
+ # Unsigned 16 bit Big Endian
158
+ SND_PCM_FORMAT_U16_BE = 5
159
+ # Signed 24 bit Little Endian using low three bytes in 32-bit word
160
+ SND_PCM_FORMAT_S24_LE = 6
161
+ # Signed 24 bit Big Endian using low three bytes in 32-bit word
162
+ SND_PCM_FORMAT_S24_BE = 7
163
+ # Unsigned 24 bit Little Endian using low three bytes in 32-bit word
164
+ SND_PCM_FORMAT_U24_LE = 8
165
+ # Unsigned 24 bit Big Endian using low three bytes in 32-bit word
166
+ SND_PCM_FORMAT_U24_BE = 9
167
+ # Signed 32 bit Little Endian
168
+ SND_PCM_FORMAT_S32_LE = 10
169
+ # Signed 32 bit Big Endian
170
+ SND_PCM_FORMAT_S32_BE = 11
171
+ # Unsigned 32 bit Little Endian
172
+ SND_PCM_FORMAT_U32_LE = 12
173
+ # Unsigned 32 bit Big Endian
174
+ SND_PCM_FORMAT_U32_BE = 13
175
+ # Float 32 bit Little Endian, Range -1.0 to 1.0
176
+ SND_PCM_FORMAT_FLOAT_LE = 14
177
+ # Float 32 bit Big Endian, Range -1.0 to 1.0
178
+ SND_PCM_FORMAT_FLOAT_BE = 15
179
+ # Float 64 bit Little Endian, Range -1.0 to 1.0
180
+ SND_PCM_FORMAT_FLOAT64_LE = 16
181
+ # Float 64 bit Big Endian, Range -1.0 to 1.0
182
+ SND_PCM_FORMAT_FLOAT64_BE = 17
183
+ # IEC-958 Little Endian
184
+ SND_PCM_FORMAT_IEC958_SUBFRAME_LE = 18
185
+ # IEC-958 Big Endian
186
+ SND_PCM_FORMAT_IEC958_SUBFRAME_BE = 19
187
+ # Mu-Law
188
+ SND_PCM_FORMAT_MU_LAW = 20
189
+ # A-Law
190
+ SND_PCM_FORMAT_A_LAW = 21
191
+ # Ima-ADPCM
192
+ SND_PCM_FORMAT_IMA_ADPCM = 22
193
+ # MPEG
194
+ SND_PCM_FORMAT_MPEG = 23
195
+ # GSM
196
+ SND_PCM_FORMAT_GSM = 24
197
+ # Special
198
+ SND_PCM_FORMAT_SPECIAL = 31
199
+ # Signed 24bit Little Endian in 3bytes format
200
+ SND_PCM_FORMAT_S24_3LE = 32
201
+ # Signed 24bit Big Endian in 3bytes format
202
+ SND_PCM_FORMAT_S24_3BE = 33
203
+ # Unsigned 24bit Little Endian in 3bytes format
204
+ SND_PCM_FORMAT_U24_3LE = 34
205
+ # Unsigned 24bit Big Endian in 3bytes format
206
+ SND_PCM_FORMAT_U24_3BE = 35
207
+ # Signed 20bit Little Endian in 3bytes format
208
+ SND_PCM_FORMAT_S20_3LE = 36
209
+ # Signed 20bit Big Endian in 3bytes format
210
+ SND_PCM_FORMAT_S20_3BE = 37
211
+ # Unsigned 20bit Little Endian in 3bytes format
212
+ SND_PCM_FORMAT_U20_3LE = 38
213
+ # Unsigned 20bit Big Endian in 3bytes format
214
+ SND_PCM_FORMAT_U20_3BE = 39
215
+ # Signed 18bit Little Endian in 3bytes format
216
+ SND_PCM_FORMAT_S18_3LE = 40
217
+ # Signed 18bit Big Endian in 3bytes format
218
+ SND_PCM_FORMAT_S18_3BE = 41
219
+ # Unsigned 18bit Little Endian in 3bytes format
220
+ SND_PCM_FORMAT_U18_3LE = 42
221
+ # Unsigned 18bit Big Endian in 3bytes format
222
+ SND_PCM_FORMAT_U18_3BE = 43
223
+ SND_PCM_FORMAT_LAST = SND_PCM_FORMAT_U18_3BE
224
+
225
+ #snd_pcm access
226
+ # mmap access with simple interleaved channels
227
+ SND_PCM_ACCESS_MMAP_INTERLEAVED = 0
228
+ # mmap access with simple non interleaved channels
229
+ SND_PCM_ACCESS_MMAP_NONINTERLEAVED = 1
230
+ # mmap access with complex placement
231
+ SND_PCM_ACCESS_MMAP_COMPLEX = 2
232
+ # snd_pcm_readi/snd_pcm_writei access
233
+ SND_PCM_ACCESS_RW_INTERLEAVED = 3
234
+ # snd_pcm_readn/snd_pcm_writen access
235
+ SND_PCM_ACCESS_RW_NONINTERLEAVED = 4
236
+ SND_PCM_ACCESS_LAST = SND_PCM_ACCESS_RW_NONINTERLEAVED
237
+
238
+ end
239
+ end
240
+ end