File Coverage

blib/lib/Test2/Tools/MIDI.pm
Criterion Covered Total %
statement 247 247 100.0
branch 140 146 95.8
condition n/a
subroutine 24 24 100.0
pod 16 16 100.0
total 427 433 98.6


line stmt bran cond sub pod time code
1             # -*- Perl -*-
2             #
3             # A module to test MIDI file contents.
4              
5             package Test2::Tools::MIDI;
6             our $VERSION = '0.02';
7 2     2   530096 use 5.10.0;
  2         7  
8 2     2   12 use strict;
  2         6  
  2         58  
9 2     2   9 use warnings;
  2         3  
  2         100  
10 2     2   11 use Carp 'confess';
  2         3  
  2         117  
11 2     2   12 use Test2::API 'context';
  2         13  
  2         102  
12              
13 2     2   11 use base 'Exporter';
  2         3  
  2         6400  
14             our @EXPORT = qw(
15             midi_aftertouch midi_channel_aftertouch midi_control_change midi_eof
16             midi_footer midi_header midi_note_off midi_note_on midi_patch
17             midi_pitch_wheel midi_skip midi_skip_dtime midi_tempo midi_text midi_track
18             );
19              
20             sub _failure ($$\@) {
21 61     61   14533 my $result = 1;
22 61 100       102 if ( @{ $_[2] } ) {
  61         177  
23             $_[0]->fail( $_[1], join ' ',
24 24         50 map { "$_->[0] [$_->[1],$_->[2]]" } @{ $_[2] } );
  56         318  
  24         57  
25 24         5883 $result = 0;
26             } else {
27 37         144 $_[0]->pass( $_[1] );
28             }
29 61         4847 $_[0]->release;
30 61         2650 return $result;
31             }
32              
33             # note_on, note_off and a few others share this pattern, though you may
34             # need to squint a little to make the "controller" and "value" of
35             # control_change fit the pitch and velocity fields
36             sub _dpv ($$$$$$$) {
37 17     17   52 my ( $fh, $dtime, $channel, $pitch, $velocity, $want_code, $name ) =
38             @_;
39              
40 17         29 my @failure;
41 17         85 my $q = read_vlq($fh);
42 17 100       43 if ( $q != $dtime ) {
43 4         14 push @failure, [ dtime => $q, $dtime ];
44             }
45 17         39 my $amount = read $fh, my $track, 3;
46 17 50       40 confess "$name read $!" unless defined $amount;
47 17 100       40 if ( $amount != 3 ) {
48 1         5 push @failure, [ length => $amount, 3 ];
49 1         7 goto FAIL_DPV;
50             }
51 16         56 my ( $pa, $re, $ci ) = unpack CCC => $track;
52 16         38 my ( $ch, $code ) = ( $pa & 0xF, $pa & 0xF0 );
53 16 100       35 if ( $ch != $channel ) {
54 4         9 push @failure, [ channel => $ch, $channel ];
55             }
56 16 100       37 if ( $code != $want_code ) {
57 2         6 push @failure, [ code => $ch, $want_code ];
58             }
59 16 100       34 if ( $re != $pitch ) {
60 4         9 push @failure, [ pitch => $re, $pitch ];
61             }
62 16 100       34 if ( $ci != $velocity ) {
63 4         9 push @failure, [ velocity => $ci, $velocity ];
64             }
65             FAIL_DPV:
66 17         52 _failure( context(), $name, @failure );
67             }
68              
69             sub midi_aftertouch ($$$$$) {
70 1     1 1 42 push @_, 0xA0, 'MIDI key_after_touch';
71 1         5 goto &_dpv;
72             }
73              
74             sub midi_channel_aftertouch ($$$$) {
75 3     3 1 428 my ( $fh, $dtime, $channel, $velocity ) = @_;
76 3         7 my @failure;
77 3         38 my $q = read_vlq($fh);
78 3 100       11 if ( $q != $dtime ) {
79 1         5 push @failure, [ dtime => $q, $dtime ];
80             }
81 3         7 my $amount = read $fh, my $track, 2;
82 3 50       8 confess "midi_channel_aftertouch read $!" unless defined $amount;
83 3 100       9 if ( $amount != 2 ) {
84 1         4 push @failure, [ length => $amount, 2 ];
85 1         5 goto FAIL_CHAFT;
86             }
87 2         8 my ( $pa, $velo ) = unpack CC => $track;
88 2         6 my ( $ch, $code ) = ( $pa & 0xF, $pa & 0xF0 );
89 2 100       6 if ( $ch != $channel ) {
90 1         3 push @failure, [ channel => $ch, $channel ];
91             }
92 2 100       5 if ( $code != 0xD0 ) {
93 1         3 push @failure, [ code => $code, 0xD0 ];
94             }
95 2 100       8 if ( $velo != $velocity ) {
96 1         3 push @failure, [ velocity => $velo, $velocity ];
97             }
98             FAIL_CHAFT:
99 3         12 _failure( context(), 'MIDI channel_aftertouch', @failure );
100             }
101              
102             sub midi_control_change ($$$$$) {
103 1     1 1 4021 push @_, 0xB0, 'MIDI control_change';
104 1         4 goto &_dpv;
105             }
106              
107             sub midi_eof ($) {
108 2     2 1 429 my $eof = eof $_[0];
109 2         8 my $ctx = context();
110 2         208 my $result = 1;
111 2 100       6 if ($eof) {
112 1         9 $ctx->pass('MIDI EOF');
113             } else {
114 1         6 $ctx->fail('MIDI EOF');
115 1         156 $result = 0;
116             }
117 2         148 $ctx->release;
118 2         80 return $result;
119             }
120              
121             sub midi_footer ($$) {
122 3     3 1 46 my ( $fh, $dtime ) = @_;
123 3         7 my @failure;
124 3         8 my $q = read_vlq($fh);
125 3 100       10 if ( $q != $dtime ) {
126 1         4 push @failure, [ dtime => $q, $dtime ];
127             }
128 3         8 my $amount = read $fh, my $footer, 3;
129 3 50       11 confess "midi_footer read $!" unless defined $amount;
130 3 100       8 if ( $amount != 3 ) {
131 1         4 push @failure, [ length => $amount, 3 ];
132 1         5 goto FAIL_FOOTER;
133             }
134 2         5 my $expect = "\xFF\x2F\x00";
135 2 100       6 if ( $footer ne $expect ) {
136             push @failure,
137 1         4 [ footer => map { sprintf '%vx', $_ } $footer, $expect ];
  2         11  
138             }
139             FAIL_FOOTER:
140 3         11 _failure( context(), 'MIDI footer', @failure );
141             }
142              
143             sub midi_header ($$$$) {
144 5     5 1 257922 my ( $fh, $want_format, $want_tracks, $want_division ) = @_;
145              
146 5         93 my $amount = read $fh, my $header, 14;
147 5 100       345 confess "midi_header read $!" unless defined $amount;
148 4         9 my @failure;
149 4 100       14 if ( $amount != 14 ) {
150 1         4 push @failure, [ byte_count => $amount, 14 ];
151 1         7 goto FAIL_HEADER;
152             }
153 3         18 my ( $mthd, $header_len, $format, $tracks, $division ) =
154             unpack a4Nnnn => $header;
155 3 100       13 if ( $mthd ne 'MThd' ) {
156 1         5 push @failure, [ id => $mthd, 'MThd' ];
157             }
158 3 100       8 if ( $header_len != 6 ) {
159 1         3 push @failure, [ header_length => $header_len, 6 ];
160             }
161 3 100       10 if ( $format != $want_format ) {
162 1         3 push @failure, [ format => $format, $want_format ];
163             }
164 3 100       11 if ( $tracks != $want_tracks ) {
165 1         3 push @failure, [ tracks => $tracks, $want_tracks ];
166             }
167 3 100       10 if ( $division != $want_division ) {
168 1         2 push @failure, [ division => $division, $want_division ];
169             }
170             FAIL_HEADER:
171 4         17 _failure( context(), 'MIDI header', @failure );
172             }
173              
174             sub midi_note_off ($$$$$) {
175 8     8 1 129 push @_, 0x80, 'MIDI note_off';
176 8         26 goto &_dpv;
177             }
178              
179             sub midi_note_on ($$$$$) {
180 7     7 1 1011 push @_, 0x90, 'MIDI note_on';
181 7         28 goto &_dpv;
182             }
183              
184             # TODO probably need to support some more events around about here
185              
186             sub midi_patch ($$$$) {
187 3     3 1 461 my ( $fh, $dtime, $channel, $want_patch ) = @_;
188 3         7 my @failure;
189 3         9 my $q = read_vlq($fh);
190 3 100       10 if ( $q != $dtime ) {
191 1         4 push @failure, [ dtime => $q, $dtime ];
192             }
193 3         8 my $amount = read $fh, my $track, 2;
194 3 50       7 confess "midi_patch read $!" unless defined $amount;
195 3 100       10 if ( $amount != 2 ) {
196 1         3 push @failure, [ length => $amount, 2 ];
197 1         6 goto FAIL_PATCH;
198             }
199 2         7 my ( $pa, $patch ) = unpack CC => $track;
200 2         5 my ( $ch, $code ) = ( $pa & 0xF, $pa & 0xF0 );
201 2 100       7 if ( $ch != $channel ) {
202 1         4 push @failure, [ channel => $ch, $channel ];
203             }
204 2 100       7 if ( $code != 0xC0 ) {
205 1         3 push @failure, [ code => $code, 0xC0 ];
206             }
207 2 100       6 if ( $patch != $want_patch ) {
208 1         3 push @failure, [ patch => $patch, $want_patch ];
209             }
210             FAIL_PATCH:
211 3         10 _failure( context(), 'MIDI patch', @failure );
212             }
213              
214             sub midi_pitch_wheel ($$$$) {
215 3     3 1 4167 my ( $fh, $dtime, $channel, $wheel ) = @_;
216 3         22 my @failure;
217 3         9 my $q = read_vlq($fh);
218 3 100       10 if ( $q != $dtime ) {
219 1         5 push @failure, [ dtime => $q, $dtime ];
220             }
221 3         8 my $amount = read $fh, my $track, 3;
222 3 100       9 if ( $amount != 3 ) {
223 1         4 push @failure, [ length => $amount, 3 ];
224 1         5 goto FAIL_WHEEL;
225             }
226 2         7 my ( $pa, $high, $low ) = unpack CCC => $track;
227 2         5 my ( $ch, $code ) = ( $pa & 0xF, $pa & 0xF0 );
228 2 100       7 if ( $ch != $channel ) {
229 1         4 push @failure, [ channel => $ch, $channel ];
230             }
231 2 100       7 if ( $code != 0xE0 ) {
232 1         3 push @failure, [ code => $code, 0xE0 ];
233             }
234 2         6 my $value = $high | ( $low << 7 ) - 0x2000;
235 2 100       7 if ( $value != $wheel ) {
236 1         3 push @failure, [ wheel => $value, $wheel ];
237             }
238             FAIL_WHEEL:
239 3         9 _failure( context(), 'MIDI pitch_wheel', @failure );
240             }
241              
242             sub midi_skip ($$) {
243 4     4 1 6550 my ( $fh, $size ) = @_;
244 4         29 my $amount = read $fh, my ($unused), $size;
245 4 100       390 confess "midi_skip read $!" unless defined $amount;
246 3         6 my @failure;
247 3 100       9 if ( $amount != $size ) {
248 1         5 push @failure, [ byte_count => $amount, $size ];
249             }
250 3         11 _failure( context(), 'MIDI skip', @failure );
251             }
252              
253             sub midi_skip_dtime ($$) {
254 1     1 1 14608 read_vlq( $_[0] );
255 1         4 goto &midi_skip;
256             }
257              
258             sub midi_tempo ($$$) {
259 4     4 1 4291 my ( $fh, $dtime, $tempo_want ) = @_;
260 4         7 my @failure;
261 4         11 my $q = read_vlq($fh);
262 4 100       14 if ( $q != $dtime ) {
263 1         4 push @failure, [ dtime => $q, $dtime ];
264             }
265 4         10 my $amount = read $fh, my $track, 6;
266 4 50       19 confess "midi_tempo read $!" unless defined $amount;
267 4 100       13 if ( $amount != 6 ) {
268 1         5 push @failure, [ length => $amount, 6 ];
269 1         6 goto FAIL_TEMPO;
270             }
271 3         13 my ( $code, $high, $low ) = unpack Z3Cn => $track;
272 3         7 my $expect = "\xFF\x51\x03";
273 3 100       9 if ( $code ne $expect ) {
274             push @failure,
275 1         4 [ tempo_code => map { sprintf '%vx', $_ } $code, $expect ];
  2         10  
276             }
277 3         8 my $tempo = ( $high << 16 ) | $low;
278 3 100       7 if ( $tempo != $tempo_want ) {
279 1         3 push @failure, [ tempo => $tempo, $tempo_want ];
280             }
281             FAIL_TEMPO:
282 4         14 _failure( context(), 'MIDI tempo', @failure );
283             }
284              
285             sub midi_text ($$$$) {
286 19     19 1 6957 my ( $fh, $dtime, $type, $want_string ) = @_;
287 19         71 my $code;
288 19 100       144 if ( $type eq 'text' ) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
289 4         7 $code = "\xFF\x01";
290             } elsif ( $type eq 'copyright' ) {
291 1         3 $code = "\xFF\x02";
292             } elsif ( $type eq 'name' ) {
293 1         2 $code = "\xFF\x03";
294             } elsif ( $type eq 'instrument' ) {
295 1         2 $code = "\xFF\x04";
296             } elsif ( $type eq 'lyric' ) {
297 1         3 $code = "\xFF\x05";
298             } elsif ( $type eq 'marker' ) {
299 1         2 $code = "\xFF\x06";
300             } elsif ( $type eq 'cue' ) {
301 1         3 $code = "\xFF\x07";
302             } elsif ( $type eq 'text8' ) {
303 1         2 $code = "\xFF\x08";
304             } elsif ( $type eq 'text9' ) {
305 1         2 $code = "\xFF\x09";
306             } elsif ( $type eq 'texta' ) {
307 1         2 $code = "\xFF\x0A";
308             } elsif ( $type eq 'textb' ) {
309 1         3 $code = "\xFF\x0B";
310             } elsif ( $type eq 'textc' ) {
311 1         3 $code = "\xFF\x0C";
312             } elsif ( $type eq 'textd' ) {
313 1         3 $code = "\xFF\x0D";
314             } elsif ( $type eq 'texte' ) {
315 1         2 $code = "\xFF\x0E";
316             } elsif ( $type eq 'textf' ) {
317 1         3 $code = "\xFF\x0F";
318             } else {
319 1         357 confess "unknown type '$type'";
320             }
321 18         33 my @failure;
322 18         40 my $q = read_vlq($fh);
323 18 100       45 if ( $q != $dtime ) {
324 1         4 push @failure, [ dtime => $q, $dtime ];
325             }
326 18         36 my $amount = read $fh, my $track, 2;
327 18 50       38 confess "midi_text read $!" unless defined $amount;
328 18 100       44 if ( $amount != 2 ) {
329 1         3 push @failure, [ code_length => $amount, 2 ];
330 1         6 goto FAIL_TEXT;
331             }
332 17 100       41 if ( $track ne $code ) {
333             push @failure,
334 1         4 [ text_code => map { sprintf '%vx', $_ } $track, $code ];
  2         11  
335             }
336 17         35 my $string_length = read_vlq($fh);
337 17         38 $amount = read $fh, $track, $string_length;
338 17 100       41 if ( $amount != $string_length ) {
339 1         4 push @failure, [ text_length => $amount, $string_length ];
340 1         5 goto FAIL_TEXT;
341             }
342 16 100       37 if ( $track ne $want_string ) {
343 1         5 push @failure, [ text => $track, $want_string ];
344             }
345             FAIL_TEXT:
346 18         57 _failure( context(), "MIDI text_$type", @failure );
347             }
348              
349             sub midi_track ($$) {
350 4     4 1 585 my ( $fh, $want_length ) = @_;
351              
352 4         31 my $amount = read $fh, my $track, 8;
353 4 100       401 confess "midi_track read $!" unless defined $amount;
354 3         7 my @failure;
355 3 100       10 if ( $amount != 8 ) {
356 1         4 push @failure, [ byte_count => $amount, 8 ];
357 1         6 goto FAIL_TRACK;
358             }
359 2         10 my ( $mtrk, $track_len ) = unpack a4N => $track;
360 2 100       8 if ( $mtrk ne 'MTrk' ) {
361 1         5 push @failure, [ id => $mtrk, 'MTrk' ];
362             }
363 2 100       7 if ( $track_len != $want_length ) {
364 1         3 push @failure, [ track_length => $track_len, $want_length ];
365 1         5 goto FAIL_TRACK;
366             }
367             FAIL_TRACK:
368 3         11 _failure( context(), 'MIDI track', @failure );
369             }
370              
371             sub read_vlq ($) {
372 72     72 1 1934 my $q = 0;
373 72         115 while (1) {
374 86         232 my $r = read $_[0], my $byte, 1;
375 86 100       433 confess "read_vlq read $!" unless defined $r;
376 85 100       396 confess "read_vlq eof" if $r == 0;
377 84         208 my $n = unpack C => $byte;
378 84         192 $q = ( $q << 7 ) | ( $n & 0x7f );
379 84 100       204 if ( $n < 0x80 ) {
380 70 100       365 confess "read_vlq range $q" if $q > 0xFFFFFFF;
381 69         158 return $q;
382             }
383             }
384             }
385              
386             1;
387             __END__