1 | #!/usr/bin/env perl
|
---|
2 |
|
---|
3 | use strict;
|
---|
4 | use warnings;
|
---|
5 |
|
---|
6 |
|
---|
7 | use Cwd qw(cwd getcwd);
|
---|
8 |
|
---|
9 | sub get_audio_duration
|
---|
10 | {
|
---|
11 | my ($audio_file) = @_;
|
---|
12 |
|
---|
13 | my $audio_cmd = "./AUDIO-DURATION.sh $audio_file";
|
---|
14 |
|
---|
15 | open(my $fh, '-|', $audio_cmd) or die $!;
|
---|
16 |
|
---|
17 | # Just need to read in one line:
|
---|
18 |
|
---|
19 | my $audio_duration = <$fh>;
|
---|
20 | chomp($audio_duration);
|
---|
21 |
|
---|
22 | # otherwise would do
|
---|
23 | #
|
---|
24 | #while (my $line = <$fh>) {
|
---|
25 | # # Do stuff with each $line.
|
---|
26 | #}
|
---|
27 |
|
---|
28 | close($fh);
|
---|
29 |
|
---|
30 | return $audio_duration;
|
---|
31 | }
|
---|
32 |
|
---|
33 | sub generate_bespoke_profile
|
---|
34 | {
|
---|
35 | my ($startTime, $frame_dur_secs, $profile_template_file,$profile_file) = @_;
|
---|
36 |
|
---|
37 | #
|
---|
38 | # Generate bespoke profile_file for this time-slice
|
---|
39 | #
|
---|
40 | open(my $ipfh, '<', $profile_template_file) or die $!;
|
---|
41 | open(my $opfh, '>', $profile_file) or die $!;
|
---|
42 |
|
---|
43 | my $endTime = $startTime + $frame_dur_secs;
|
---|
44 |
|
---|
45 | while (my $line = <$ipfh>) {
|
---|
46 | chomp($line);
|
---|
47 | $line =~ s/\@startTime\@/$startTime/g;
|
---|
48 | $line =~ s/\@endTime\@/$endTime/g;
|
---|
49 |
|
---|
50 | print $opfh "$line\n";
|
---|
51 | }
|
---|
52 |
|
---|
53 | close($opfh);
|
---|
54 | close($ipfh);
|
---|
55 | }
|
---|
56 |
|
---|
57 |
|
---|
58 | #
|
---|
59 | # Extract features and convert to CSV
|
---|
60 | #
|
---|
61 | sub extract_audio_features_as_csv
|
---|
62 | {
|
---|
63 | my ($audio_root,$audio_file,$profile_file,$ignore_fields, $fallback_rec) = @_;
|
---|
64 |
|
---|
65 | my $tmp_json_file = "tmp/${audio_root}_essentiafeatures.json";
|
---|
66 | my $tmp_csv_file = "tmp/${audio_root}_essentiafeatures.csv";
|
---|
67 |
|
---|
68 | my $extract_cmd = "essentia_streaming_extractor_music $audio_file $tmp_json_file $profile_file";
|
---|
69 | my $extract_status = system($extract_cmd);
|
---|
70 |
|
---|
71 | if ($extract_status != 0) {
|
---|
72 | if (($extract_status == 256) && defined($fallback_rec)) {
|
---|
73 | # effectively error status '1'
|
---|
74 | print "$!\n";
|
---|
75 | print "Warning: Failed to run command with exit status 1:\n";
|
---|
76 | print " $extract_cmd\n";
|
---|
77 | print "\n";
|
---|
78 | print "The most likely issue is that the segement of audio process was silence\n";
|
---|
79 | print "=> Generating row of 0 feature values for CSV output\n";
|
---|
80 |
|
---|
81 | my @lines = ($fallback_rec->{'headerline'}, $fallback_rec->{'zeroline'});
|
---|
82 | return \@lines;
|
---|
83 | }
|
---|
84 | else {
|
---|
85 |
|
---|
86 | print STDERR "$!\n";
|
---|
87 | print STDERR "Error: Failed to run command:\n";
|
---|
88 | print STDERR " $extract_cmd\n";
|
---|
89 | return undef;
|
---|
90 | }
|
---|
91 | }
|
---|
92 |
|
---|
93 | my $convert_cmd = "json_to_csv.py -i $tmp_json_file -o $tmp_csv_file --ignore $ignore_fields";
|
---|
94 | my $convert_status = system($convert_cmd);
|
---|
95 |
|
---|
96 | # Whatever the outcome of this command, now finished with the tmp_json_file
|
---|
97 | unlink($tmp_json_file) or die "Can't delete $tmp_json_file: $!\n";
|
---|
98 |
|
---|
99 | if ($convert_status != 0) {
|
---|
100 | print STDERR "$!\n";
|
---|
101 | print STDERR "Error: Failed to run command:\n";
|
---|
102 | print STDERR " $convert_cmd\n";
|
---|
103 | return undef;
|
---|
104 | }
|
---|
105 |
|
---|
106 |
|
---|
107 | open(my $ifh, '<', $tmp_csv_file) or die $!;
|
---|
108 |
|
---|
109 | # read in lines to array: single line shot
|
---|
110 | chomp(my @lines = <$ifh>);
|
---|
111 |
|
---|
112 | # my @lines = ();
|
---|
113 | #
|
---|
114 | # while (my $line = <$ifh>) {
|
---|
115 | # chomp($line);
|
---|
116 | # push(@lines,$line);
|
---|
117 | # }
|
---|
118 |
|
---|
119 | close($ifh);
|
---|
120 |
|
---|
121 | unlink($tmp_csv_file) or die "Can't delete $tmp_csv_file: $!\n";
|
---|
122 |
|
---|
123 | return \@lines;
|
---|
124 | }
|
---|
125 |
|
---|
126 | sub generate_csv_feature_header
|
---|
127 | {
|
---|
128 | my $exemplar_inputfile="import/00/ds_20491_4100.m4a";
|
---|
129 |
|
---|
130 | # _essentiafeatures_frames.csv
|
---|
131 |
|
---|
132 |
|
---|
133 | }
|
---|
134 |
|
---|
135 |
|
---|
136 | sub ensure_representative_fallback
|
---|
137 | {
|
---|
138 | my ($audio_file,$profile_file,$ignore_fields) = @_;
|
---|
139 |
|
---|
140 | my $fallback_rec = {};
|
---|
141 |
|
---|
142 | my $representative_headerline_file = "etc/representative_header.csv";
|
---|
143 | my $representative_zeroline_file = "etc/representative_zeroline.csv";
|
---|
144 |
|
---|
145 | if ((! -f $representative_headerline_file) || (! -f $representative_zeroline_file)) {
|
---|
146 | print "Generating Representative Fallback files for CSV Header and Zero-val files:\n";
|
---|
147 | print " $representative_headerline_file and $representative_zeroline_file\n";
|
---|
148 | print "\n";
|
---|
149 |
|
---|
150 | my ($full_audio_root) = ($audio_file =~ m/^(.*)\..+?$/);
|
---|
151 | my ($audio_root) = ($full_audio_root =~ m/^(?:.*\/)?(.*?)$/);
|
---|
152 |
|
---|
153 | my $lines = extract_audio_features_as_csv($audio_root,$audio_file,$profile_file,$ignore_fields,undef);
|
---|
154 |
|
---|
155 | # Count how many elements in the first line (which is the CSV header)
|
---|
156 | my $header_line = $lines->[0];
|
---|
157 | my @header_line_vals = split(",",$header_line);
|
---|
158 |
|
---|
159 | # Build an array full of zeros to match
|
---|
160 | my @zero_vals = ();
|
---|
161 | for my $v (@header_line_vals) {
|
---|
162 | push(@zero_vals,0);
|
---|
163 | }
|
---|
164 |
|
---|
165 | my $zero_line = join(",",@zero_vals);
|
---|
166 |
|
---|
167 | # output headerline
|
---|
168 | open(my $ofh, '>', $representative_headerline_file) or die $!;
|
---|
169 | print $ofh "$header_line\n";
|
---|
170 | close($ofh);
|
---|
171 |
|
---|
172 |
|
---|
173 | # output zeroline
|
---|
174 | open($ofh, '>', $representative_zeroline_file) or die $!;
|
---|
175 | print $ofh "$zero_line\n";
|
---|
176 | close($ofh);
|
---|
177 |
|
---|
178 | $fallback_rec->{'headerline'} = $header_line;
|
---|
179 | $fallback_rec->{'zeroline'} = $zero_line;
|
---|
180 | }
|
---|
181 | else {
|
---|
182 | # read in files
|
---|
183 | print "Reading in Representative Fallback files for CSV Header and Zero-val files:\n";
|
---|
184 | print " $representative_headerline_file and $representative_zeroline_file\n";
|
---|
185 | print "\n";
|
---|
186 |
|
---|
187 | open(my $ifh, '<', $representative_headerline_file) or die $!;
|
---|
188 | chomp(my @header_lines = <$ifh>);
|
---|
189 | close($ifh);
|
---|
190 |
|
---|
191 | open($ifh, '<', $representative_zeroline_file) or die $!;
|
---|
192 | chomp(my @zero_lines = <$ifh>);
|
---|
193 | close($ifh);
|
---|
194 |
|
---|
195 |
|
---|
196 | $fallback_rec->{'headerline'} = $header_lines[0];
|
---|
197 | $fallback_rec->{'zeroline'} = $zero_lines[0];
|
---|
198 |
|
---|
199 | }
|
---|
200 |
|
---|
201 | return $fallback_rec;
|
---|
202 | }
|
---|
203 |
|
---|
204 |
|
---|
205 | sub main
|
---|
206 | {
|
---|
207 | my $representative_audio_file = "import/00/ds_20491_4100.m4a";
|
---|
208 |
|
---|
209 |
|
---|
210 | if (scalar(@ARGV) != 3) {
|
---|
211 | print STDERR "****\n";
|
---|
212 | print STDERR "* Error: incorrect usage\n";
|
---|
213 | print STDERR "* Usage: $0 frame-step-in-secs frame-duration-in-secs audio_input_file\n";
|
---|
214 | print STDERR "****\n";
|
---|
215 | exit(1);
|
---|
216 | }
|
---|
217 |
|
---|
218 | my $frame_step_secs = $ARGV[0];
|
---|
219 | my $frame_dur_secs = $ARGV[1];
|
---|
220 |
|
---|
221 | my $audio_file = $ARGV[2];
|
---|
222 | my ($full_audio_root) = ($audio_file =~ m/^(.*)\..+?$/);
|
---|
223 | my ($audio_root) = ($full_audio_root =~ m/^(?:.*\/)?(.*?)$/);
|
---|
224 |
|
---|
225 |
|
---|
226 | if ( ! -d "tmp" ) {
|
---|
227 | print "Creating directory: tmp\n";
|
---|
228 | mkdir("tmp");
|
---|
229 | }
|
---|
230 |
|
---|
231 |
|
---|
232 | my $csv_file = "${full_audio_root}_essentiafeatures_frames.csv";
|
---|
233 |
|
---|
234 | my $pwd = cwd();
|
---|
235 | my $profile_template_file = "$pwd/essentia-2013-2014.profile.in";
|
---|
236 | my $profile_file = "$pwd/essentia-2013-2014.profile";
|
---|
237 |
|
---|
238 | # knock out any arrays in the JSON extracted features file
|
---|
239 | my $ignore_fields="\
|
---|
240 | lowlevel.barkbands.* \
|
---|
241 | lowlevel.erbbands.* \
|
---|
242 | lowlevel.gfcc.* \
|
---|
243 | lowlevel.melbands.* \
|
---|
244 | lowlevel.melbands128.* \
|
---|
245 | lowlevel.mfcc.* \
|
---|
246 | \
|
---|
247 | lowlevel.spectral_contrast_coeffs.* \
|
---|
248 | lowlevel.spectral_contrast_valleys.* \
|
---|
249 | \
|
---|
250 | metadata.* \
|
---|
251 | \
|
---|
252 | rhythm.beats_loudness_band_ratio.* \
|
---|
253 | rhythm.beats_position.* \
|
---|
254 | rhythm.bpm_histogram.* \
|
---|
255 | \
|
---|
256 | tonal.hpcp.* \
|
---|
257 | tonal.chords_histogram.* \
|
---|
258 | tonal.thpcp.*";
|
---|
259 |
|
---|
260 | $ignore_fields =~ s/\n//sg;
|
---|
261 |
|
---|
262 |
|
---|
263 | generate_bespoke_profile(0,$frame_dur_secs,$profile_template_file,$profile_file);
|
---|
264 | my $fallback_rec = ensure_representative_fallback($representative_audio_file,$profile_file,$ignore_fields);
|
---|
265 |
|
---|
266 | my $audio_duration = get_audio_duration($audio_file);
|
---|
267 |
|
---|
268 | if ( ! -f $csv_file ) {
|
---|
269 |
|
---|
270 | print "******\n";
|
---|
271 | print "* Running Essentia music extractor\n";
|
---|
272 | print "* on input file: $audio_file (duration $audio_duration)\n";
|
---|
273 | print "* with profile: $profile_file\n";
|
---|
274 | print "* generating output: $csv_file\n";
|
---|
275 | print "****\n";
|
---|
276 |
|
---|
277 | open(my $ofh, '>', $csv_file) or die $!;
|
---|
278 |
|
---|
279 | for (my $t=0; $t<$audio_duration; $t+=$frame_step_secs) {
|
---|
280 | ##print "," if ($t>0);
|
---|
281 | print "*\n";
|
---|
282 | print "*\n";
|
---|
283 | print "* ### [Time step: $t]\n";
|
---|
284 | print "*\n";
|
---|
285 | print "*\n";
|
---|
286 |
|
---|
287 | generate_bespoke_profile($t,$frame_dur_secs,$profile_template_file,$profile_file);
|
---|
288 |
|
---|
289 | my $lines = extract_audio_features_as_csv($audio_root,$audio_file,$profile_file,$ignore_fields, $fallback_rec);
|
---|
290 | if (!defined($lines) ) {
|
---|
291 | next;
|
---|
292 | }
|
---|
293 |
|
---|
294 | if ($t == 0) {
|
---|
295 | # output first line from $tmp_csv_file to $csv_file
|
---|
296 | print $ofh $lines->[0], "\n";
|
---|
297 | }
|
---|
298 |
|
---|
299 | # append 2nd line of $tmp_json_file (i.e. data vals) to $csv_file
|
---|
300 | print $ofh $lines->[1], "\n";
|
---|
301 |
|
---|
302 | # break out of loop if there isn't enough time left for a full $frame_dur_secs
|
---|
303 | my $end_of_next_frame = $t+$frame_step_secs+$frame_dur_secs;
|
---|
304 | last if ($end_of_next_frame > $audio_duration);
|
---|
305 | }
|
---|
306 |
|
---|
307 | close($ofh);
|
---|
308 |
|
---|
309 | print "******\n";
|
---|
310 |
|
---|
311 | }
|
---|
312 | else {
|
---|
313 | print "* Skipping frame-by-frame audio features computation as $csv_file already exists\n";
|
---|
314 | }
|
---|
315 | }
|
---|
316 |
|
---|
317 | main();
|
---|
318 |
|
---|
319 |
|
---|
320 |
|
---|