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.0412';
7              
8 1     1   1330 use Moo;
  1         11225  
  1         4  
9 1     1   1970 use strictures 2;
  1         1596  
  1         40  
10              
11 1     1   930 use Text::CSV ();
  1         21873  
  1         29  
12 1     1   466 use File::ShareDir qw/ dist_dir /;
  1         27377  
  1         71  
13 1     1   6 use List::Util qw/ any /;
  1         2  
  1         79  
14              
15 1     1   500 use namespace::clean;
  1         11505  
  1         12  
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 1014 my ($self) = @_;
39              
40             # Collect the key signatures and titles
41 1         3 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         35 while ( my $line = readline($fh) ) {
47 64         108 chomp $line;
48 64 100 100     236 next if $line =~ /^\s*$/ || $line =~ /^#/;
49 60         203 my @parts = split /\s+/, $line, 4;
50 60         357 $data{ $parts[0] } = {
51             bwv => $parts[1],
52             key => $parts[2],
53             title => $parts[3],
54             };
55             }
56              
57 1         32 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       221 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         54 while ( my $row = $csv->getline($fh) ) {
70              
71 5665         195567 ( my $id = $row->[0] ) =~ s/\s*//g;
72              
73 5665         10833 my $notes = '';
74              
75 5665         10919 for my $note ( 2 .. 13 ) {
76 67980 100       115837 $notes .= $row->[$note] eq 'YES' ? 1 : 0;
77             }
78              
79 5665         19638 ( my $bass = $row->[14] ) =~ s/\s*//g;
80 5665         19076 ( my $accent = $row->[15] ) =~ s/\s*//g;
81 5665         23853 ( my $chord = $row->[16] ) =~ s/\s*//g;
82              
83 5665   66     13701 $progression->{$id}{key} ||= $data{$id}{key};
84 5665   66     11103 $progression->{$id}{bwv} ||= $data{$id}{bwv};
85 5665   66     10872 $progression->{$id}{title} ||= $data{$id}{title};
86              
87 5665         23420 my $struct = {
88             notes => $notes,
89             bass => $bass,
90             accent => $accent,
91             chord => $chord,
92             };
93              
94 5665         9148 push @{ $progression->{$id}{events} }, $struct;
  5665         120478  
95             }
96              
97 1 50       76 $csv->eof or die $csv->error_diag;
98 1         44 close $fh;
99              
100 1         15 $self->data($progression);
101              
102 1         73 return $self->data;
103             }
104              
105              
106             sub search {
107 63     63 1 7737 my ( $self, %args ) = @_;
108              
109 63         141 my %results = ();
110              
111 63 100       203 if ( $args{id} ) {
112 26         115 my @ids = split /\s+/, $args{id};
113              
114 26         54 for my $id ( @ids ) {
115 30         128 $results{$id} = $self->data->{$id};
116             }
117             }
118              
119 63 100       174 if ( $args{key} ) {
120 12 100       42 my @iter = keys %results ? keys %results : keys %{ $self->data };
  7         83  
121              
122 12         72 my @keys = split /\s+/, $args{key};
123              
124 12         24 for my $id ( @iter ) {
125 427 100       744 if ( $results{$id} ) {
126             delete $results{$id}
127 7 100   9   37 unless any { $_ eq $results{$id}{key} } @keys;
  9         42  
128             }
129             else {
130             $results{$id} = $self->data->{$id}
131 420 100   2005   1038 if any { $_ eq $self->data->{$id}{key} } @keys;
  2005         4066  
132             }
133             }
134             }
135              
136 63 100       170 if ( $args{bass} ) {
137 8         69 %results = $self->_search_param( bass => $args{bass}, \%results );
138             }
139              
140 63 100       161 if ( $args{chord} ) {
141 8         34 %results = $self->_search_param( chord => $args{chord}, \%results );
142             }
143              
144 63 100       167 if ( $args{notes} ) {
145 27 100       109 my @iter = keys %results ? keys %results : keys %{ $self->data };
  16         323  
146              
147 27 100       142 my $and = $args{notes} =~ /&/ ? 1 : 0;
148 27 100       152 my $re = $and ? qr/\s*&\s*/ : qr/\s+/;
149              
150 27         217 my @notes = split $re, $args{notes};
151              
152 27         283 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         71 ID: for my $id ( @iter ) {
173 972         1783 my %and_notes = ();
174              
175 972         1331 my $match = 0;
176              
177 972         1380 for my $event ( @{ $self->data->{$id}{events} } ) {
  972         5855  
178 92219         337275 my @bitstring = split //, $event->{notes};
179              
180 92219         130759 my $i = 0;
181              
182 92219         133930 for my $bit ( @bitstring ) {
183 1106628 100       1871671 if ( $bit ) {
184 303415         503236 for my $note ( sort @notes ) {
185 530590 100 100     1501535 if ( defined $index{$note} && $i == $index{$note} ) {
186 49038 100       82266 if ( $and ) {
187 20572         33525 $and_notes{$note}++;
188             }
189             else {
190 28466         43040 $match++;
191             }
192             }
193             }
194             }
195              
196 1106628         1614660 $i++;
197             }
198             }
199              
200 972 100       1896 if ( $and ) {
201 305 100       840 if ( keys %and_notes ) {
202 292         440 my %notes;
203 292         870 @notes{@notes} = undef;
204              
205 292         453 my $i = 0;
206              
207 292         679 for my $n ( keys %and_notes ) {
208             $i++
209 547 50       1119 if exists $notes{$n};
210             }
211              
212 292 100       613 if ( $i == scalar keys %notes ) {
213 198         1460 $results{$id} = $self->data->{$id};
214             }
215             else {
216             delete $results{$id}
217 94 100       426 if $results{$id};
218             }
219             }
220             }
221             else {
222 667 100 100     2168 if ( $results{$id} && $match <= 0 ) {
    100          
223 2         15 delete $results{$id};
224             }
225             elsif ( $match > 0 ) {
226 547         3442 $results{$id} = $self->data->{$id};
227             }
228             }
229             }
230             }
231              
232 63         785 return \%results;
233             }
234              
235              
236             sub bits2notes {
237 20     20 1 55 my ( $self, $string, $accidental ) = @_;
238              
239 20   100     96 $accidental ||= 'b';
240              
241 20         30 my @notes = ();
242              
243 1     1   1786 no warnings 'qw';
  1         2  
  1         594  
244 20         57 my @positions = qw( C C#|Db D D#|Eb E F F#|Gb G G#|Ab A A#|Bb B );
245              
246 20         79 my @bits = split //, $string;
247              
248 20         31 my $i = 0;
249              
250 20         41 for my $bit ( @bits ) {
251 240 100       447 if ( $bit ) {
252 21         58 my @note = split /\|/, $positions[$i];
253 21         34 my $note = '';
254              
255 21 100       50 if ( @note > 1 ) {
256 12 100       28 $note = $accidental eq '#' ? $note[0] : $note[1];
257             }
258             else {
259 9         15 $note = $note[0];
260             }
261              
262 21         57 push @notes, $note;
263             }
264              
265 240         354 $i++;
266             }
267              
268 20         189 return \@notes;
269             }
270              
271             sub _search_param {
272 16     16   48 my ( $self, $name, $param, $seen ) = @_;
273              
274 16 100       60 my @iter = keys %$seen ? keys %$seen : keys %{ $self->data };
  8         90  
275              
276 16         33 my %results = ();
277              
278 16 100       75 my $and = $param =~ /&/ ? 1 : 0;
279 16 100       74 my $re = $and ? qr/\s*&\s*/ : qr/\s+/;
280              
281 16         28 my %notes = ();
282 16         118 @notes{ split $re, $param } = undef;
283              
284 16         41 ID: for my $id ( @iter ) {
285 488         709 my %and_notes = ();
286              
287 488         676 my $match = 0;
288              
289 488         649 for my $event ( @{ $self->data->{$id}{events} } ) {
  488         1501  
290 46616         73738 for my $note ( keys %notes ) {
291 69924 100       148686 if ( $note eq $event->{$name} ) {
292 3516 100       5344 if ( $and ) {
293 1172         2077 $and_notes{$note}++;
294             }
295             else {
296 2344         3782 $match++;
297             }
298             }
299             }
300             }
301              
302 488 100       819 if ( $and ) {
303 122 100       247 if ( keys %and_notes ) {
304 86         173 my $i = 0;
305              
306 86         152 for my $n ( keys %and_notes ) {
307             $i++
308 86 50       180 if exists $notes{$n};
309             }
310              
311 86 50       159 if ( $i == scalar keys %notes ) {
312 0         0 $results{$id} = $self->data->{$id};
313             }
314             else {
315             delete $results{$id}
316 86 50       214 if $results{$id};
317             }
318             }
319             }
320             else {
321 366 50 33     989 if ( $results{$id} && $match <= 0 ) {
    100          
322 0         0 delete $results{$id};
323             }
324             elsif ( $match > 0 ) {
325 172         515 $results{$id} = $self->data->{$id};
326             }
327             }
328             }
329              
330 16         230 return %results;
331             }
332              
333             1;
334              
335             __END__