File Coverage

blib/lib/Music/BachChoralHarmony.pm
Criterion Covered Total %
statement 151 153 98.6
branch 77 86 89.5
condition 18 23 78.2
subroutine 13 13 100.0
pod 3 3 100.0
total 262 278 94.2


line stmt bran cond sub pod time code
1             package Music::BachChoralHarmony;
2             our $AUTHORITY = 'cpan:GENE';
3              
4             # ABSTRACT: Parse the UCI Bach choral harmony data set
5              
6             our $VERSION = '0.0410';
7              
8 1     1   1191 use Moo;
  1         9785  
  1         5  
9 1     1   1690 use strictures 2;
  1         1405  
  1         35  
10 1     1   580 use namespace::clean;
  1         9923  
  1         15  
11              
12 1     1   915 use Text::CSV;
  1         18373  
  1         44  
13 1     1   474 use File::ShareDir qw/ dist_dir /;
  1         23154  
  1         54  
14 1     1   7 use List::Util qw/ any /;
  1         3  
  1         1311  
15              
16              
17             has data_file => (
18             is => 'ro',
19             default => sub { dist_dir('Music-BachChoralHarmony') . '/jsbach_chorals_harmony.data' },
20             );
21              
22              
23             has key_title => (
24             is => 'ro',
25             default => sub { dist_dir('Music-BachChoralHarmony') . '/jsbach_BWV_keys_titles.txt' },
26             );
27              
28              
29             has data => (
30             is => 'rw',
31             init_arg => undef,
32             default => sub { {} },
33             );
34              
35              
36             sub parse {
37 1     1 1 893 my ($self) = @_;
38              
39             # Collect the key signatures and titles
40 1         2 my %data;
41              
42 1 50       43 open my $fh, '<', $self->key_title
43             or die "Can't read ", $self->key_title, ": $!";
44              
45 1         27 while ( my $line = readline($fh) ) {
46 64         94 chomp $line;
47 64 100 100     198 next if $line =~ /^\s*$/ || $line =~ /^#/;
48 60         166 my @parts = split /\s+/, $line, 4;
49 60         295 $data{ $parts[0] } = {
50             bwv => $parts[1],
51             key => $parts[2],
52             title => $parts[3],
53             };
54             }
55              
56 1         33 close $fh;
57              
58             # Collect the events
59 1 50       17 my $csv = Text::CSV->new( { binary => 1 } )
60             or die "Can't use CSV: ", Text::CSV->error_diag;
61              
62 1 50       201 open $fh, '<', $self->data_file
63             or die "Can't read ", $self->data_file, ": $!";
64              
65 1         3 my $progression;
66              
67             # 000106b_ 2 YES NO NO NO YES NO NO YES NO NO NO NO E 5 C_M
68 1         47 while ( my $row = $csv->getline($fh) ) {
69              
70 5665         159062 ( my $id = $row->[0] ) =~ s/\s*//g;
71              
72 5665         8821 my $notes = '';
73              
74 5665         8472 for my $note ( 2 .. 13 ) {
75 67980 100       94322 $notes .= $row->[$note] eq 'YES' ? 1 : 0;
76             }
77              
78 5665         16030 ( my $bass = $row->[14] ) =~ s/\s*//g;
79 5665         15476 ( my $accent = $row->[15] ) =~ s/\s*//g;
80 5665         19519 ( my $chord = $row->[16] ) =~ s/\s*//g;
81              
82 5665   66     10764 $progression->{$id}{key} ||= $data{$id}{key};
83 5665   66     8300 $progression->{$id}{bwv} ||= $data{$id}{bwv};
84 5665   66     8589 $progression->{$id}{title} ||= $data{$id}{title};
85              
86 5665         19128 my $struct = {
87             notes => $notes,
88             bass => $bass,
89             accent => $accent,
90             chord => $chord,
91             };
92              
93 5665         7203 push @{ $progression->{$id}{events} }, $struct;
  5665         99352  
94             }
95              
96 1 50       46 $csv->eof or die $csv->error_diag;
97 1         21 close $fh;
98              
99 1         11 $self->data($progression);
100              
101 1         66 return $self->data;
102             }
103              
104              
105             sub search {
106 63     63 1 6279 my ( $self, %args ) = @_;
107              
108 63         124 my %results = ();
109              
110 63 100       186 if ( $args{id} ) {
111 26         100 my @ids = split /\s+/, $args{id};
112              
113 26         45 for my $id ( @ids ) {
114 30         109 $results{$id} = $self->data->{$id};
115             }
116             }
117              
118 63 100       217 if ( $args{key} ) {
119 12 100       43 my @iter = keys %results ? keys %results : keys %{ $self->data };
  7         80  
120              
121 12         58 my @keys = split /\s+/, $args{key};
122              
123 12         18 for my $id ( @iter ) {
124 427 100       585 if ( $results{$id} ) {
125             delete $results{$id}
126 7 100   9   33 unless any { $_ eq $results{$id}{key} } @keys;
  9         38  
127             }
128             else {
129             $results{$id} = $self->data->{$id}
130 420 100   2005   846 if any { $_ eq $self->data->{$id}{key} } @keys;
  2005         3242  
131             }
132             }
133             }
134              
135 63 100       140 if ( $args{bass} ) {
136 8         31 %results = $self->_search_param( bass => $args{bass}, \%results );
137             }
138              
139 63 100       148 if ( $args{chord} ) {
140 8         24 %results = $self->_search_param( chord => $args{chord}, \%results );
141             }
142              
143 63 100       165 if ( $args{notes} ) {
144 27 100       123 my @iter = keys %results ? keys %results : keys %{ $self->data };
  16         397  
145              
146 27 100       141 my $and = $args{notes} =~ /&/ ? 1 : 0;
147 27 100       153 my $re = $and ? qr/\s*&\s*/ : qr/\s+/;
148              
149 27         200 my @notes = split $re, $args{notes};
150              
151 27         364 my %index = (
152             'C' => 0,
153             'C#' => 1,
154             'Db' => 1,
155             'D' => 2,
156             'D#' => 3,
157             'Eb' => 3,
158             'E' => 4,
159             'F' => 5,
160             'F#' => 6,
161             'Gb' => 6,
162             'G' => 7,
163             'G#' => 8,
164             'Ab' => 8,
165             'A' => 9,
166             'A#' => 10,
167             'Bb' => 10,
168             'B' => 11,
169             );
170              
171 27         57 ID: for my $id ( @iter ) {
172 972         1825 my %and_notes = ();
173              
174 972         1425 my $match = 0;
175              
176 972         1312 for my $event ( @{ $self->data->{$id}{events} } ) {
  972         4930  
177 92219         286693 my @bitstring = split //, $event->{notes};
178              
179 92219         125386 my $i = 0;
180              
181 92219         133044 for my $bit ( @bitstring ) {
182 1106628 100       1732719 if ( $bit ) {
183 303415         496080 for my $note ( sort @notes ) {
184 530590 100 100     1474196 if ( defined $index{$note} && $i == $index{$note} ) {
185 49038 100       79227 if ( $and ) {
186 20572         31286 $and_notes{$note}++;
187             }
188             else {
189 28466         42706 $match++;
190             }
191             }
192             }
193             }
194              
195 1106628         1588896 $i++;
196             }
197             }
198              
199 972 100       1991 if ( $and ) {
200 305 100       973 if ( keys %and_notes ) {
201 292         448 my %notes;
202 292         996 @notes{@notes} = undef;
203              
204 292         443 my $i = 0;
205              
206 292         677 for my $n ( keys %and_notes ) {
207             $i++
208 547 50       1112 if exists $notes{$n};
209             }
210              
211 292 100       619 if ( $i == scalar keys %notes ) {
212 198         1582 $results{$id} = $self->data->{$id};
213             }
214             else {
215             delete $results{$id}
216 94 100       466 if $results{$id};
217             }
218             }
219             }
220             else {
221 667 100 100     2281 if ( $results{$id} && $match <= 0 ) {
    100          
222 2         14 delete $results{$id};
223             }
224             elsif ( $match > 0 ) {
225 547         3390 $results{$id} = $self->data->{$id};
226             }
227             }
228             }
229             }
230              
231 63         695 return \%results;
232             }
233              
234              
235             sub bits2notes {
236 20     20 1 44 my ( $self, $string, $accidental ) = @_;
237              
238 20   100     74 $accidental ||= 'b';
239              
240 20         26 my @notes = ();
241              
242 1     1   7 no warnings 'qw';
  1         2  
  1         514  
243 20         51 my @positions = qw( C C#|Db D D#|Eb E F F#|Gb G G#|Ab A A#|Bb B );
244              
245 20         64 my @bits = split //, $string;
246              
247 20         28 my $i = 0;
248              
249 20         33 for my $bit ( @bits ) {
250 240 100       345 if ( $bit ) {
251 21         40 my @note = split /\|/, $positions[$i];
252 21         26 my $note = '';
253              
254 21 100       43 if ( @note > 1 ) {
255 12 100       25 $note = $accidental eq '#' ? $note[0] : $note[1];
256             }
257             else {
258 9         13 $note = $note[0];
259             }
260              
261 21         45 push @notes, $note;
262             }
263              
264 240         287 $i++;
265             }
266              
267 20         102 return \@notes;
268             }
269              
270             sub _search_param {
271 16     16   37 my ( $self, $name, $param, $seen ) = @_;
272              
273 16 100       61 my @iter = keys %$seen ? keys %$seen : keys %{ $self->data };
  8         89  
274              
275 16         26 my %results = ();
276              
277 16 100       50 my $and = $param =~ /&/ ? 1 : 0;
278 16 100       60 my $re = $and ? qr/\s*&\s*/ : qr/\s+/;
279              
280 16         27 my %notes = ();
281 16         95 @notes{ split $re, $param } = undef;
282              
283 16         32 ID: for my $id ( @iter ) {
284 488         606 my %and_notes = ();
285              
286 488         524 my $match = 0;
287              
288 488         528 for my $event ( @{ $self->data->{$id}{events} } ) {
  488         1157  
289 46616         59514 for my $note ( keys %notes ) {
290 69924 100       119616 if ( $note eq $event->{$name} ) {
291 3516 100       4256 if ( $and ) {
292 1172         1558 $and_notes{$note}++;
293             }
294             else {
295 2344         3246 $match++;
296             }
297             }
298             }
299             }
300              
301 488 100       700 if ( $and ) {
302 122 100       208 if ( keys %and_notes ) {
303 86         94 my $i = 0;
304              
305 86         117 for my $n ( keys %and_notes ) {
306             $i++
307 86 50       152 if exists $notes{$n};
308             }
309              
310 86 50       116 if ( $i == scalar keys %notes ) {
311 0         0 $results{$id} = $self->data->{$id};
312             }
313             else {
314             delete $results{$id}
315 86 50       175 if $results{$id};
316             }
317             }
318             }
319             else {
320 366 50 33     802 if ( $results{$id} && $match <= 0 ) {
    100          
321 0         0 delete $results{$id};
322             }
323             elsif ( $match > 0 ) {
324 172         408 $results{$id} = $self->data->{$id};
325             }
326             }
327             }
328              
329 16         192 return %results;
330             }
331              
332             1;
333              
334             __END__