carrierwave-audio-waveform 1.0.0

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b817a62eeaac43003254b3790fdc864faf0a0980
4
+ data.tar.gz: 84176297d0547e499d84bedef2e22585618e1e8e
5
+ SHA512:
6
+ metadata.gz: c321c0dcaf6d14e6a8c6526c532eca40cb30ecf5690fae3896c1185d308344621974d310fff12cda3446ff210a8747c536de918fed7d13cacca7262e8851f49f
7
+ data.tar.gz: 42ce80dacf18c25b68f7732e5983f4547a505bd991c50b9b7eddcaf62fce51c549e200c2dd7af6b5e8b41fbdd592456a16733ffb4ee13f88d437e526a1490dd2
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Jiri Kolarik
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,35 @@
1
+ # CarrierWave::AudioWaveform
2
+
3
+ Generate waveform images from audio files within Carrierwave
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'carrierwave-audio-waveform'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install carrierwave-audio-waveform
18
+
19
+ ## Usage
20
+
21
+ Include CarrierWave::AudioWaveform into your CarrierWave uploader class:
22
+
23
+ ```ruby
24
+ class AudioUploader < CarrierWave::Uploader::Base
25
+ include CarrierWave::AudioWaveform
26
+ end
27
+ ```
28
+
29
+ ## Contributing
30
+
31
+ 1. Fork it
32
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
33
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
34
+ 4. Push to the branch (`git push origin my-new-feature`)
35
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require 'carrierwave/audio_waveform'
@@ -0,0 +1,21 @@
1
+ require 'carrierwave'
2
+ require 'carrierwave/audio_waveform/waveformer'
3
+
4
+ module CarrierWave
5
+ module AudioWaveform
6
+ module ClassMethods
7
+ extend ActiveSupport::Concern
8
+
9
+ def waveform options={}
10
+ process waveform: [ options ]
11
+ end
12
+ end
13
+
14
+ def waveform options={}
15
+ cache_stored_file! if !cached?
16
+
17
+ image_filename = Waveformer.generate(current_path, options)
18
+ File.rename image_filename, current_path
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ module CarrierWave
2
+ module AudioWaveform
3
+ VERSION = '1.0.0'
4
+ end
5
+ end
@@ -0,0 +1,397 @@
1
+ require 'ruby-audio'
2
+ require 'ruby-sox'
3
+ require 'oily_png'
4
+ require 'fileutils'
5
+
6
+ module CarrierWave
7
+ module AudioWaveform
8
+ class Waveformer
9
+ DefaultOptions = {
10
+ :method => :peak,
11
+ :width => 1800,
12
+ :height => 280,
13
+ :background_color => "#666666",
14
+ :color => "#00ccff",
15
+ :logger => nil
16
+ }
17
+
18
+ TransparencyMask = "#00ff00"
19
+ TransparencyAlternate = "#ffff00" # in case the mask is the background color!
20
+
21
+ attr_reader :source
22
+
23
+ # Scope these under Waveform so you can catch the ones generated by just this
24
+ # class.
25
+ class RuntimeError < ::RuntimeError;end;
26
+ class ArgumentError < ::ArgumentError;end;
27
+
28
+ class << self
29
+ # Generate a Waveform image at the given filename with the given options.
30
+ #
31
+ # Available options (all optional) are:
32
+ #
33
+ # :method => The method used to read sample frames, available methods
34
+ # are peak and rms. peak is probably what you're used to seeing, it uses
35
+ # the maximum amplitude per sample to generate the waveform, so the
36
+ # waveform looks more dynamic. RMS gives a more fluid waveform and
37
+ # probably more accurately reflects what you hear, but isn't as
38
+ # pronounced (typically).
39
+ #
40
+ # Can be :rms or :peak
41
+ # Default is :peak.
42
+ #
43
+ # :width => The width (in pixels) of the final waveform image.
44
+ # Default is 1800.
45
+ #
46
+ # :height => The height (in pixels) of the final waveform image.
47
+ # Default is 280.
48
+ #
49
+ # :auto_width => msec per pixel. This will overwrite the width of the
50
+ # final waveform image depending on the length of the audio file.
51
+ # Example:
52
+ # 100 => 1 pixel per 100 msec; a one minute audio file will result in a width of 600 pixels
53
+ #
54
+ # :background_color => Hex code of the background color of the generated
55
+ # waveform image. Pass :transparent for transparent background.
56
+ # Default is #666666 (gray).
57
+ #
58
+ # :color => Hex code of the color to draw the waveform, or can pass
59
+ # :transparent to render the waveform transparent (use w/ a solid
60
+ # color background to achieve a "cutout" effect).
61
+ # Default is #00ccff (cyan-ish).
62
+ #
63
+ # :sample_width => Integer specifying the sample width. If this
64
+ # is specified, there will be gaps (minimum of 1px wide, as specified
65
+ # by :gap_width) between samples that are this wide in pixels.
66
+ # Default is nil
67
+ # Minimum is 1 (for anything other than nil)
68
+ #
69
+ # :gap_width => Integer specifying the gap width. If sample_width
70
+ # is specified, this will be the size of the gaps between samples in pixels.
71
+ # Default is nil
72
+ # Minimum is 1 (for anything other than nil, or when sample_width is present but gap_width is not)
73
+ #
74
+ # :logger => IOStream to log progress to.
75
+ #
76
+ # Example:
77
+ # CarrierWave::AudioWaveform::Waveformer.generate("Kickstart My Heart.wav")
78
+ # CarrierWave::AudioWaveform::Waveformer.generate("Kickstart My Heart.wav", :method => :rms)
79
+ # CarrierWave::AudioWaveform::Waveformer.generate("Kickstart My Heart.wav", :color => "#ff00ff", :logger => $stdout)
80
+ #
81
+ def generate(source, options={})
82
+ options = DefaultOptions.merge(options)
83
+ filename = options[:filename] || self.generate_png_filename(source)
84
+
85
+ raise ArgumentError.new("No source audio filename given, must be an existing sound file.") unless source
86
+ raise ArgumentError.new("No destination filename given for waveform") unless filename
87
+ raise RuntimeError.new("Source audio file '#{source}' not found.") unless File.exist?(source)
88
+
89
+ old_source = source
90
+ source = generate_wav_source(source)
91
+
92
+ @log = Log.new(options[:logger])
93
+ @log.start!
94
+
95
+ if options[:auto_width]
96
+ RubyAudio::Sound.open(source) do |audio|
97
+ options[:width] = (audio.info.length * 1000 / options[:auto_width].to_i).ceil
98
+ end
99
+ end
100
+
101
+ # Frames gives the amplitudes for each channel, for our waveform we're
102
+ # saying the "visual" amplitude is the average of the amplitude across all
103
+ # the channels. This might be a little weird w/ the "peak" method if the
104
+ # frames are very wide (i.e. the image width is very small) -- I *think*
105
+ # the larger the frames are, the more "peaky" the waveform should get,
106
+ # perhaps to the point of inaccurately reflecting the actual sound.
107
+ samples = frames(source, options[:width], options[:method]).collect do |frame|
108
+ frame.inject(0.0) { |sum, peak| sum + peak } / frame.size
109
+ end
110
+
111
+ @log.timed("\nDrawing...") do
112
+ # Don't remove the file until we're sure the
113
+ # source was readable
114
+ if File.exists?(filename)
115
+ @log.out("Output file #{filename} encountered. Removing.")
116
+ File.unlink(filename)
117
+ end
118
+
119
+ image = draw samples, options
120
+ image.save filename
121
+ end
122
+
123
+ if source != old_source
124
+ @log.out("Removing temporary file at #{source}")
125
+ FileUtils.rm(source)
126
+ end
127
+
128
+ @log.done!("Generated waveform '#{filename}'")
129
+
130
+ filename
131
+ end
132
+
133
+ def generate_png_filename(source)
134
+ ext = File.extname(source)
135
+ source_file_path_without_extension = File.join File.dirname(source), File.basename(source, ext)
136
+ "#{source_file_path_without_extension}.png"
137
+ end
138
+
139
+ private
140
+
141
+
142
+ # Returns a wav file if one was not passed in, or the original if it was
143
+ def generate_wav_source(source)
144
+ ext = File.extname(source)
145
+ ext_gsubbed = ext.gsub(/\./, '')
146
+
147
+ if ext != ".wav"
148
+ input_options = { type: ext_gsubbed }
149
+ output_options = { type: "wav" }
150
+ source_filename_without_extension = File.basename(source, ext)
151
+ output_file_path = File.join File.dirname(source), "tmp_#{source_filename_without_extension}_#{Time.now.to_i}.wav"
152
+ converter = Sox::Cmd.new
153
+ converter.add_input source, input_options
154
+ converter.set_output output_file_path, output_options
155
+ converter.run
156
+ output_file_path
157
+ else
158
+ source
159
+ end
160
+ rescue Sox::Error => e
161
+ raise e unless e.message.include?("FAIL formats:")
162
+ raise RuntimeError.new("Source file #{source} could not be converted to .wav by Sox (Sox: #{e.message})")
163
+ end
164
+
165
+ # Returns a sampling of frames from the given RubyAudio::Sound using the
166
+ # given method the sample size is determined by the given pixel width --
167
+ # we want one sample frame per horizontal pixel.
168
+ def frames(source, width, method = :peak)
169
+ raise ArgumentError.new("Unknown sampling method #{method}") unless [ :peak, :rms ].include?(method)
170
+
171
+ frames = []
172
+
173
+ RubyAudio::Sound.open(source) do |audio|
174
+ frames_read = 0
175
+ frames_per_sample = (audio.info.frames.to_f / width.to_f).to_i
176
+ sample = RubyAudio::Buffer.new("float", frames_per_sample, audio.info.channels)
177
+
178
+ @log.timed("Sampling #{frames_per_sample} frames per sample: ") do
179
+ while(frames_read = audio.read(sample)) > 0
180
+ frames << send(method, sample, audio.info.channels)
181
+ @log.out(".")
182
+ end
183
+ end
184
+ end
185
+
186
+ frames
187
+ rescue RubyAudio::Error => e
188
+ raise e unless e.message == "File contains data in an unknown format."
189
+ raise RuntimeError.new("Source audio file #{source} could not be read by RubyAudio library -- Hint: non-WAV files are no longer supported, convert to WAV first using something like ffmpeg (RubyAudio: #{e.message})")
190
+ end
191
+
192
+ # Draws the given samples using the given options, returns a ChunkyPNG::Image.
193
+ def draw(samples, options)
194
+ image = ChunkyPNG::Image.new(options[:width], options[:height],
195
+ options[:background_color] == :transparent ? ChunkyPNG::Color::TRANSPARENT : options[:background_color]
196
+ )
197
+
198
+ if options[:color] == :transparent
199
+ color = transparent = ChunkyPNG::Color.from_hex(
200
+ # Have to do this little bit because it's possible the color we were
201
+ # intending to use a transparency mask *is* the background color, and
202
+ # then we'd end up wiping out the whole image.
203
+ options[:background_color].downcase == TransparencyMask ? TransparencyAlternate : TransparencyMask
204
+ )
205
+ else
206
+ color = ChunkyPNG::Color.from_hex(options[:color])
207
+ end
208
+
209
+ # Calling "zero" the middle of the waveform, like there's positive and
210
+ # negative amplitude
211
+ zero = options[:height] / 2.0
212
+
213
+ # If a sample_width is passed, let's space those things out
214
+ if options[:sample_width]
215
+ samples = spaced_samples(samples, options[:sample_width], options[:gap_width])
216
+ end
217
+
218
+ samples.each_with_index do |sample, x|
219
+ next if sample.nil?
220
+ # Half the amplitude goes above zero, half below
221
+ amplitude = sample * options[:height].to_f / 2.0
222
+ # If you give ChunkyPNG floats for pixel positions all sorts of things
223
+ # go haywire.
224
+ image.line(x, (zero - amplitude).round, x, (zero + amplitude).round, color)
225
+ end
226
+
227
+ # Simple transparency masking, it just loops over every pixel and makes
228
+ # ones which match the transparency mask color completely clear.
229
+ if transparent
230
+ (0..image.width - 1).each do |x|
231
+ (0..image.height - 1).each do |y|
232
+ image[x, y] = ChunkyPNG::Color.rgba(0, 0, 0, 0) if image[x, y] == transparent
233
+ end
234
+ end
235
+ end
236
+
237
+ image
238
+ end
239
+
240
+ def spaced_samples samples, sample_width = 1, gap_width = 1
241
+ sample_width = sample_width.to_i >= 1 ? sample_width.to_i : 1
242
+ gap_width = gap_width.to_i >= 1 ? gap_width.to_i : 1
243
+ width_counter = sample_width
244
+ current_sample_index = 0
245
+ spaced_samples = []
246
+ avg = nil
247
+ while samples[current_sample_index]
248
+ at_front_of_image = current_sample_index < sample_width
249
+
250
+ # This determines if it's a gap, but we don't want
251
+ # a gap to start with, hence the last booelan check
252
+ if width_counter.to_i > sample_width.to_i && !at_front_of_image
253
+ # This is a gap
254
+ spaced_samples << nil
255
+ width_counter -= 1
256
+ else
257
+ # This is a sample
258
+ # If this is a new block of samples, get the average
259
+ if avg.nil?
260
+ avg = calculate_avg_sample(samples, current_sample_index, sample_width)
261
+ end
262
+ spaced_samples << avg
263
+ # This is 1-indexed since it starts at sample_width
264
+ # (or sample_width + gap_width for anything other than the initial passes)
265
+ if width_counter.to_i < 2
266
+ width_counter = sample_width + gap_width
267
+ avg = nil
268
+ else
269
+ width_counter -= 1
270
+ end
271
+ end
272
+ current_sample_index += 1
273
+ end
274
+
275
+ spaced_samples
276
+ end
277
+
278
+ # Calculate the average of a group of samples
279
+ # Return the sample's value if it's a group of 1
280
+ def calculate_avg_sample(samples, current_sample_index, sample_width)
281
+ if sample_width > 1
282
+ floats = samples[current_sample_index..(current_sample_index + sample_width - 1)].collect(&:to_f)
283
+ #floats.inject(:+) / sample_width
284
+ channel_rms(floats)
285
+ else
286
+ samples[current_sample_index]
287
+ end
288
+ end
289
+
290
+ # Returns an array of the peak of each channel for the given collection of
291
+ # frames -- the peak is individual to the channel, and the returned collection
292
+ # of peaks are not (necessarily) from the same frame(s).
293
+ def peak(frames, channels=1)
294
+ peak_frame = []
295
+ (0..channels-1).each do |channel|
296
+ peak_frame << channel_peak(frames, channel)
297
+ end
298
+ peak_frame
299
+ end
300
+
301
+ # Returns an array of rms values for the given frameset where each rms value is
302
+ # the rms value for that channel.
303
+ def rms(frames, channels=1)
304
+ rms_frame = []
305
+ (0..channels-1).each do |channel|
306
+ rms_frame << channel_rms(frames, channel)
307
+ end
308
+ rms_frame
309
+ end
310
+
311
+ # Returns the peak voltage reached on the given channel in the given collection
312
+ # of frames.
313
+ #
314
+ # TODO: Could lose some resolution and only sample every other frame, would
315
+ # likely still generate the same waveform as the waveform is so comparitively
316
+ # low resolution to the original input (in most cases), and would increase
317
+ # the analyzation speed (maybe).
318
+ def channel_peak(frames, channel=0)
319
+ peak = 0.0
320
+ frames.each do |frame|
321
+ next if frame.nil?
322
+ frame = Array(frame)
323
+ peak = frame[channel].abs if frame[channel].abs > peak
324
+ end
325
+ peak
326
+ end
327
+
328
+ # Returns the rms value across the given collection of frames for the given
329
+ # channel.
330
+ def channel_rms(frames, channel=0)
331
+ Math.sqrt(frames.inject(0.0){ |sum, frame| sum += (frame ? Array(frame)[channel] ** 2 : 0) } / frames.size)
332
+ end
333
+ end
334
+ end
335
+
336
+ class Waveformer
337
+ # A simple class for logging + benchmarking, nice to have good feedback on a
338
+ # long batch operation.
339
+ #
340
+ # There's probably 10,000,000 other bechmarking classes, but writing this was
341
+ # easier than using Google.
342
+ class Log
343
+ attr_accessor :io
344
+
345
+ def initialize(io=$stdout)
346
+ @io = io
347
+ end
348
+
349
+ # Prints the given message to the log
350
+ def out(msg)
351
+ io.print(msg) if io
352
+ end
353
+
354
+ # Prints the given message to the log followed by the most recent benchmark
355
+ # (note that it calls .end! which will stop the benchmark)
356
+ def done!(msg="")
357
+ out "#{msg} (#{self.end!}s)\n"
358
+ end
359
+
360
+ # Starts a new benchmark clock and returns the index of the new clock.
361
+ #
362
+ # If .start! is called again before .end! then the time returned will be
363
+ # the elapsed time from the next call to start!, and calling .end! again
364
+ # will return the time from *this* call to start! (that is, the clocks are
365
+ # LIFO)
366
+ def start!
367
+ (@benchmarks ||= []) << Time.now
368
+ @current = @benchmarks.size - 1
369
+ end
370
+
371
+ # Returns the elapsed time from the most recently started benchmark clock
372
+ # and ends the benchmark, so that a subsequent call to .end! will return
373
+ # the elapsed time from the previously started benchmark clock.
374
+ def end!
375
+ elapsed = (Time.now - @benchmarks[@current])
376
+ @current -= 1
377
+ elapsed
378
+ end
379
+
380
+ # Returns the elapsed time from the benchmark clock w/ the given index (as
381
+ # returned from when .start! was called).
382
+ def time?(index)
383
+ Time.now - @benchmarks[index]
384
+ end
385
+
386
+ # Benchmarks the given block, printing out the given message first (if
387
+ # given).
388
+ def timed(message=nil, &block)
389
+ start!
390
+ out(message) if message
391
+ yield
392
+ done!
393
+ end
394
+ end
395
+ end
396
+ end
397
+ end
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: carrierwave-audio-waveform
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Trevor Hinesley
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-10-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: carrierwave
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: ruby-audio
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: ruby-sox
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: oily_png
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: byebug
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: CarrierWave Audio Waveform
112
+ email:
113
+ - trevor@trevorhinesley.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - LICENSE.txt
119
+ - README.md
120
+ - lib/carrierwave-audio-waveform.rb
121
+ - lib/carrierwave/audio_waveform.rb
122
+ - lib/carrierwave/audio_waveform/version.rb
123
+ - lib/carrierwave/audio_waveform/waveformer.rb
124
+ homepage: https://github.com/TrevorHinesley/carrierwave-audio-waveform
125
+ licenses:
126
+ - MIT
127
+ metadata: {}
128
+ post_install_message:
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ requirements: []
143
+ rubyforge_project:
144
+ rubygems_version: 2.5.1
145
+ signing_key:
146
+ specification_version: 4
147
+ summary: Generate waveform images from audio files within Carrierwave
148
+ test_files: []