audio_monster 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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