File Coverage

blib/lib/MIDI/Segment.pm
Criterion Covered Total %
statement 90 91 98.9
branch 18 20 90.0
condition 15 17 88.2
subroutine 8 8 100.0
pod 2 2 100.0
total 133 138 96.3


line stmt bran cond sub pod time code
1             # -*- Perl -*-
2             # a means to segment MIDI by equal duration
3             package MIDI::Segment;
4             our $VERSION = '0.01';
5 2     2   256458 use 5.10.0;
  2         17  
6 2     2   23 use strict;
  2         3  
  2         40  
7 2     2   12 use warnings;
  2         4  
  2         89  
8              
9             use constant {
10             # index into MIDI::Event events
11 2         2153 NAME => 0, # note_on, etc
12             DTIME => 1, # delta time
13             VELO => 4, # velocity (volume or loudness)
14              
15             # velocity less or equal to this will not be considered a notable
16             # "note_on". this may need to be a user-supplied parameter
17             MINVELO => 0,
18 2     2   12 };
  2         8  
19              
20             sub _durations {
21 5     5   6 my ($opus) = @_;
22 5         11 my ( $maxdur, $vague ) = ( 0, 0 );
23 5         7 my ( @all_onsets, @links, @track_lens );
24 5         9 for my $track ( @{ $opus->tracks_r } ) {
  5         12  
25 6         40 my @onsets = (0);
26 6         8 my %onset2index;
27 6         9 my $when = 0;
28 6         15 my $evlist = $track->events_r;
29 6         42 for my $tindex ( 0 .. $#$evlist ) {
30 33         45 my $event = $evlist->[$tindex];
31             # TODO may need a "minimum duration" so that too-small
32             # durations are not found, but the user could also ignore
33             # those when calling split
34 33 100 66     104 if ( $event->[NAME] eq 'note_on'
      100        
35             and $event->[VELO] > MINVELO
36             and $when > $onsets[-1] ) {
37 10         17 push @onsets, $when;
38 10         20 $onset2index{$when} = $tindex;
39             }
40 33         49 $when += $event->[DTIME];
41             }
42 6         10 push @track_lens, $when;
43 6 100       14 $maxdur = $when if $when > $maxdur;
44 6         10 my $last = $evlist->[-1];
45 6 100 100     35 $vague = 1
      66        
46             if $last->[DTIME] == 0
47             and $last->[NAME] eq 'note_on'
48             and $last->[VELO] > MINVELO;
49 6         9 shift @onsets;
50 6         10 push @all_onsets, \@onsets;
51 6         17 push @links, \%onset2index;
52             }
53 5 100       22 die "no events in MIDI" if $maxdur <= 0;
54 4         8 my $ragged = 0;
55 4 100       8 if ( @track_lens > 1 ) {
56 1         3 for my $i ( 1 .. $#track_lens ) {
57 1 50       4 if ( $track_lens[0] != $track_lens[$i] ) {
58 1         3 $ragged = $i;
59 1         2 last;
60             }
61             }
62             }
63             return {
64 4         28 links => \@links,
65             onsets => \@all_onsets,
66             opus => $opus,
67             maximum => $maxdur,
68             ragged => $ragged, # track lengths differ?
69             segments => [],
70             track_lengths => \@track_lens,
71             vague => $vague, # track ends on 0 dtime note_on?
72             };
73             }
74              
75             sub _possible_segments {
76 2     2   5 my ( $half, $tracks ) = @_;
77 2         3 my %possible;
78 2         4 TRACK: for my $onsets (@$tracks) {
79 2         5 my ( $lower, $upper ) = ( 0, $#$onsets );
80 2         3 my $midpoint;
81 2         5 while ( $lower <= $upper ) {
82 3         5 $midpoint = ( $lower + $upper ) >> 1;
83 3 50       11 if ( $half < $onsets->[$midpoint] ) {
    100          
84 0         0 $upper = $midpoint - 1;
85             } elsif ( $half > $onsets->[$midpoint] ) {
86 2         5 $lower = $midpoint + 1;
87             } else {
88 1         4 @possible{ @{$onsets}[ 0 .. $midpoint ] } = ();
  1         4  
89 1         3 next TRACK;
90             }
91             }
92 1         3 @possible{ @{$onsets}[ 0 .. $midpoint - 1 ] } = ();
  1         4  
93             }
94 2         11 return [ sort { $a <=> $b } keys %possible ];
  1         8  
95             }
96              
97             sub new {
98 5     5 1 4769 my ( $class, $opus ) = @_;
99 5         16 my $self = _durations($opus);
100             # TODO maybe user-supplied parameters could auto-correct some of
101             # these cases, e.g. to extend the tracks to some duration?
102             die "problematic MIDI v=$self->{vague} r=$self->{ragged}"
103 4 100 100     38 if $self->{vague} or $self->{ragged};
104             my $potential =
105 2         9 _possible_segments( int( $self->{maximum} / 2 ), $self->{onsets} );
106 2         6 DURATION: for my $dur (@$potential) {
107 3         5 my $window = $dur;
108 3         9 while ( $window < $self->{maximum} ) {
109 6         9 for my $links ( @{ $self->{links} } ) {
  6         11  
110 6 100       16 next DURATION unless exists $links->{$window};
111             }
112 5         19 $window += $dur;
113             }
114 2         6 push @{ $self->{segments} }, $dur;
  2         5  
115             }
116 2         12 return bless( $self, $class ), $self->{segments};
117             }
118              
119             sub split {
120 3     3 1 9726 my ( $self, $dur ) = @_;
121 3         5 my @segtracks;
122 3         7 my $links = $self->{links};
123 3         11 my $tracks = $self->{opus}->tracks_r;
124 3         25 for my $tidx ( 0 .. $#$tracks ) {
125 3         5 my @segments;
126 3         9 my $evlist = $tracks->[$tidx]->events_r;
127 3         17 my $start = 0;
128 3         5 my $window = $dur;
129 3         5 my $sidx = 0;
130 3         8 while ( $window < $self->{maximum} ) {
131 5   100     25 my $end = $links->[$tidx]{$window} // die "no onset at $window track $tidx";
132             # TODO how create this situation for a test?
133             #die "cannot end before start ($start, $end)" if $end <= $start;
134 4         9 $segtracks[ $sidx++ ][$tidx] = [ @{$evlist}[ $start .. $end - 1 ] ];
  4         11  
135 4         7 $window += $dur;
136 4         9 $start = $end;
137             }
138 2         4 $segtracks[$sidx][$tidx] = [ @{$evlist}[ $start .. $#$evlist ] ];
  2         7  
139             }
140 2         7 return \@segtracks;
141             }
142              
143             1;
144             __END__