File Coverage

blib/lib/MIDI/Stream/Encoder.pm
Criterion Covered Total %
statement 20 20 100.0
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 27 27 100.0


line stmt bran cond sub pod time code
1 12     12   2500095 use v5.26;
  12         44  
2 12     12   78 use warnings;
  12         27  
  12         728  
3 12     12   6453 use Feature::Compat::Class;
  12         5531  
  12         85  
4              
5             # ABSTRACT: MIDI event to bytestream encoder
6              
7             package MIDI::Stream::Encoder;
8 12     12   8965 class MIDI::Stream::Encoder :isa( MIDI::Stream );
  12         48  
  12         803  
9              
10              
11             our $VERSION = '0.005';
12              
13 12     12   80 use Time::HiRes qw/ gettimeofday tv_interval /;
  12         70  
  12         115  
14 12     12   1044 use Carp qw/ carp croak /;
  12         23  
  12         796  
15 12         21616 use MIDI::Stream::Tables qw/
16             has_channel keys_for is_single_byte
17             status_byte split_bytes is_realtime
18             message_length
19 12     12   6492 /;
  12         37  
20              
21              
22             field $enable_14bit_cc :param = 0;
23             field $enable_running_status :param = 0;
24             field $running_status_retransmit :param = 10;
25             field $concat :param = 0;
26              
27             field @msb;
28             field $running_status = 0;
29             field $running_status_count = 0;
30              
31             my $_flatten = method( $event ) {
32             my @keys = ( 'name', keys_for( $event->{ name } )->@* );
33             my @e = $event->@{ @keys };
34             [ $event->@{ @keys } ];
35             };
36              
37             my $_running_status = method( $status ) {
38             return $status unless $enable_running_status;
39             # MIDI 1.0 Detailed Specification v4.2.1 p. 5
40             # Data Types > Status Bytes > Running Status:
41             #
42             # "For Voice and Mode messages only ...
43             # Running Status will be stopped when any other Status byte
44             # intervenes. Real-Time messages should not affect Running Status."
45             #
46             # I interpret this as:
47             # - Running status is only for channel messages
48             # - System messages reset status, but do not set it
49             # - ...apart form realtime status which does not reset or set
50             return $status if is_realtime( $status );
51             if ( ! has_channel( $status ) ) {
52             $self->clear_running_status;
53             return $status;
54             }
55              
56             # Running status found, and haven't reached retransmit threshold
57             return 0 if
58             $status == $running_status &&
59             $running_status_count++ < $running_status_retransmit;
60              
61             # Set and return status
62             $running_status_count = 0;
63             $running_status = $status;
64             };
65              
66              
67             my $encode_one = method( $event ) {
68             my @event = $event->@*;
69              
70             my $event_name = shift @event;
71             my $status = status_byte( $event_name );
72             if ( ! $status ) {
73             carp "Ignoring unknown status : $event_name";
74             return;
75             }
76              
77             if ( $event_name eq 'pitch_bend' ) {
78             splice @event, 1, 1, split_bytes( $event[1] + 8192 );
79             }
80              
81             if ( $event_name eq 'song_position' ) {
82             splice @event, 0, 1, split_bytes( $event[0] );
83             }
84              
85             # 'Note off' events with velocity should retain their status,
86             # and set running-status accordingly.
87             # 'Note on' with velocity 0 is treated as 'Note off'.
88             # Strings of 'Note on' events can take better advantage of
89             # running-status.
90             $status |= 0x10 if
91             $enable_running_status &&
92             $status == 0x80 &&
93             !$event[ 2 ] &&
94             $status != ( $running_status & 0xf0 );
95              
96             $status |= shift @event & 0xf if has_channel( $status );
97              
98             $status = $self->$_running_status( $status );
99             join '', map { chr } $status
100             ? ( $status, @event )
101             : @event
102             };
103              
104             method encode( $event ) {
105             $event = $self->$_flatten( $event )
106             if ref $event eq 'HASH';
107             $event = $event->as_arrayref
108             if eval{ $event->isa('MIDI::Stream::Event') };
109             my @event = $event->@*;
110             my @events;
111              
112             if ( $event[0] eq 'sysex' ) {
113             if ( ref $event[1] eq 'ARRAY' ) {
114             @event = ( $event[0], $event[1]->@* );
115             push @event, 0xf7 unless $event[-1] == 0xf7;
116             }
117             else {
118             my $msg = chr( 0xf0 ) . $event[1];
119             $msg .= substr( $event[1], -1 ) ne chr( 0xf7 )
120             ? chr( 0xf7 )
121             : '';
122             return $msg;
123             }
124             }
125              
126             if ( $enable_14bit_cc && $event[0] eq 'control_change' && $event[2] < 0x20 ) {
127             my ( $lsb, $msb ) = split_bytes( $event [3] );
128             # Comparing new MSB against last-sent MSB for this CC
129             if ( ( $msb[ $event[2] ] // -1 ) == $msb ) {
130             # MSB already sent, just send LSB on CC + 32
131             $event[2] |= 0x20;
132             $event[3] = $lsb;
133             }
134             else {
135             # Re-send MSB, concatenate LSB running status
136             $msb[ $event[2] ] = $msb;
137             $event[3] = $msb;
138             if ( $concat ) {
139             push @event, $event[2] | 0x20, $lsb;
140             }
141             else {
142             push @events, ( [ @event ], [ @event[ 0, 1 ], $event[2] | 0x20, $lsb ] );
143             }
144             }
145             }
146             elsif ( ! $concat && has_channel( $event[0] ) ) {
147             my $length = message_length( $event[0] );
148              
149             if ( @event > $length + 1 ) {
150             my @status = @event[ 0, 1 ];
151             my $i = 2;
152             my $l = $length - 1;
153             while ( $event[ $i ] ) {
154             push @events, [ @status, @event[ $i, $i + $l - 1 ] ];
155             $i += $l;
156             }
157             }
158             }
159              
160             @events
161             ? map { $self->$encode_one( $_ ) } @events
162             : $self->$encode_one( \@event );
163              
164             }
165              
166              
167             method encode_events( @events ) {
168             $concat
169             ? join '', map { $self->encode( $_ ) } @events
170             : map { $self->encode( $_ ) } @events;
171             }
172              
173              
174             method clear_running_status {
175             $running_status_count = 0;
176             $running_status = 0;
177             }
178              
179              
180             1;
181              
182             __END__