File Coverage

blib/lib/MIDI/Segment.pm
Criterion Covered Total %
statement 94 95 98.9
branch 16 20 80.0
condition 15 17 88.2
subroutine 8 8 100.0
pod 2 2 100.0
total 135 142 95.0


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.03';
5 2     2   261247 use 5.10.0;
  2         16  
6 2     2   10 use strict;
  2         4  
  2         40  
7 2     2   9 use warnings;
  2         4  
  2         79  
8              
9             use constant {
10             # index into MIDI::Event events
11 2         2107 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   11 };
  2         4  
19              
20             sub _durations {
21 5     5   9 my ($opus) = @_;
22 5         9 my ( $maxdur, $vague ) = ( 0, 0 );
23 5         9 my ( @all_onsets, @links, @track_lens );
24 5         8 for my $track ( @{ $opus->tracks_r } ) {
  5         13  
25 6         40 my @onsets = (0);
26 6         18 my %onset2index;
27 6         9 my $notes = 0;
28 6         7 my $when = 0;
29 6         15 my $evlist = $track->events_r;
30 6         44 for my $tindex ( 0 .. $#$evlist ) {
31 37         47 my $event = $evlist->[$tindex];
32             # TODO may need a "minimum duration" so that too-small
33             # durations are not found, but the user could also ignore
34             # those when calling split
35 37 100 66     114 if ( $event->[NAME] eq 'note_on'
      100        
36             and $event->[VELO] > MINVELO
37             and $when > $onsets[-1] ) {
38 10         16 push @onsets, $when;
39 10         20 $onset2index{$when} = $tindex;
40 10         13 $notes = 1;
41             }
42 37         62 $when += $event->[DTIME];
43             }
44             # more complicated would be to skip these tracks, but that would
45             # require a sparse list of tracks to apply the subsequent
46             # calculations to
47 6 100       22 die "no note_on in track" unless $notes;
48 5         16 push @track_lens, $when;
49 5 50       14 $maxdur = $when if $when > $maxdur;
50 5         11 my $last = $evlist->[-1];
51 5 100 100     29 $vague = 1
      66        
52             if $last->[DTIME] == 0
53             and $last->[NAME] eq 'note_on'
54             and $last->[VELO] > MINVELO;
55 5         7 shift @onsets;
56 5         9 push @all_onsets, \@onsets;
57 5         16 push @links, \%onset2index;
58             }
59             # TODO can this be reached with the "no note_on in track" limitation?
60             #die "no events in MIDI" if $maxdur <= 0;
61 4         7 my $ragged = 0;
62 4 100       9 if ( @track_lens > 1 ) {
63 1         3 for my $i ( 1 .. $#track_lens ) {
64 1 50       4 if ( $track_lens[0] != $track_lens[$i] ) {
65 1         2 $ragged = $i;
66 1         2 last;
67             }
68             }
69             }
70             return {
71 4         28 links => \@links,
72             onsets => \@all_onsets,
73             opus => $opus,
74             maximum => $maxdur,
75             ragged => $ragged, # track lengths differ?
76             segments => [],
77             track_lengths => \@track_lens,
78             vague => $vague, # track ends on 0 dtime note_on?
79             };
80             }
81              
82             sub _possible_segments {
83 2     2   5 my ( $max, $tracks ) = @_;
84 2         5 my $half = int( $max / 2 );
85 2         3 my %possible;
86 2         5 TRACK: for my $onsets (@$tracks) {
87             # TODO or die here, if this is reachable somehow sane
88             #next unless @$onsets;
89 2         5 my ( $lower, $upper ) = ( 0, $#$onsets );
90 2         3 my $midpoint;
91 2         5 while ( $lower <= $upper ) {
92 3         5 $midpoint = ( $lower + $upper ) >> 1;
93 3 50       9 if ( $half < $onsets->[$midpoint] ) {
    100          
94 0         0 $upper = $midpoint - 1;
95             } elsif ( $half > $onsets->[$midpoint] ) {
96 2         5 $lower = $midpoint + 1;
97             } else {
98 1         2 @possible{ @{$onsets}[ 0 .. $midpoint ] } = ();
  1         4  
99 1         4 next TRACK;
100             }
101             }
102 1         5 @possible{ @{$onsets}[ 0 .. $midpoint - 1 ] } = ();
  1         3  
103             }
104             # and only those possible that evenly split the duration
105 2         7 return [ sort { $a <=> $b } grep { $max % $_ == 0 } keys %possible ];
  1         5  
  3         16  
106             }
107              
108             sub new {
109 5     5 1 5031 my ( $class, $opus ) = @_;
110 5         12 my $self = _durations($opus);
111             # TODO maybe user-supplied parameters could auto-correct some of
112             # these cases, e.g. to extend the tracks to some duration?
113             die "problematic MIDI v=$self->{vague} r=$self->{ragged}"
114 4 100 100     39 if $self->{vague} or $self->{ragged};
115             my $potential =
116 2         6 _possible_segments( $self->{maximum}, $self->{onsets} );
117 2         9 DURATION: for my $dur (@$potential) {
118 2         3 my $window = $dur;
119 2         6 while ( $window < $self->{maximum} ) {
120 4         6 for my $links ( @{ $self->{links} } ) {
  4         7  
121 4 50       22 next DURATION unless exists $links->{$window};
122             }
123 4         10 $window += $dur;
124             }
125 2         4 push @{ $self->{segments} }, $dur;
  2         11  
126             }
127 2         21 return bless( $self, $class ), $self->{segments};
128             }
129              
130             sub split {
131 3     3 1 10578 my ( $self, $dur ) = @_;
132 3         4 my @segtracks;
133 3         7 my $links = $self->{links};
134 3         12 my $tracks = $self->{opus}->tracks_r;
135 3         22 for my $tidx ( 0 .. $#$tracks ) {
136 3         7 my @segments;
137 3         8 my $evlist = $tracks->[$tidx]->events_r;
138 3         18 my $start = 0;
139 3         5 my $window = $dur;
140 3         6 my $sidx = 0;
141 3         19 while ( $window < $self->{maximum} ) {
142 5   100     27 my $end = $links->[$tidx]{$window}
143             // die "no onset at $window track $tidx";
144             # TODO how create this situation for a test?
145             #die "cannot end before start ($start, $end)" if $end <= $start;
146 4         9 $segtracks[ $sidx++ ][$tidx] = [ @{$evlist}[ $start .. $end - 1 ] ];
  4         11  
147 4         7 $window += $dur;
148 4         11 $start = $end;
149             }
150 2         4 $segtracks[$sidx][$tidx] = [ @{$evlist}[ $start .. $#$evlist ] ];
  2         7  
151             }
152 2         8 return \@segtracks;
153             }
154              
155             1;
156             __END__