audio_monster 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,951 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require "audio_monster/version"
4
+
5
+ require 'open3'
6
+ require 'timeout'
7
+ require 'mp3info'
8
+ require 'logger'
9
+ require 'nu_wav'
10
+ require 'tempfile'
11
+ require 'mimemagic'
12
+ require 'active_support/all'
13
+
14
+ module AudioMonster
15
+
16
+ class Monster
17
+ include Configuration
18
+ include ::NuWav
19
+
20
+ def initialize(options={})
21
+ apply_configuration(options)
22
+ end
23
+
24
+ def tone_detect(path, tone, threshold=0.05, min_time=0.5)
25
+ ranges = []
26
+
27
+ tone = tone.to_i
28
+ threshold = threshold.to_f
29
+ min_time = min_time.to_f
30
+ normalized_wav_dat = nil
31
+
32
+ begin
33
+
34
+ normalized_wav_dat = create_temp_file(path + '.dat')
35
+ normalized_wav_dat.close
36
+
37
+ command = "#{bin(:sox)} '#{path}' '#{normalized_wav_dat.path}' channels 1 rate 200 bandpass #{tone} 3 gain 6"
38
+ out, err = run_command(command)
39
+ current_range = nil
40
+
41
+ File.foreach(normalized_wav_dat.path) do |row|
42
+ next if row[0] == ';'
43
+
44
+ data = row.split.map(&:to_f)
45
+ time = data[0]
46
+ energy = data[1].abs()
47
+
48
+ if energy >= threshold
49
+ if !current_range
50
+ current_range = {start: time, finish: time, min: energy, max: energy}
51
+ else
52
+ current_range[:finish] = time
53
+ current_range[:min] = [current_range[:min], energy].min
54
+ current_range[:max] = [current_range[:max], energy].max
55
+ end
56
+ else
57
+ if current_range && ((current_range[:finish] + min_time.to_f) < time)
58
+ ranges << current_range
59
+ current_range = nil
60
+ end
61
+ end
62
+ end
63
+
64
+ if current_range
65
+ ranges << current_range
66
+ end
67
+
68
+ ensure
69
+ normalized_wav_dat.close rescue nil
70
+ normalized_wav_dat.unlink rescue nil
71
+ end
72
+
73
+ ranges
74
+ end
75
+
76
+ def silence_detect(path, threshold=0.001, min_time=2.0)
77
+ ranges = []
78
+ # puts "\n#{Time.now} tone_detect(path, tone): #{path}, #{tone}\n"
79
+
80
+ threshold = threshold.to_f
81
+ min_time = min_time.to_f
82
+ normalized_wav_dat = nil
83
+
84
+ begin
85
+
86
+ normalized_wav_dat = create_temp_file(path + '.dat')
87
+ normalized_wav_dat.close
88
+
89
+ command = "#{bin(:sox)} '#{path}' '#{normalized_wav_dat.path}' channels 1 rate 1000 norm"
90
+ out, err = run_command(command)
91
+
92
+ current_range = nil
93
+
94
+ File.foreach(normalized_wav_dat.path) do |row|
95
+ next if row[0] == ';'
96
+
97
+ data = row.split.map(&:to_f)
98
+ time = data[0]
99
+ energy = data[1].abs()
100
+
101
+ if energy < threshold
102
+ if !current_range
103
+ current_range = {start: time, finish: time, min: energy, max: energy}
104
+ else
105
+ current_range[:finish] = time
106
+ current_range[:min] = [current_range[:min], energy].min
107
+ current_range[:max] = [current_range[:max], energy].max
108
+ end
109
+ else
110
+ next unless current_range
111
+ ranges << current_range if ((current_range[:finish] - current_range[:start]) > min_time.to_f)
112
+ current_range = nil
113
+ end
114
+ end
115
+
116
+ if current_range && ((current_range[:finish] - current_range[:start]) > min_time.to_f)
117
+ ranges << current_range
118
+ end
119
+
120
+ ensure
121
+ normalized_wav_dat.close rescue nil
122
+ normalized_wav_dat.unlink rescue nil
123
+ end
124
+
125
+ ranges
126
+ end
127
+
128
+ def encode_wav_pcm_from_mpeg(original_path, wav_path, options={})
129
+ logger.info "encode_wav_pcm_from_mpeg: #{original_path}, #{wav_path}, #{options.inspect}"
130
+ # check to see if there is an original
131
+ check_local_file(original_path)
132
+
133
+ logger.debug "encode_wav_pcm_from_mpeg: start"
134
+ command = "#{bin(:madplay)} -Q -i --output=wave:'#{wav_path}' '#{original_path}'"
135
+
136
+ out, err = run_command(command)
137
+
138
+ # check to see if there is a file created, or don't go on.
139
+ check_local_file(wav_path)
140
+ return [out, err]
141
+ end
142
+
143
+ def encode_wav_pcm_from_flac(original_path, wav_path, options={})
144
+ logger.info "encode_wav_pcm_from_flac: #{original_path}, #{wav_path}, #{options.inspect}"
145
+ # check to see if there is an original
146
+ check_local_file(original_path)
147
+
148
+ logger.debug "encode_wav_pcm_from_mpeg: start"
149
+ command = "#{bin(:flac)} -s -f --decode '#{original_path}' --output-name='#{wav_path}'"
150
+ out, err = run_command(command)
151
+
152
+ # check to see if there is a file created, or don't go on.
153
+ check_local_file(wav_path)
154
+ return [out, err]
155
+ end
156
+
157
+ alias encode_wav_pcm_from_mp2 encode_wav_pcm_from_mpeg
158
+ alias encode_wav_pcm_from_mp3 encode_wav_pcm_from_mpeg
159
+
160
+ # experimental...should work on any ffmpeg compatible file
161
+ def decode_audio(original_path, wav_path, options={})
162
+ # check to see if there is an original
163
+ logger.info "decode_audio: #{original_path}, #{wav_path}, #{options.inspect}"
164
+ check_local_file(original_path)
165
+
166
+ # if the file extension is banged up, try to get from options, or guess at 'mov'
167
+ input_format = ''
168
+ if options[:source_format] || (File.extname(original_path).length != 4)
169
+ input_format = options[:source_format] ? "-f #{options[:source_format]}" : '-f mov'
170
+ end
171
+
172
+ channels = options[:channels] ? options[:channels].to_i : 2
173
+ sample_rate = options[:sample_rate] ? options[:sample_rate].to_i : 44100
174
+ logger.debug "decode_audio: start"
175
+ command = "#{bin(:ffmpeg)} -nostats -loglevel warning -vn -i '#{original_path}' -acodec pcm_s16le -ac #{channels} -ar #{sample_rate} -y -f wav '#{wav_path}'"
176
+
177
+ out, err = run_command(command)
178
+
179
+ # check to see if there is a file created, or don't go on.
180
+ check_local_file(wav_path)
181
+ return [out, err]
182
+ end
183
+
184
+ def info_for_mpeg(mpeg_path, info = nil)
185
+ logger.debug "info_for_mpeg: start"
186
+ length = audio_file_duration(mpeg_path)
187
+ info ||= Mp3Info.new(mpeg_path)
188
+ result = {
189
+ :size => File.size(mpeg_path),
190
+ :content_type => 'audio/mpeg',
191
+ :channel_mode => info.channel_mode,
192
+ :bit_rate => info.bitrate,
193
+ :length => [info.length.to_i, length.to_i].max,
194
+ :sample_rate => info.samplerate,
195
+ :version => info.mpeg_version, # mpeg specific
196
+ :layer => info.layer # mpeg specific
197
+ }
198
+
199
+ # indicate this can be GC'd
200
+ info = nil
201
+
202
+ result
203
+ end
204
+
205
+ alias info_for_mp2 info_for_mpeg
206
+ alias info_for_mp3 info_for_mpeg
207
+
208
+ def info_for_wav(wav_file_path)
209
+ wf = WaveFile.parse(wav_file_path)
210
+ fmt = wf.chunks[:fmt]
211
+ {
212
+ :size => File.size(wav_file_path),
213
+ :content_type => 'audio/vnd.wave',
214
+ :channel_mode => fmt.number_of_channels <= 1 ? 'Mono' : 'Stereo',
215
+ :bit_rate => (fmt.byte_rate * 8) / 1000, #kilo bytes per sec
216
+ :length => wf.duration,
217
+ :sample_rate => fmt.sample_rate
218
+ }
219
+ end
220
+
221
+ def info_for_audio(path)
222
+ {
223
+ :size => File.size(path),
224
+ :content_type => (MimeMagic.by_path(path) || MimeMagic.by_magic(path)).to_s,
225
+ :channel_mode => audio_file_channels(path) <= 1 ? 'Mono' : 'Stereo',
226
+ :bit_rate => audio_file_bit_rate(path),
227
+ :length => audio_file_duration(path),
228
+ :sample_rate => audio_file_sample_rate(path)
229
+ }
230
+ end
231
+
232
+ def audio_file_duration(path)
233
+ audio_file_info(path, 'D').to_f
234
+ end
235
+
236
+ def audio_file_channels(path)
237
+ audio_file_info(path, 'c').to_i
238
+ end
239
+
240
+ def audio_file_sample_rate(path)
241
+ audio_file_info(path, 'r').to_i
242
+ end
243
+
244
+ def audio_file_bit_rate(path)
245
+ audio_file_info(path, 'B').to_i
246
+ end
247
+
248
+ def audio_file_info(path, flag)
249
+ check_local_file(path)
250
+ out, err = run_command("#{bin(:soxi)} -V0 -#{flag} '#{path}'", :nice=>'n', :echo_return=>false)
251
+ out.chomp
252
+ end
253
+
254
+ # valid options
255
+ # :sample_rate
256
+ # :bit_rate
257
+ # :per_channel_bit_rate
258
+ # :channel_mode
259
+ # :protect
260
+ # :copyright
261
+ # :original
262
+ # :emphasis
263
+ def encode_mp2_from_wav(original_path, mp2_path, options={})
264
+ check_local_file(original_path)
265
+
266
+ options.to_options!
267
+ # parse the wave to see what values to use if not overridden by the options
268
+ wf = WaveFile.parse(original_path)
269
+ fmt = wf.chunks[:fmt]
270
+
271
+ wav_sample_size = fmt.sample_bits
272
+
273
+ # twolame can only handle up to 16 for floating point (seems to convert to 16 internaly anyway)
274
+ # "Note: the 32-bit samples are currently scaled down to 16-bit samples internally."
275
+ # libtwolame.h twolame_encode_buffer_float32 http://www.twolame.org/doc/twolame_8h.html#8e77eb0f22479f8ec1bd4f1b042f9cd9
276
+ if (fmt.compression_code.to_i == PCM_FLOATING_COMPRESSION && fmt.sample_bits > 32)
277
+ wav_sample_size = 16
278
+ end
279
+
280
+ # input options
281
+ prefix_command = ''
282
+ raw_input = ''
283
+ sample_rate = "--samplerate #{fmt.sample_rate}"
284
+ sample_bits = "--samplesize #{wav_sample_size}"
285
+ channels = "--channels #{fmt.number_of_channels}"
286
+ input_path = "'#{original_path}'"
287
+
288
+ # output options
289
+ mp2_sample_rate = if MP2_SAMPLE_RATES.include?(options[:sample_rate].to_s)
290
+ options[:sample_rate]
291
+ elsif MP2_SAMPLE_RATES.include?(fmt.sample_rate.to_s)
292
+ fmt.sample_rate.to_s
293
+ else
294
+ '44100'
295
+ end
296
+
297
+ if mp2_sample_rate.to_i != fmt.sample_rate.to_i
298
+ prefix_command = "#{bin(:sox)} '#{original_path}' -t raw -r #{mp2_sample_rate} - | "
299
+ input_path = '-'
300
+ raw_input = '--raw-input'
301
+ end
302
+
303
+ mode = if TWOLAME_MODES.include?(options[:channel_mode])
304
+ options[:channel_mode] #use the channel mode from the options if specified
305
+ elsif fmt.number_of_channels <= 1
306
+ 'm' # default to monoaural for 1 channel input
307
+ else
308
+ 's' # default to joint stereo for 2 channel input
309
+ end
310
+ channel_mode = "--mode #{mode}"
311
+
312
+ kbps = if options[:per_channel_bit_rate]
313
+ options[:per_channel_bit_rate].to_i * ((mode == 'm') ? 1 : 2)
314
+ elsif options[:bit_rate]
315
+ options[:bit_rate].to_i
316
+ else
317
+ 0
318
+ end
319
+
320
+ kbps = if MP2_BITRATES.include?(kbps)
321
+ kbps
322
+ elsif mode == 'm' || (mode =='a' && fmt.number_of_channels <= 1)
323
+ 128 # default for monoaural is 128 kbps
324
+ else
325
+ 256 # default for stereo/dual channel is 256 kbps
326
+ end
327
+ bit_rate = "--bitrate #{kbps}"
328
+
329
+ downmix = (mode == 'm' && fmt.number_of_channels > 1) ? '--downmix' : ''
330
+
331
+ # default these headers when options not present
332
+ protect = (options.key?(:protect) && !options[:protect] ) ? '' : '--protect'
333
+ copyright = (options.key?(:copyright) && !options[:copyright] ) ? '' : '--copyright'
334
+ original = (options.key?(:original) && !options[:original] ) ? '--non-original' : '--original'
335
+ emphasis = (options.key?(:emphasis)) ? "--deemphasis #{options[:emphasis]}" : '--deemphasis n'
336
+
337
+ ##
338
+ # execute the command
339
+ ##
340
+ input_options = "#{raw_input} #{sample_rate} #{sample_bits} #{channels}"
341
+ output_options = "#{channel_mode} #{bit_rate} #{downmix} #{protect} #{copyright} #{original} #{emphasis}"
342
+
343
+ command = "#{prefix_command} #{bin(:twolame)} -t 0 #{input_options} #{output_options} #{input_path} '#{mp2_path}'"
344
+ out, err = run_command(command)
345
+ unless out.split("\n").last =~ TWOLAME_SUCCESS_RE
346
+ raise "encode_mp2_from_wav - twolame response on transcoding was not recognized as a success, #{out}, #{err}"
347
+ end
348
+
349
+ # make sure there is a file at the end of this
350
+ check_local_file(mp2_path)
351
+
352
+ true
353
+ end
354
+
355
+ # valid options
356
+ # :sample_rate
357
+ # :bit_rate
358
+ # :channel_mode
359
+ def encode_mp3_from_wav(original_path, mp3_path, options={})
360
+ logger.info "encode_mp3_from_wav: #{original_path}, #{mp3_path}, #{options.inspect}"
361
+
362
+ check_local_file(original_path)
363
+
364
+ options.to_options!
365
+ # parse the wave to see what values to use if not overridden by the options
366
+ wf = WaveFile.parse(original_path)
367
+ fmt = wf.chunks[:fmt]
368
+
369
+ input_path = '-'
370
+
371
+ mp3_sample_rate = if MP3_SAMPLE_RATES.include?(options[:sample_rate].to_s)
372
+ options[:sample_rate].to_s
373
+ elsif MP3_SAMPLE_RATES.include?(fmt.sample_rate.to_s)
374
+ logger.debug "sample_rate: fmt.sample_rate = #{fmt.sample_rate}"
375
+ fmt.sample_rate.to_s
376
+ else
377
+ '44100'
378
+ end
379
+ logger.debug "mp3_sample_rate: #{options[:sample_rate]}, #{fmt.sample_rate}"
380
+
381
+ mode = if LAME_MODES.include?(options[:channel_mode])
382
+ options[:channel_mode] #use the channel mode from the options if specified
383
+ elsif fmt.number_of_channels <= 1
384
+ 'm' # default to monoaural for 1 channel input
385
+ else
386
+ 'j' # default to joint stereo for 2 channel input
387
+ end
388
+ channel_mode = "-m #{mode}"
389
+
390
+ # if mono selected, but input is in stereo, need to specify downmix to 1 channel for sox
391
+ downmix = (mode == 'm' && fmt.number_of_channels > 1) ? '-c 1' : ''
392
+
393
+ # if sample rate different, change that as well in sox before piping to lame
394
+ resample = (mp3_sample_rate.to_i != fmt.sample_rate.to_i) ? "-r #{mp3_sample_rate} " : ''
395
+ logger.debug "resample: #{resample} from comparing #{mp3_sample_rate} #{fmt.sample_rate}"
396
+
397
+ # output to wav (-t wav) has a warning
398
+ # '/usr/local/bin/sox wav: Length in output .wav header will be wrong since can't seek to fix it'
399
+ # that messsage can safely be ignored, wa output is easier/safer for lame to recognize, so worth ignoring this message
400
+ prefix_command = "#{bin(:sox)} '#{original_path}' -t wav #{resample} #{downmix} - | "
401
+
402
+ kbps = if options[:per_channel_bit_rate]
403
+ options[:per_channel_bit_rate].to_i * ((mode == 'm') ? 1 : 2)
404
+ elsif options[:bit_rate]
405
+ options[:bit_rate].to_i
406
+ else
407
+ 0
408
+ end
409
+
410
+ kbps = if MP3_BITRATES.include?(kbps)
411
+ kbps
412
+ elsif mode == 'm'
413
+ 128 # default for monoaural is 128 kbps
414
+ else
415
+ 256 # default for stereo/dual channel is 256 kbps
416
+ end
417
+ bit_rate = "--cbr -b #{kbps}"
418
+
419
+ ##
420
+ # execute the command
421
+ ##
422
+ output_options = "#{channel_mode} #{bit_rate}"
423
+
424
+ command = "#{prefix_command} #{bin(:lame)} -S #{output_options} #{input_path} '#{mp3_path}'"
425
+
426
+ out, err = run_command(command)
427
+
428
+ unless out.split("\n")[-1] =~ LAME_SUCCESS_RE
429
+ raise "encode_mp3_from_wav - lame completion unsuccessful: #{out}"
430
+ end
431
+
432
+ err.split("\n").each do |l|
433
+ if l =~ LAME_ERROR_RE
434
+ raise "encode_mp3_from_wav - lame response had fatal error: #{l}"
435
+ end
436
+ end
437
+ logger.debug "encode_mp3_from_wav: end!"
438
+
439
+ check_local_file(mp3_path)
440
+
441
+ true
442
+ end
443
+
444
+ def encode_ogg_from_wav(original_path, result_path, options={})
445
+ logger.info("encode_ogg_from_wav: original_path: #{original_path}, result_path: #{result_path}, options: #{options.inspect}")
446
+
447
+ check_local_file(original_path)
448
+
449
+ options.to_options!
450
+ # parse the wave to see what values to use if not overridden by the options
451
+ wf = WaveFile.parse(original_path)
452
+ fmt = wf.chunks[:fmt]
453
+
454
+ sample_rate = if MP3_SAMPLE_RATES.include?(options[:sample_rate].to_s)
455
+ options[:sample_rate].to_s
456
+ elsif MP3_SAMPLE_RATES.include?(fmt.sample_rate.to_s)
457
+ logger.debug "sample_rate: fmt.sample_rate = #{fmt.sample_rate}"
458
+ fmt.sample_rate.to_s
459
+ else
460
+ '44100'
461
+ end
462
+ logger.debug "sample_rate: #{options[:sample_rate]}, #{fmt.sample_rate}"
463
+
464
+ mode = if LAME_MODES.include?(options[:channel_mode])
465
+ options[:channel_mode] #use the channel mode from the options if specified
466
+ elsif fmt.number_of_channels <= 1
467
+ 'm' # default to monoaural for 1 channel input
468
+ else
469
+ 'j' # default to joint stereo for 2 channel input
470
+ end
471
+
472
+ # can directly set # of channels, 16 or less
473
+ # otherwise fallback on the mode, like mpegs
474
+ # or 2 if all else fails
475
+ channels = if (options[:channels].to_i > 0 )
476
+ [options[:channels].to_i, 16].min
477
+ else
478
+ (mode && (mode == 'm')) ? 1 : 2
479
+ end
480
+
481
+ kbps = if options[:per_channel_bit_rate]
482
+ options[:per_channel_bit_rate].to_i * channels
483
+ elsif options[:bit_rate]
484
+ options[:bit_rate].to_i
485
+ else
486
+ 0
487
+ end
488
+
489
+ bit_rate = (MP3_BITRATES.include?(kbps) ? kbps : 96).to_s + "k"
490
+
491
+ command = "#{bin(:ffmpeg)} -nostats -loglevel warning -vn -i '#{original_path}' -acodec libvorbis -ac #{channels} -ar #{sample_rate} -ab #{bit_rate} -y -f ogg '#{result_path}'"
492
+
493
+ out, err = run_command(command)
494
+
495
+ check_local_file(result_path)
496
+
497
+ return true
498
+ end
499
+
500
+ # need start_at, ends_on
501
+ def create_wav_wrapped_mpeg(mpeg_path, result_path, options={})
502
+ options.to_options!
503
+
504
+ start_at = get_datetime_for_option(options[:start_at])
505
+ end_at = get_datetime_for_option(options[:end_at])
506
+
507
+ wav_wrapped_mpeg = NuWav::WaveFile.from_mpeg(mpeg_path)
508
+ cart = wav_wrapped_mpeg.chunks[:cart]
509
+ cart.title = options[:title] || File.basename(mpeg_path)
510
+ cart.artist = options[:artist]
511
+ cart.cut_id = options[:cut_id]
512
+ cart.producer_app_id = options[:producer_app_id] if options[:producer_app_id]
513
+ cart.start_date = start_at.strftime(PRSS_DATE_FORMAT)
514
+ cart.start_time = start_at.strftime(AES46_2002_TIME_FORMAT)
515
+ cart.end_date = end_at.strftime(PRSS_DATE_FORMAT)
516
+ cart.end_time = end_at.strftime(AES46_2002_TIME_FORMAT)
517
+
518
+ # pass in the options used by NuWav -
519
+ # :no_pad_byte - when true, will not add the pad byte to the data chunk
520
+ nu_wav_options = options.slice(:no_pad_byte)
521
+ wav_wrapped_mpeg.to_file(result_path, nu_wav_options)
522
+
523
+ check_local_file(result_path)
524
+
525
+ return true
526
+ end
527
+
528
+ def get_datetime_for_option(d)
529
+ return DateTime.now unless d
530
+ d.respond_to?(:strftime) ? d : DateTime.parse(d.to_s)
531
+ end
532
+
533
+ alias create_wav_wrapped_mp2 create_wav_wrapped_mpeg
534
+ alias create_wav_wrapped_mp3 create_wav_wrapped_mpeg
535
+
536
+ def add_cart_chunk_to_wav(wave_path, result_path, options={})
537
+ wave = NuWav::WaveFile.parse(wave_path)
538
+ unless wave.chunks[:cart]
539
+ cart = CartChunk.new
540
+ now = Time.now
541
+ today = Date.today
542
+ later = today << 12
543
+
544
+ cart.title = options[:title] || File.basename(wave_path)
545
+ cart.artist = options[:artist]
546
+ cart.cut_id = options[:cut_id]
547
+
548
+ cart.version = options[:version] || '0101'
549
+ cart.producer_app_id = options[:producer_app_id] || 'ContentDepot'
550
+ cart.producer_app_version = options[:producer_app_version] || '1.0'
551
+ cart.level_reference = options[:level_reference] || 0
552
+ cart.tag_text = options[:tag_text] || "\r\n"
553
+ cart.start_date = (options[:start_at] || today).strftime(PRSS_DATE_FORMAT)
554
+ cart.start_time = (options[:start_at] || now).strftime(AES46_2002_TIME_FORMAT)
555
+ cart.end_date = (options[:end_at] || later).strftime(PRSS_DATE_FORMAT)
556
+ cart.end_time = (options[:end_at] || now).strftime(AES46_2002_TIME_FORMAT)
557
+
558
+ wave.chunks[:cart] = cart
559
+ end
560
+
561
+ wave.to_file(result_path)
562
+
563
+ check_local_file(result_path)
564
+
565
+ return true
566
+ end
567
+
568
+ def slice_wav(wav_path, out_path, start, length)
569
+ check_local_file(wav_path)
570
+
571
+ wav_info = info_for_wav(wav_path)
572
+ logger.debug "slice_wav: wav_info:#{wav_info.inspect}"
573
+
574
+ command = "#{bin(:sox)} -t wav '#{wav_path}' -t wav '#{out_path}' trim #{start} #{length}"
575
+ out, err = run_command(command)
576
+ response = out + err
577
+ response.split("\n").each{ |out| raise("slice_wav: cut file error: '#{response}' on:\n #{command}") if out =~ SOX_ERROR_RE }
578
+
579
+ check_local_file(out_path)
580
+ out_path
581
+ end
582
+
583
+ def cut_wav(wav_path, out_path, length, fade=5)
584
+ logger.info "cut_wav: wav_path:#{wav_path}, length:#{length}, fade:#{fade}"
585
+
586
+ wav_info = info_for_wav(wav_path)
587
+ logger.debug "cut_wav: wav_info:#{wav_info.inspect}"
588
+
589
+ new_length = [wav_info[:length].to_i, length].min
590
+ fade_length = [wav_info[:length].to_i, fade].min
591
+
592
+ # find out if the wav file is stereo or mono as this needs to match the starting wav
593
+ channels = wav_info[:channel_mode] == 'Mono' ? 1 : 2
594
+ sample_rate = wav_info[:sample_rate]
595
+
596
+ command = "#{bin(:sox)} -t wav '#{wav_path}' -t raw -s -b 16 -c #{channels} - trim 0 #{new_length} | #{bin(:sox)} -t raw -r #{sample_rate} -s -b 16 -c #{channels} - -t wav '#{out_path}' fade h 0 #{new_length} #{fade_length}"
597
+ out, err = run_command(command)
598
+ response = out + err
599
+ response.split("\n").each{ |out| raise("cut_wav: cut file error: '#{response}' on:\n #{command}") if out =~ SOX_ERROR_RE }
600
+ end
601
+
602
+ def concat_wavs(in_paths, out_path)
603
+ first_wav_info = info_for_wav(in_paths.first)
604
+ channels = first_wav_info[:channel_mode] == 'Mono' ? 1 : 2
605
+ sample_rate = first_wav_info[:sample_rate]
606
+ tmp_files = []
607
+
608
+ concat_paths = in_paths.inject("") {|cmd, path|
609
+ concat_path = path
610
+ wav_info = info_for_wav(concat_path)
611
+ current_channels = wav_info[:channel_mode] == 'Mono' ? 1 : 2
612
+ current_sample_rate = wav_info[:sample_rate]
613
+ if current_channels != channels || current_sample_rate != sample_rate
614
+
615
+ concat_file = create_temp_file(path)
616
+ concat_file.close
617
+
618
+ concat_path = concat_file.path
619
+ command = "#{bin(:sox)} -t wav #{path} -t wav -c #{channels} -r #{sample_rate} '#{concat_path}'"
620
+ out, err = run_command(command)
621
+ response = out + err
622
+ response.split("\n").each{ |out| raise("concat_wavs: create temp file error: '#{response}' on:\n #{command}") if out =~ SOX_ERROR_RE }
623
+ tmp_files << concat_file
624
+ end
625
+ cmd << "-t wav '#{concat_path}' "
626
+ }
627
+ command = "#{bin(:sox)} #{concat_paths} -t wav '#{out_path}'"
628
+ out, err = run_command(command)
629
+
630
+ response = out + err
631
+ response.split("\n").each{ |out| raise("concat_wavs: concat files error: '#{response}' on:\n #{command}") if out =~ SOX_ERROR_RE }
632
+ ensure
633
+ tmp_files.each do |tf|
634
+ tf.close rescue nil
635
+ tf.unlink rescue nil
636
+ end
637
+ tmp_files = nil
638
+ end
639
+
640
+ def append_wav_to_wav(wav_path, append_wav_path, out_path, add_length, fade_length=5)
641
+ append_wav_info = info_for_wav(append_wav_path)
642
+ raise "append wav is not sufficiently long enough (#{append_wav_info[:length]}) to add length (#{add_length})" if append_wav_info[:length].to_i < add_length
643
+
644
+ append_length = [append_wav_info[:length].to_i, (add_length - 1)].min
645
+
646
+ append_fade_length = [append_wav_info[:length].to_i, fade_length].min
647
+
648
+ # find out if the wav file is stereo or mono as this needs to match the starting wav
649
+ wav_info = info_for_wav(wav_path)
650
+ channels = wav_info[:channel_mode] == 'Mono' ? 1 : 2
651
+ sample_rate = wav_info[:sample_rate]
652
+ append_file = nil
653
+
654
+ begin
655
+ append_file = create_temp_file(append_wav_path)
656
+ append_file.close
657
+
658
+ # create the wav to append
659
+ command = "#{bin(:sox)} -t wav '#{append_wav_path}' -t raw -s -b 16 -c #{channels} - trim 0 #{append_length} | #{bin(:sox)} -t raw -r #{sample_rate} -s -b 16 -c #{channels} - -t raw - fade h 0 #{append_length} #{append_fade_length} | #{bin(:sox)} -t raw -r #{sample_rate} -s -b 16 -c #{channels} - -t wav '#{append_file.path}' pad 1 0"
660
+ out, err = run_command(command)
661
+ response = out + err
662
+ response.split("\n").each{ |out| raise("append_wav_to_wav: create append file error: '#{response}' on:\n #{command}") if out =~ SOX_ERROR_RE }
663
+
664
+ # append the files to out_file
665
+ command = "#{bin(:sox)} -t wav '#{wav_path}' -t wav '#{append_file.path}' -t wav '#{out_path}'"
666
+ out, err = run_command(command)
667
+ response = out + err
668
+ response.split("\n").each{ |out| raise("append_wav_to_wav: create append file error: '#{response}' on:\n #{command}") if out =~ SOX_ERROR_RE }
669
+ ensure
670
+ append_file.close rescue nil
671
+ append_file.unlink rescue nil
672
+ end
673
+
674
+ return true
675
+ end
676
+
677
+ def append_mp3_to_wav(wav_path, mp3_path, out_path, add_length, fade_length=5)
678
+ # raise "append_mp3_to_wav: Can't find file to create mp3 preview of: #{mp3_path}" unless File.exist?(mp3_path)
679
+
680
+ mp3info = Mp3Info.new(mp3_path)
681
+ raise "mp3 is not sufficiently long enough (#{mp3info.length.to_i}) to add length (#{add_length})" if mp3info.length.to_i < add_length
682
+ append_length = [mp3info.length.to_i, (add_length - 1)].min
683
+ append_fade_length = [mp3info.length.to_i, fade_length].min
684
+
685
+
686
+ # find out if the wav file is stereo or mono as this meeds to match the wav from the mp3
687
+ wavinfo = info_for_wav(wav_path)
688
+ channels = wavinfo[:channel_mode] == 'Mono' ? 1 : 2
689
+ sample_rate = wavinfo[:sample_rate]
690
+ append_file = nil
691
+
692
+ begin
693
+ append_file = create_temp_file(mp3_path)
694
+ append_file.close
695
+
696
+ # create the mp3 to append
697
+ command = "#{bin(:madplay)} -q -o wave:- '#{mp3_path}' - | #{bin(:sox)} -t wav - -t raw -s -b 16 -c #{channels} - trim 0 #{append_length} | #{bin(:sox)} -t raw -r #{sample_rate} -s -b 16 -c #{channels} - -t wav - fade h 0 #{append_length} #{append_fade_length} | #{bin(:sox)} -t wav - -t wav '#{append_file.path}' pad 1 0"
698
+ out, err = run_command(command)
699
+ response = out + err
700
+ response.split("\n").each{ |out| raise("append_mp3_to_wav: create append file error: '#{response}' on:\n #{command}") if out =~ SOX_ERROR_RE }
701
+
702
+ # append the files to out_filew
703
+ command = "#{bin(:sox)} -t wav '#{wav_path}' -t wav '#{append_file.path}' -t wav '#{out_path}'"
704
+ out, err = run_command(command)
705
+ response = out + err
706
+ response.split("\n").each{ |out| raise("append_mp3_to_wav: create append file error: '#{response}' on:\n #{command}") if out =~ SOX_ERROR_RE }
707
+ ensure
708
+ append_file.close rescue nil
709
+ append_file.unlink rescue nil
710
+ end
711
+
712
+ return true
713
+ end
714
+
715
+ def normalize_wav(wav_path, out_path, level=-9)
716
+ logger.info "normalize_wav: wav_path:#{wav_path}, level:#{level}"
717
+ command = "#{bin(:sox)} -t wav '#{wav_path}' -t wav '#{out_path}' gain -n #{level.to_i}"
718
+ out, err = run_command(command)
719
+ response = out + err
720
+ response.split("\n").each{ |out| raise("normalize_wav: normalize audio file error: '#{response}' on:\n #{command}") if out =~ SOX_ERROR_RE }
721
+ end
722
+
723
+ def validate_mpeg(audio_file_path, options)
724
+ @errors = {}
725
+
726
+ options = HashWithIndifferentAccess.new(options)
727
+
728
+ info = mp3info_validation(audio_file_path, options)
729
+
730
+ # there are condtions where this spews output uncontrollably - so lose it for now: AK on 20080915
731
+ # e.g. mpck:/home/app/mediajoint/tmp/audio_monster/prxfile-66097_111955868219902-0:3366912:read error
732
+ # mpck_validation(audio_file_path, errors) if errors.size <= 0
733
+
734
+ # if the format seems legit, check the audio itself
735
+ mp3val_validation(audio_file_path, options)
736
+
737
+ return @errors, info
738
+ end
739
+
740
+ alias validate_mp2 validate_mpeg
741
+ alias validate_mp3 validate_mpeg
742
+
743
+ def create_temp_file(base_file_name=nil, bin_mode=true)
744
+ file_name = File.basename(base_file_name)
745
+ file_ext = File.extname(base_file_name)
746
+ FileUtils.mkdir_p(tmp_dir) unless File.exists?(tmp_dir)
747
+ tmp = Tempfile.new([file_name, file_ext], tmp_dir)
748
+ tmp.binmode if bin_mode
749
+ tmp
750
+ end
751
+
752
+ protected
753
+
754
+ # Validation methods
755
+ def add_error(attribute, message)
756
+ @errors ||= {}
757
+ @errors[attribute] = [] unless @errors[attribute]
758
+ @errors[attribute] << message
759
+ end
760
+
761
+ def valid_operator(op)
762
+ [">=", "<=", "==", "=", ">", "<"].include?(op) ? (op == "=" ? "==" : op) : ">="
763
+ end
764
+
765
+ def files_validation(audio_file_path, errors)
766
+ response = run_command("#{FILE} '#{audio_file_path}'", :echo_return=>false).chomp
767
+ logger.debug("'file' on #{audio_file_path}. Response: #{response}")
768
+ unless response =~ FILE_SUCCESS
769
+ response =~ /.*: /
770
+ add_error(:file, "is not a valid mp2 file, we think it's a '#{$'}'")
771
+ end
772
+ end
773
+
774
+ def mp3info_validation(audio_file_path, options)
775
+ info = nil
776
+
777
+ begin
778
+ info = Mp3Info.new(audio_file_path)
779
+ rescue Mp3InfoError => err
780
+ add_error(:file, "is not a valid mpeg audio file.")
781
+ return
782
+ end
783
+
784
+ if options[:version]
785
+ version = options[:version].to_i
786
+ mpeg_version = info.mpeg_version.to_i
787
+ add_error(:version, "must be mpeg version #{version}, but audio version is #{mpeg_version}") unless mpeg_version == version
788
+ end
789
+
790
+ if options[:layer]
791
+ layer = options[:layer].to_i
792
+ mpeg_layer = info.layer.to_i
793
+ add_error(:layer, "must be mpeg layer #{layer}, but audio layer is #{mpeg_layer}") unless mpeg_layer == layer
794
+ end
795
+
796
+ if options[:channel_mode]
797
+ cm_list = options[:channel_mode].to_a
798
+ add_error(:channel_mode, "channel mode must be one of (#{cm_list.to_sentence})") unless cm_list.include?(info.channel_mode)
799
+ end
800
+
801
+ if options[:channels]
802
+ channels = options[:channels].to_i
803
+ mpeg_channels = "Single Channel" == info.channel_mode ? 1 : 2
804
+ add_error(:channels, "must have channel count of #{channels}, but audio is #{mpeg_channels}") unless mpeg_channels == channels
805
+ end
806
+
807
+ # only certain rates are valid for different layer/versions, but don't add that right now
808
+ if options[:sample_rate]
809
+ sample_rate = 44100
810
+ op = ">="
811
+ mpeg_sample_rate = info.samplerate.to_i
812
+ if options[:sample_rate].match(' ')
813
+ op, sample_rate = options[:sample_rate].split(' ')
814
+ sample_rate = sample_rate.to_i
815
+ op = valid_operator(op)
816
+ else
817
+ sample_rate = options[:sample_rate].to_i
818
+ end
819
+ add_error(:sample_rate, "sample rate should be #{op} #{sample_rate}, but is #{mpeg_sample_rate}") unless eval("#{mpeg_sample_rate} #{op} #{sample_rate}")
820
+ end
821
+
822
+ if options[:bit_rate]
823
+ bit_rate = 128
824
+ op = ">="
825
+ mpeg_bit_rate = info.bitrate.to_i
826
+ if options[:bit_rate].match(' ')
827
+ op, bit_rate = options[:bit_rate].split(' ')
828
+ bit_rate = bit_rate.to_i
829
+ op = valid_operator(op)
830
+ else
831
+ bit_rate = options[:bit_rate].to_i
832
+ end
833
+ add_error(:bit_rate, "bit rate should be #{op} #{bit_rate}, but is #{mpeg_bit_rate}") unless eval("#{mpeg_bit_rate} #{op} #{bit_rate}")
834
+ end
835
+
836
+ if options[:per_channel_bit_rate]
837
+ per_channel_bit_rate = 128
838
+ op = ">="
839
+ mpeg_channels = "Single Channel" == info.channel_mode ? 1 : 2
840
+ mpeg_per_channel_bit_rate = info.bitrate.to_i / mpeg_channels
841
+
842
+ if options[:per_channel_bit_rate].match(' ')
843
+ op, per_channel_bit_rate = options[:per_channel_bit_rate].split(' ')
844
+ per_channel_bit_rate = per_channel_bit_rate.to_i
845
+ op = valid_operator(op)
846
+ else
847
+ per_channel_bit_rate = options[:per_channel_bit_rate].to_i
848
+ end
849
+ add_error(:per_channel_bit_rate, "per channel bit rate should be #{op} #{per_channel_bit_rate}, but is #{mpeg_per_channel_bit_rate}, and channels = #{mpeg_channels}") unless eval("#{mpeg_per_channel_bit_rate} #{op} #{per_channel_bit_rate}")
850
+ end
851
+ info_for_mpeg(audio_file_path, info)
852
+ end
853
+
854
+ def mp3val_validation(audio_file_path, options)
855
+ warning = false
856
+ error = false
857
+ out, err = run_command("#{bin(:mp3val)} -si '#{audio_file_path}'", :echo_return=>false)
858
+ lines = out.split("\n")
859
+ lines.each { |o|
860
+ if (o =~ MP3VAL_IGNORE_RE)
861
+ next
862
+ elsif (o =~ MP3VAL_WARNING_RE)
863
+ add_error(:file, "is not a valid mpeg file, there were serious warnings when validating the audio.") unless warning
864
+ warning = true
865
+ elsif (o =~ MP3VAL_ERROR_RE)
866
+ add_error(:file, "is not a valid mpeg file, there were errors when validating the audio.") unless error
867
+ error = true
868
+ else
869
+ next
870
+ end
871
+ }
872
+ end
873
+
874
+ # Pass the command to run, and a timeout
875
+ def run_command(command, options={})
876
+ timeout = options[:timeout] || 7200
877
+
878
+ # default to adding a nice 13 if nothing specified
879
+ nice = if options.key?(:nice)
880
+ (options[:nice] == 'n') ? '' : "nice -n #{options[:nice]} "
881
+ else
882
+ 'nice -n 19 '
883
+ end
884
+
885
+ echo_return = (options.key?(:echo_return) && !options[:echo_return]) ? '' : '; echo $?'
886
+
887
+ cmd = "#{nice}#{command}#{echo_return}"
888
+
889
+ logger.info "run_command: #{cmd}"
890
+ begin
891
+ result = Timeout::timeout(timeout) {
892
+ Open3::popen3(cmd) do |i,o,e|
893
+ out_str = ""
894
+ err_str = ""
895
+ i.close # important!
896
+ o.sync = true
897
+ e.sync = true
898
+ o.each{|line|
899
+ out_str << line
900
+ line.chomp!
901
+ logger.debug "stdout: #{line}"
902
+ }
903
+ e.each { |line|
904
+ err_str << line
905
+ line.chomp!
906
+ logger.debug "stderr: #{line}"
907
+ }
908
+ return out_str, err_str
909
+ end
910
+ }
911
+ rescue Timeout::Error => toe
912
+ logger.error "run_command:Timeout Error - running command, took longer than #{timeout} seconds to execute: '#{cmd}'"
913
+ raise toe
914
+ end
915
+ end
916
+
917
+ def mpck_validation(audio_file_path, options)
918
+ errors= []
919
+ # validate using mpck
920
+ response = run_command("nice -n 19 #{bin(:mpck)} #{audio_file_path}")
921
+ response.split("\n").each { |o|
922
+ if ((o =~ MPCK_ERROR_RE) && !(o =~ MPCK_IGNORE_RE))
923
+ errors << "is not a valid mp2 file. The file is bad according to the 'mpck' audio check."
924
+ end
925
+ }
926
+
927
+ errors
928
+ end
929
+
930
+ def method_missing(name, *args, &block)
931
+ if name.to_s.starts_with?('encode_wav_pcm_from_')
932
+ decode_audio(*args)
933
+ elsif name.to_s.starts_with?('info_for_')
934
+ info_for_audio(*args)
935
+ else
936
+ super
937
+ end
938
+ end
939
+
940
+ protected
941
+
942
+ def check_local_file(file_path)
943
+ raise "File missing or 0 length: #{file_path}" unless (File.size?(file_path).to_i > 0)
944
+ end
945
+
946
+ def get_lame_channel_mode(channel_mode)
947
+ ["Stereo", "JStereo"].include?(channel_mode) ? "j" : "m"
948
+ end
949
+
950
+ end
951
+ end