File Coverage

blib/lib/MIDI/Stream/Decoder.pm
Criterion Covered Total %
statement 29 29 100.0
branch n/a
condition n/a
subroutine 10 10 100.0
pod n/a
total 39 39 100.0


line stmt bran cond sub pod time code
1 13     13   211869 use v5.26;
  13         53  
2 13     13   77 use warnings;
  13         25  
  13         742  
3 13     13   482 use Feature::Compat::Class;
  13         530  
  13         108  
4              
5             # ABSTRACT: MIDI bytestream decoder
6              
7             package MIDI::Stream::Decoder;
8 1     1   650 class MIDI::Stream::Decoder :isa( MIDI::Stream );
  1         3  
  1         43  
9              
10              
11             our $VERSION = '0.005';
12              
13 13     13   2785 use Time::HiRes qw/ gettimeofday tv_interval /;
  13         37  
  13         120  
14 13     13   1268 use Carp qw/ carp croak /;
  13         22  
  13         1001  
15 13     13   470 use MIDI::Stream::Tables qw/ is_cc is_realtime message_length combine_bytes /;
  13         74  
  13         992  
16 13     13   6259 use MIDI::Stream::FIFO;
  13         42  
  13         481  
17 13     13   6442 use MIDI::Stream::EventFactory;
  13         52  
  13         10420  
18              
19              
20             field $retain_events :param = 1;
21              
22             field $enable_14bit_cc :param = 0;
23             field @cc;
24              
25             field $callback :param = sub { @_ };
26             field $filter_cb = {};
27              
28             field $clock_ppqn :param = 24;
29             field $clock_samples :param = 24;
30             field $clock_fifo = MIDI::Stream::FIFO->new( length => $clock_samples );
31             field $round_tempo :param = 0;
32              
33             field @events;
34             field @pending_event;
35             field $message_length;
36              
37             my $_expand_cc = method( $event ) {
38             return $event unless is_cc( $event->[ 0 ] );
39             return $event unless $enable_14bit_cc;
40             return $event if $event->[ 1 ] > 0x3f;
41             return $event if $event->[ 2 ] > 0x7f;
42              
43             if ( $event->[ 1 ] & 0x20 ) {
44             my $msb = $cc[ $event->[ 1 ] & ~0x20 ];
45             return unless defined $msb;
46             $event->[ 2 ] = combine_bytes( $event->[ 2 ], $msb );
47             $event->[ 1 ] &= ~0x20;
48             return $event;
49             }
50              
51             $cc[ $event->[ 1 ] ] = $event->[ 2 ];
52             return;
53             };
54              
55             my $_sample_clock = method() {
56             state $t = [ gettimeofday ];
57             $clock_fifo->add( tv_interval( $t ) );
58             $t = [ gettimeofday ];
59             };
60              
61             my $_push_event = method( $event = undef ) {
62             state $t = [ gettimeofday ];
63             # Do not use a reference to @pending_event!
64             # Contents will have changed by the time you get round to using it.
65             $event //= [ @pending_event ];
66             $event = $self->$_expand_cc( $event );
67             return unless $event;
68             my $dt = tv_interval( $t );
69             $t = [ gettimeofday ];
70              
71             my $stream_event = MIDI::Stream::EventFactory->event( $dt, $event );
72              
73             if ( !$stream_event ) {
74             carp( "Ignoring unknown status $event->[0]" );
75             return;
76             }
77              
78             push @events, $stream_event if $retain_events;
79              
80             my @callbacks = ( $filter_cb->{ all } // [] )->@*;
81             push @callbacks, ( $filter_cb->{ $stream_event->name } // [] )->@*;
82              
83             for my $cb ( @callbacks ) {
84 13     13   106 no warnings 'uninitialized';
  13         42  
  13         17989  
85             last if $cb->( $stream_event ) eq $self->stop;
86             }
87              
88             $callback->( $stream_event );
89             };
90              
91             my $_reset_pending_event = method( $status = undef ) {
92             @pending_event = ();
93             push @pending_event, $status if defined $status;
94             $message_length = message_length( $status );
95             };
96              
97              
98             method decode( @bytestrings ) {
99             my $bytestring = join '', @bytestrings;
100             my @bytes = unpack 'C*', $bytestring;
101             my $status;
102              
103             BYTE:
104             while ( @bytes ) {
105              
106             # Status byte - start/end of message
107             if ( $bytes[0] & 0x80 ) {
108             my $status = shift @bytes;
109              
110             # Sample the clock to determine tempo ASAP
111             $status == 0xf8 && $self->$_sample_clock();
112              
113             # End-of-Xclusive
114             if ( $status == 0xf7 ) {
115             carp( "EOX received for non-SysEx message - ignoring!") && next BYTE
116             unless $pending_event[0] == 0xf0;
117             $self->$_push_event();
118             $self->$_reset_pending_event();
119             next BYTE;
120             }
121              
122             # Real-Time messages can appear within other messages.
123             if ( is_realtime( $status ) ) {
124             # Push unless we have an undefined realtime status
125             $self->$_push_event( [ $status ] ) unless $status == 0xf9 || $status == 0xfd;
126             next BYTE;
127             }
128              
129             # Any non-Real-Time status byte ends a SysEx
130             # Push the pending sysex and proceed ...
131             if ( @pending_event && $pending_event[0] == 0xf0 ) {
132             $self->$_push_event();
133             }
134              
135             # Undefined system statuses which should reset running status -
136             # a full message needs to be received after this
137             if ( $status == 0xf4 || $status == 0xf5 ) {
138             @pending_event = ();
139             next BYTE;
140             }
141              
142             # Should now be able to push any single-byte statuses,
143             # e.g. Tune request
144             if ( message_length( $status ) == 1 ) {
145             @pending_event = ();
146             $self->$_push_event( [ $status ] );
147             next BYTE;
148             }
149              
150             $self->$_reset_pending_event( $status );
151             next BYTE;
152             } # end if status byte
153              
154             my $byte = shift @bytes;
155             next BYTE unless @pending_event;
156              
157             push @pending_event, $byte;
158             my $remaining = $message_length - @pending_event;
159              
160             # A complete message denoted by length, not upcoming status bytes
161             if ( $message_length && $remaining <= 0 ) {
162             $self->$_push_event();
163             $self->$_reset_pending_event( $pending_event[0] );
164             }
165             } # end while
166              
167             scalar @events;
168             }
169              
170              
171             method attach_callback( $event, $callback ) {
172             if ( ref $event eq 'ARRAY' ) {
173             $self->attach_callback( $_, $callback ) for $event->@*;
174             return;
175             }
176             push $filter_cb->{ $event }->@*, $callback;
177             }
178              
179              
180             method cancel_event_callback( $event ) {
181             if ( ref $event eq 'ARRAY' ) {
182             $self->cancel_event_callbacks( $_ ) for $event->@*;
183             return;
184             }
185             delete $filter_cb->{ $event };
186             }
187              
188              
189             method cancel_callback {
190             undef $callback;
191             }
192              
193              
194             method events {
195             splice @events;
196             }
197              
198              
199             method fetch_one_event {
200             shift @events;
201             }
202              
203              
204             method tempo {
205             my $tempo = 60 / ( $clock_fifo->average * $clock_ppqn );
206             $round_tempo ? sprintf( '%.0f', $tempo ) : $tempo;
207             }
208              
209             method continue { MIDI::Stream::Tables::continue() }
210             method stop { MIDI::Stream::Tables::stop() }
211              
212             1;
213              
214             __END__