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.0411';
7              
8 1     1   1060 use Moo;
  1         9304  
  1         4  
9 1     1   1534 use strictures 2;
  1         1320  
  1         33  
10              
11 1     1   749 use Text::CSV ();
  1         17797  
  1         25  
12 1     1   389 use File::ShareDir qw/ dist_dir /;
  1         21884  
  1         61  
13 1     1   16 use List::Util qw/ any /;
  1         2  
  1         74  
14              
15 1     1   378 use namespace::clean;
  1         9800  
  1         7  
16              
17              
18             has data_file => (
19             is => 'ro',
20             default => sub { dist_dir('Music-BachChoralHarmony') . '/jsbach_chorals_harmony.data' },
21             );
22              
23              
24             has key_title => (
25             is => 'ro',
26             default => sub { dist_dir('Music-BachChoralHarmony') . '/jsbach_BWV_keys_titles.txt' },
27             );
28              
29              
30             has data => (
31             is => 'rw',
32             init_arg => undef,
33             default => sub { {} },
34             );
35              
36              
37             sub parse {
38 1     1 1 883 my ($self) = @_;
39              
40             # Collect the key signatures and titles
41 1         2 my %data;
42              
43 1 50       46 open my $fh, '<', $self->key_title
44             or die "Can't read ", $self->key_title, ": $!";
45              
46 1         33 while ( my $line = readline($fh) ) {
47 64         85 chomp $line;
48 64 100 100     198 next if $line =~ /^\s*$/ || $line =~ /^#/;
49 60         172 my @parts = split /\s+/, $line, 4;
50 60         273 $data{ $parts[0] } = {
51             bwv => $parts[1],
52             key => $parts[2],
53             title => $parts[3],
54             };
55             }
56              
57 1         33 close $fh;
58              
59             # Collect the events
60 1 50       17 my $csv = Text::CSV->new( { binary => 1 } )
61             or die "Can't use CSV: ", Text::CSV->error_diag;
62              
63 1 50       199 open $fh, '<', $self->data_file
64             or die "Can't read ", $self->data_file, ": $!";
65              
66 1         4 my $progression;
67              
68             # 000106b_ 2 YES NO NO NO YES NO NO YES NO NO NO NO E 5 C_M
69 1         47 while ( my $row = $csv->getline($fh) ) {
70              
71 5665         162174 ( my $id = $row->[0] ) =~ s/\s*//g;
72              
73 5665         9349 my $notes = '';
74              
75 5665         9382 for my $note ( 2 .. 13 ) {
76 67980 100       99568 $notes .= $row->[$note] eq 'YES' ? 1 : 0;
77             }
78              
79 5665         16639 ( my $bass = $row->[14] ) =~ s/\s*//g;
80 5665         16008 ( my $accent = $row->[15] ) =~ s/\s*//g;
81 5665         21138 ( my $chord = $row->[16] ) =~ s/\s*//g;
82              
83 5665   66     11424 $progression->{$id}{key} ||= $data{$id}{key};
84 5665   66     8907 $progression->{$id}{bwv} ||= $data{$id}{bwv};
85 5665   66     8526 $progression->{$id}{title} ||= $data{$id}{title};
86              
87 5665         27592 my $struct = {
88             notes => $notes,
89             bass => $bass,
90             accent => $accent,
91             chord => $chord,
92             };
93              
94 5665         8114 push @{ $progression->{$id}{events} }, $struct;
  5665         103518  
95             }
96              
97 1 50       48 $csv->eof or die $csv->error_diag;
98 1         18 close $fh;
99              
100 1         11 $self->data($progression);
101              
102 1         61 return $self->data;
103             }
104              
105              
106             sub search {
107 63     63 1 11581 my ( $self, %args ) = @_;
108              
109 63         128 my %results = ();
110              
111 63 100       186 if ( $args{id} ) {
112 26         108 my @ids = split /\s+/, $args{id};
113              
114 26         50 for my $id ( @ids ) {
115 30         123 $results{$id} = $self->data->{$id};
116             }
117             }
118              
119 63 100       165 if ( $args{key} ) {
120 12 100       37 my @iter = keys %results ? keys %results : keys %{ $self->data };
  7         75  
121              
122 12         61 my @keys = split /\s+/, $args{key};
123              
124 12         17 for my $id ( @iter ) {
125 427 100       607 if ( $results{$id} ) {
126             delete $results{$id}
127 7 100   9   28 unless any { $_ eq $results{$id}{key} } @keys;
  9         35  
128             }
129             else {
130             $results{$id} = $self->data->{$id}
131 420 100   2005   872 if any { $_ eq $self->data->{$id}{key} } @keys;
  2005         3048  
132             }
133             }
134             }
135              
136 63 100       193 if ( $args{bass} ) {
137 8         30 %results = $self->_search_param( bass => $args{bass}, \%results );
138             }
139              
140 63 100       166 if ( $args{chord} ) {
141 8         26 %results = $self->_search_param( chord => $args{chord}, \%results );
142             }
143              
144 63 100       144 if ( $args{notes} ) {
145 27 100       108 my @iter = keys %results ? keys %results : keys %{ $self->data };
  16         300  
146              
147 27 100       124 my $and = $args{notes} =~ /&/ ? 1 : 0;
148 27 100       128 my $re = $and ? qr/\s*&\s*/ : qr/\s+/;
149              
150 27         174 my @notes = split $re, $args{notes};
151              
152 27         249 my %index = (
153             'C' => 0,
154             'C#' => 1,
155             'Db' => 1,
156             'D' => 2,
157             'D#' => 3,
158             'Eb' => 3,
159             'E' => 4,
160             'F' => 5,
161             'F#' => 6,
162             'Gb' => 6,
163             'G' => 7,
164             'G#' => 8,
165             'Ab' => 8,
166             'A' => 9,
167             'A#' => 10,
168             'Bb' => 10,
169             'B' => 11,
170             );
171              
172 27         51 ID: for my $id ( @iter ) {
173 972         1591 my %and_notes = ();
174              
175 972         1140 my $match = 0;
176              
177 972         1074 for my $event ( @{ $self->data->{$id}{events} } ) {
  972         3420  
178 92219         235293 my @bitstring = split //, $event->{notes};
179              
180 92219         105658 my $i = 0;
181              
182 92219         111133 for my $bit ( @bitstring ) {
183 1106628 100       1464437 if ( $bit ) {
184 303415         406507 for my $note ( sort @notes ) {
185 530590 100 100     1222202 if ( defined $index{$note} && $i == $index{$note} ) {
186 49038 100       65647 if ( $and ) {
187 20572         27572 $and_notes{$note}++;
188             }
189             else {
190 28466         35913 $match++;
191             }
192             }
193             }
194             }
195              
196 1106628         1323930 $i++;
197             }
198             }
199              
200 972 100       1579 if ( $and ) {
201 305 100       701 if ( keys %and_notes ) {
202 292         381 my %notes;
203 292         761 @notes{@notes} = undef;
204              
205 292         354 my $i = 0;
206              
207 292         587 for my $n ( keys %and_notes ) {
208             $i++
209 547 50       930 if exists $notes{$n};
210             }
211              
212 292 100       605 if ( $i == scalar keys %notes ) {
213 198         1134 $results{$id} = $self->data->{$id};
214             }
215             else {
216             delete $results{$id}
217 94 100       326 if $results{$id};
218             }
219             }
220             }
221             else {
222 667 100 100     2087 if ( $results{$id} && $match <= 0 ) {
    100          
223 2         17 delete $results{$id};
224             }
225             elsif ( $match > 0 ) {
226 547         2630 $results{$id} = $self->data->{$id};
227             }
228             }
229             }
230             }
231              
232 63         573 return \%results;
233             }
234              
235              
236             sub bits2notes {
237 20     20 1 45 my ( $self, $string, $accidental ) = @_;
238              
239 20   100     71 $accidental ||= 'b';
240              
241 20         31 my @notes = ();
242              
243 1     1   1548 no warnings 'qw';
  1         2  
  1         510  
244 20         49 my @positions = qw( C C#|Db D D#|Eb E F F#|Gb G G#|Ab A A#|Bb B );
245              
246 20         68 my @bits = split //, $string;
247              
248 20         28 my $i = 0;
249              
250 20         30 for my $bit ( @bits ) {
251 240 100       318 if ( $bit ) {
252 21         45 my @note = split /\|/, $positions[$i];
253 21         27 my $note = '';
254              
255 21 100       39 if ( @note > 1 ) {
256 12 100       28 $note = $accidental eq '#' ? $note[0] : $note[1];
257             }
258             else {
259 9         13 $note = $note[0];
260             }
261              
262 21         45 push @notes, $note;
263             }
264              
265 240         270 $i++;
266             }
267              
268 20         97 return \@notes;
269             }
270              
271             sub _search_param {
272 16     16   43 my ( $self, $name, $param, $seen ) = @_;
273              
274 16 100       56 my @iter = keys %$seen ? keys %$seen : keys %{ $self->data };
  8         110  
275              
276 16         31 my %results = ();
277              
278 16 100       51 my $and = $param =~ /&/ ? 1 : 0;
279 16 100       64 my $re = $and ? qr/\s*&\s*/ : qr/\s+/;
280              
281 16         28 my %notes = ();
282 16         96 @notes{ split $re, $param } = undef;
283              
284 16         38 ID: for my $id ( @iter ) {
285 488         609 my %and_notes = ();
286              
287 488         534 my $match = 0;
288              
289 488         533 for my $event ( @{ $self->data->{$id}{events} } ) {
  488         1177  
290 46616         58936 for my $note ( keys %notes ) {
291 69924 100       122340 if ( $note eq $event->{$name} ) {
292 3516 100       4182 if ( $and ) {
293 1172         1574 $and_notes{$note}++;
294             }
295             else {
296 2344         2955 $match++;
297             }
298             }
299             }
300             }
301              
302 488 100       703 if ( $and ) {
303 122 100       182 if ( keys %and_notes ) {
304 86         103 my $i = 0;
305              
306 86         124 for my $n ( keys %and_notes ) {
307             $i++
308 86 50       135 if exists $notes{$n};
309             }
310              
311 86 50       121 if ( $i == scalar keys %notes ) {
312 0         0 $results{$id} = $self->data->{$id};
313             }
314             else {
315             delete $results{$id}
316 86 50       162 if $results{$id};
317             }
318             }
319             }
320             else {
321 366 50 33     819 if ( $results{$id} && $match <= 0 ) {
    100          
322 0         0 delete $results{$id};
323             }
324             elsif ( $match > 0 ) {
325 172         377 $results{$id} = $self->data->{$id};
326             }
327             }
328             }
329              
330 16         220 return %results;
331             }
332              
333             1;
334              
335             __END__