File Coverage

blib/lib/MIDI/Segment.pm
Criterion Covered Total %
statement 92 93 98.9
branch 17 20 85.0
condition 15 17 88.2
subroutine 8 8 100.0
pod 2 2 100.0
total 134 140 95.7


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.02';
5 2     2   264063 use 5.10.0;
  2         16  
6 2     2   12 use strict;
  2         6  
  2         42  
7 2     2   14 use warnings;
  2         3  
  2         81  
8              
9             use constant {
10             # index into MIDI::Event events
11 2         2145 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         3  
19              
20             sub _durations {
21 5     5   10 my ($opus) = @_;
22 5         9 my ( $maxdur, $vague ) = ( 0, 0 );
23 5         8 my ( @all_onsets, @links, @track_lens );
24 5         7 for my $track ( @{ $opus->tracks_r } ) {
  5         15  
25 6         40 my @onsets = (0);
26 6         8 my %onset2index;
27 6         10 my $notes = 0;
28 6         9 my $when = 0;
29 6         14 my $evlist = $track->events_r;
30 6         44 for my $tindex ( 0 .. $#$evlist ) {
31 37         50 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     146 if ( $event->[NAME] eq 'note_on'
      100        
36             and $event->[VELO] > MINVELO
37             and $when > $onsets[-1] ) {
38 10         19 push @onsets, $when;
39 10         22 $onset2index{$when} = $tindex;
40 10         33 $notes = 1;
41             }
42 37         56 $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         17 push @track_lens, $when;
49 5 50       12 $maxdur = $when if $when > $maxdur;
50 5         9 my $last = $evlist->[-1];
51 5 100 100     27 $vague = 1
      66        
52             if $last->[DTIME] == 0
53             and $last->[NAME] eq 'note_on'
54             and $last->[VELO] > MINVELO;
55 5         10 shift @onsets;
56 5         9 push @all_onsets, \@onsets;
57 5         15 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         5 my $ragged = 0;
62 4 100       10 if ( @track_lens > 1 ) {
63 1         3 for my $i ( 1 .. $#track_lens ) {
64 1 50       5 if ( $track_lens[0] != $track_lens[$i] ) {
65 1         3 $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   7 my ( $half, $tracks ) = @_;
84 2         2 my %possible;
85 2         5 TRACK: for my $onsets (@$tracks) {
86             # TODO or die here, if this is reachable somehow sane
87             #next unless @$onsets;
88 2         5 my ( $lower, $upper ) = ( 0, $#$onsets );
89 2         3 my $midpoint;
90 2         5 while ( $lower <= $upper ) {
91 3         7 $midpoint = ( $lower + $upper ) >> 1;
92 3 50       9 if ( $half < $onsets->[$midpoint] ) {
    100          
93 0         0 $upper = $midpoint - 1;
94             } elsif ( $half > $onsets->[$midpoint] ) {
95 2         5 $lower = $midpoint + 1;
96             } else {
97 1         3 @possible{ @{$onsets}[ 0 .. $midpoint ] } = ();
  1         4  
98 1         4 next TRACK;
99             }
100             }
101 1         4 @possible{ @{$onsets}[ 0 .. $midpoint - 1 ] } = ();
  1         4  
102             }
103 2         9 return [ sort { $a <=> $b } keys %possible ];
  1         7  
104             }
105              
106             sub new {
107 5     5 1 5103 my ( $class, $opus ) = @_;
108 5         12 my $self = _durations($opus);
109             # TODO maybe user-supplied parameters could auto-correct some of
110             # these cases, e.g. to extend the tracks to some duration?
111             die "problematic MIDI v=$self->{vague} r=$self->{ragged}"
112 4 100 100     42 if $self->{vague} or $self->{ragged};
113             my $potential =
114 2         9 _possible_segments( int( $self->{maximum} / 2 ), $self->{onsets} );
115 2         4 DURATION: for my $dur (@$potential) {
116 3         5 my $window = $dur;
117 3         7 while ( $window < $self->{maximum} ) {
118 6         9 for my $links ( @{ $self->{links} } ) {
  6         12  
119 6 100       14 next DURATION unless exists $links->{$window};
120             }
121 5         12 $window += $dur;
122             }
123 2         2 push @{ $self->{segments} }, $dur;
  2         5  
124             }
125 2         13 return bless( $self, $class ), $self->{segments};
126             }
127              
128             sub split {
129 3     3 1 9732 my ( $self, $dur ) = @_;
130 3         5 my @segtracks;
131 3         5 my $links = $self->{links};
132 3         13 my $tracks = $self->{opus}->tracks_r;
133 3         24 for my $tidx ( 0 .. $#$tracks ) {
134 3         5 my @segments;
135 3         8 my $evlist = $tracks->[$tidx]->events_r;
136 3         18 my $start = 0;
137 3         4 my $window = $dur;
138 3         5 my $sidx = 0;
139 3         8 while ( $window < $self->{maximum} ) {
140 5   100     28 my $end = $links->[$tidx]{$window}
141             // die "no onset at $window track $tidx";
142             # TODO how create this situation for a test?
143             #die "cannot end before start ($start, $end)" if $end <= $start;
144 4         8 $segtracks[ $sidx++ ][$tidx] = [ @{$evlist}[ $start .. $end - 1 ] ];
  4         12  
145 4         8 $window += $dur;
146 4         8 $start = $end;
147             }
148 2         4 $segtracks[$sidx][$tidx] = [ @{$evlist}[ $start .. $#$evlist ] ];
  2         6  
149             }
150 2         9 return \@segtracks;
151             }
152              
153             1;
154             __END__