Skip to content

Commit 01ae375

Browse files
authored
Merge branch 'master' into phase-test
2 parents e893ba8 + 0cd4d5b commit 01ae375

File tree

2 files changed

+119
-92
lines changed

2 files changed

+119
-92
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,9 @@ Options:
6060
`-b` Scan BEXT metadata for consistency of CodingHistory field
6161

6262
`-d` Scan file for audio dropouts (experimental!)
63+
64+
65+
## Maintainers
66+
Andrew Weaver (@privatezero)
67+
68+
Susie Cummings (@susiecummings)

audioqc

Lines changed: 113 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,15 @@ end
3838
# If no extenstion is specified it will target the default 'wav' extension. (Not case sensitive)
3939
options = []
4040
ARGV.options do |opts|
41-
opts.on('-p', '--Policy=val', String) { |val| POLICY_FILE = val }
41+
opts.on('-a', '--all') { options += ['meta', 'bext', 'signal', 'dropouts', 'md5'] }
42+
opts.on('-b', '--bext-scan') { options << 'bext' }
43+
opts.on('-c', '--checksum') { options << 'md5' }
44+
opts.on('-d', '--dropout-scan') { options << 'dropouts' }
4245
opts.on('-e', '--Extension=val', String) { |val| TARGET_EXTENSION = val.downcase }
43-
opts.on('-q', '--Quiet') { options << 'quiet' }
4446
opts.on('-m', '--meta-scan') { options << 'meta' }
45-
opts.on('-b', '--bext-scan') { options << 'bext' }
47+
opts.on('-p', '--Policy=val', String) { |val| POLICY_FILE = val }
4648
opts.on('-s', '--signal-scan') { options << 'signal' }
47-
opts.on('-d', '--dropout-scan') { options << 'dropouts' }
48-
opts.on('-a', '--all') { options += ['meta', 'bext', 'signal', 'dropouts'] }
49+
opts.on('-q', '--Quiet') { options << 'quiet' }
4950
opts.parse!
5051
end
5152

@@ -110,79 +111,7 @@ class QcTarget
110111
@input_path = value
111112
@warnings = []
112113
end
113-
114-
# Function to scan file for mediaconch compliance
115-
def media_conch_scan(policy)
116-
if File.file?(policy)
117-
@qc_results = []
118-
policy_path = File.path(policy)
119-
command = 'mediaconch --Policy=' + '"' + policy_path + '" ' + '"' + @input_path + '"'
120-
media_conch_out = `#{command}`
121-
media_conch_out.strip!
122-
media_conch_out.split('/n').each {|qcline| @qc_results << qcline}
123-
@qc_results = @qc_results.to_s
124-
if File.exist?(policy)
125-
if @qc_results.include?('pass!')
126-
@qc_results = 'PASS'
127-
else
128-
@warnings << 'MEDIACONCH FAIL'
129-
end
130-
end
131-
else
132-
@qc_results = policy
133-
end
134-
end
135-
136-
# Functions to scan audio stream characteristics
137-
# Function to get ffprobe json info
138-
def get_ffprobe
139-
ffprobe_command = 'ffprobe -print_format json -threads auto -show_entries frame_tags=lavfi.astats.Overall.Number_of_samples,lavfi.astats.Overall.Peak_level,lavfi.astats.Overall.Max_difference,lavfi.astats.Overall.Mean_difference,lavfi.astats.Overall.Peak_level,lavfi.aphasemeter.phase -f lavfi -i "amovie=' + "\\'" + @input_path + "\\'" + ',astats=reset=1:metadata=1,aphasemeter=video=0"'
140-
@ffprobe_out = JSON.parse(`#{ffprobe_command}`)
141-
@total_frame_count = @ffprobe_out['frames'].size
142-
end
143-
144-
def normalize_time(time_source)
145-
Time.at(time_source).utc.strftime('%H:%M:%S:%m')
146-
end
147-
148-
def get_mediainfo
149-
@mediainfo_out = JSON.parse(`mediainfo --Output=JSON "#{@input_path}"`)
150-
@duration_normalized = Time.at(@mediainfo_out['media']['track'][0]['Duration'].to_f).utc.strftime('%H:%M:%S')
151-
end
152-
153-
def qc_encoding_history
154-
if TARGET_EXTENSION == 'wav'
155-
@enc_hist_error = []
156-
unless @mediainfo_out['media']['track'][0]['extra'].nil?
157-
if @mediainfo_out['media']['track'][0]['extra']['bext_Present'] == 'Yes' && @mediainfo_out['media']['track'][0]['Encoded_Library_Settings']
158-
signal_chain_count = @mediainfo_out['media']['track'][0]['Encoded_Library_Settings'].scan(/A=/).count
159-
if @mediainfo_out['media']['track'][1]['Channels'] == "1"
160-
unless @mediainfo_out['media']['track'][0]['Encoded_Library_Settings'].scan(/mono/i).count == signal_chain_count
161-
@enc_hist_error << "BEXT Coding History channels don't match file"
162-
end
163-
end
164-
165-
if @mediainfo_out['media']['track'][1]['Channels'] == "2"
166-
@stereo_count = @mediainfo_out['media']['track'][0]['Encoded_Library_Settings'].scan(/stereo/i).count
167-
@dual_count = @mediainfo_out['media']['track'][0]['Encoded_Library_Settings'].scan(/dual/i).count
168-
unless @stereo_count + @dual_count == signal_chain_count
169-
@enc_hist_error << "BEXT Coding History channels don't match file"
170-
end
171-
end
172-
end
173-
else
174-
@enc_hist_error << "Encoding history not present"
175-
end
176-
@warnings << @enc_hist_error if @enc_hist_error.size > 0
177-
end
178-
end
179-
180-
def check_metaedit
181-
scan_output = `bwfmetaedit "#{@input_path}" 2>&1`.chomp.chomp
182-
@wave_conformance = scan_output.split(':').last.strip if scan_output.include?('invalid')
183-
@warnings << "Invalid Wave Detected" unless @wave_conformance.nil?
184-
end
185-
114+
186115
def check_dropouts
187116
@sample_ratios = []
188117
@possible_drops = []
@@ -201,6 +130,28 @@ class QcTarget
201130
@warnings << "Possible Dropouts Detected" if @possible_drops.length > 0
202131
end
203132

133+
def check_md5
134+
puts "Verifying embedded MD5 for #{@input_path}"
135+
md5_line = `bwfmetaedit --MD5-Verify -v "#{@input_path}" 2>&1`.chomp.chomp.split("\n")[0]
136+
if md5_line.include?('MD5, no existing MD5 chunk')
137+
@warnings << 'No MD5'
138+
elsif md5_line.include?('MD5, failed verification')
139+
@warnings << 'Failed MD5 Verification'
140+
elsif ! md5_line.include?('MD5, verified')
141+
@warnings << 'MD5 check unable to be performed'
142+
end
143+
end
144+
145+
def check_metaedit
146+
scan_output = `bwfmetaedit "#{@input_path}" 2>&1`.chomp.chomp
147+
@wave_conformance = scan_output.split(':').last.strip if scan_output.include?('invalid')
148+
if @wave_conformance.nil?
149+
@wave_conformance = ' '
150+
else
151+
@warnings << "Invalid Wave Detected" unless @wave_conformance.nil?
152+
end
153+
end
154+
204155
def find_peaks_n_phase
205156
high_db_frames = []
206157
out_of_phase_frames = []
@@ -217,12 +168,14 @@ class QcTarget
217168
phase_limit = Configurations['generic_audio_phase_limit']
218169
end
219170
@ffprobe_out['frames'].each do |frames|
220-
peaklevel = frames['tags']['lavfi.astats.Overall.Peak_level'].to_f
171+
peaklevel = frames['tags']['lavfi.astats.Overall.Peak_level']
221172
audiophase = frames['tags']['lavfi.aphasemeter.phase'].to_f
222173
phase_frames << audiophase
223174
out_of_phase_frames << audiophase if audiophase < phase_limit
224-
high_db_frames << peaklevel if peaklevel > Configurations['high_level_warning']
225-
@levels << peaklevel
175+
if peaklevel != '-inf'
176+
high_db_frames << peaklevel.to_f if peaklevel.to_f > Configurations['high_level_warning']
177+
@levels << peaklevel.to_f
178+
end
226179
end
227180
@max_level = @levels.max.round(2)
228181
@high_level_count = high_db_frames.size
@@ -238,23 +191,88 @@ class QcTarget
238191
@warnings << 'PHASE WARNING' if @phasey_frame_count > 50
239192
end
240193

194+
def get_ffprobe
195+
ffprobe_command = 'ffprobe -print_format json -threads auto -show_entries frame_tags=lavfi.astats.Overall.Number_of_samples,lavfi.astats.Overall.Peak_level,lavfi.astats.Overall.Max_difference,lavfi.astats.Overall.Mean_difference,lavfi.astats.Overall.Peak_level,lavfi.aphasemeter.phase -f lavfi -i "amovie=' + "\\'" + @input_path + "\\'" + ',astats=reset=1:metadata=1,aphasemeter=video=0"'
196+
@ffprobe_out = JSON.parse(`#{ffprobe_command}`)
197+
@total_frame_count = @ffprobe_out['frames'].size
198+
end
199+
200+
def get_mediainfo
201+
@mediainfo_out = JSON.parse(`mediainfo --Output=JSON "#{@input_path}"`)
202+
@duration_normalized = Time.at(@mediainfo_out['media']['track'][0]['Duration'].to_f).utc.strftime('%H:%M:%S')
203+
end
204+
205+
# Function to scan file for mediaconch compliance
206+
def media_conch_scan(policy)
207+
if File.file?(policy)
208+
@qc_results = []
209+
policy_path = File.path(policy)
210+
command = 'mediaconch --Policy=' + '"' + policy_path + '" ' + '"' + @input_path + '"'
211+
media_conch_out = `#{command}`
212+
media_conch_out.strip!
213+
media_conch_out.split('/n').each {|qcline| @qc_results << qcline}
214+
@qc_results = @qc_results.to_s
215+
if File.exist?(policy)
216+
if @qc_results.include?('pass!')
217+
@qc_results = 'PASS'
218+
else
219+
@warnings << 'MEDIACONCH FAIL'
220+
end
221+
end
222+
else
223+
@qc_results = policy
224+
end
225+
end
226+
227+
def normalize_time(time_source)
228+
Time.at(time_source).utc.strftime('%H:%M:%S:%m')
229+
end
230+
241231
def output_csv_line(options)
242232
line = [@input_path, @warnings.flatten.join(', '), @duration_normalized]
243-
if options.include? 'dropouts'
233+
if options.include?('dropouts')
244234
line << @possible_drops
245235
end
246-
if options.include? 'signal'
236+
if options.include?('signal')
247237
line += [@average_levels, @max_level, @high_level_count, @average_phase, @phasey_frame_count]
248238
end
249-
if options.include? 'meta'
239+
if options.include?('meta')
250240
line += [@wave_conformance] unless TARGET_EXTENSION != 'wav'
251241
line += [@qc_results]
252242
end
253243
return line
254244
end
245+
255246
def output_warnings
256247
@warnings
257248
end
249+
250+
def qc_encoding_history
251+
if TARGET_EXTENSION == 'wav'
252+
@enc_hist_error = []
253+
unless @mediainfo_out['media']['track'][0]['extra'].nil?
254+
if @mediainfo_out['media']['track'][0]['extra']['bext_Present'] == 'Yes' && @mediainfo_out['media']['track'][0]['Encoded_Library_Settings']
255+
signal_chain_count = @mediainfo_out['media']['track'][0]['Encoded_Library_Settings'].scan(/A=/).count
256+
if @mediainfo_out['media']['track'][1]['Channels'] == "1"
257+
unless @mediainfo_out['media']['track'][0]['Encoded_Library_Settings'].scan(/mono/i).count == signal_chain_count
258+
@enc_hist_error << "BEXT Coding History channels don't match file"
259+
end
260+
end
261+
262+
if @mediainfo_out['media']['track'][1]['Channels'] == "2"
263+
@stereo_count = @mediainfo_out['media']['track'][0]['Encoded_Library_Settings'].scan(/stereo/i).count
264+
@dual_count = @mediainfo_out['media']['track'][0]['Encoded_Library_Settings'].scan(/dual/i).count
265+
unless @stereo_count + @dual_count == signal_chain_count
266+
@enc_hist_error << "BEXT Coding History channels don't match file"
267+
end
268+
end
269+
end
270+
else
271+
@enc_hist_error << "Encoding history not present"
272+
end
273+
@warnings << @enc_hist_error if @enc_hist_error.size > 0
274+
end
275+
end
258276
end
259277

260278
# Make list of inputs
@@ -284,27 +302,30 @@ end
284302
file_inputs.each do |fileinput|
285303
target = QcTarget.new(File.expand_path(fileinput))
286304
target.get_mediainfo
287-
if options.include? 'meta'
305+
if options.include?('meta')
288306
if defined? POLICY_FILE
289307
target.media_conch_scan(POLICY_FILE)
290308
else
291309
target.media_conch_scan('Valid Policy File Not Found')
292310
end
293311
target.check_metaedit unless TARGET_EXTENSION != 'wav'
294312
end
295-
if options.include? 'bext'
313+
if options.include?('bext')
296314
target.qc_encoding_history
297315
end
316+
if options.include?('md5')
317+
target.check_md5
318+
end
298319
if options.include?('signal') || options.include?('dropouts')
299320
target.get_ffprobe
300-
if options.include? 'signal'
321+
if options.include?('signal')
301322
target.find_peaks_n_phase
302323
end
303-
if options.include? 'dropouts'
324+
if options.include?('dropouts')
304325
target.check_dropouts
305326
end
306327
end
307-
if options.include? 'quiet'
328+
if options.include?('quiet')
308329
if target.output_warnings.empty?
309330
puts 'QC Pass!'
310331
exit 0
@@ -323,15 +344,15 @@ output_csv = ENV['HOME'] + "/Desktop/audioqc-out_#{timestamp}.csv"
323344

324345
CSV.open(output_csv, 'wb') do |csv|
325346
headers = ['Filename', 'Warnings', 'Duration']
326-
if options.include? 'dropouts'
347+
if options.include?('dropouts')
327348
headers << 'Possible Drops'
328349
end
329350

330-
if options.include? 'signal'
351+
if options.include?('signal')
331352
headers += ['Average Level', 'Peak Level', 'Number of Frames w/ High Levels', 'Average Phase', 'Number of Phase Warnings']
332353
end
333354

334-
if options.include? 'meta'
355+
if options.include?('meta')
335356
headers << 'Wave Conformance Errors' unless TARGET_EXTENSION != 'wav'
336357
headers << 'MediaConch Policy Compliance'
337358
end

0 commit comments

Comments
 (0)