File Coverage

File:blib/lib/Music/NWC2MusicXML/Staff.pm
Coverage:97.2%

linestmtbrancondsubtimecode
1package Music::NWC2MusicXML::Staff;
2
3
15
15
15
12
8
59
use strict;
4
15
15
15
10
6
163
use warnings;
5
6our $VERSION = '0.001.1';
7
8
15
15
15
14
15
123
use Carp qw(croak carp);
9
15
15
15
8
7
109
use Readonly;
10
15
15
15
8
9
118
use Scalar::Util qw(blessed);
11
15
15
15
13
4
109
use Params::Validate::Strict qw(validate_strict);
12
15
15
15
13
8
80
use Params::Get;
13
15
15
15
986
13
3275
use Music::NWC2MusicXML::Event;
14
15Readonly::Hash my %MESSAGES => (
16        error_bad_event  => 'add_event: argument must be a Music::NWC2MusicXML::Event, got: %s',
17        error_internal   => 'Internal error: %s',
18);
19
20# Event types that constitute a sounding (or structurally significant) event
21# for the purpose of has_notes().  Hash lookup is O(1); avoids regex in a loop.
22Readonly::Hash my %SOUNDING_EVENT_TYPES => map { $_ => 1 } qw(Note Rest Chord Bar);
23
24 - 55
=head1 NAME

Music::NWC2MusicXML::Staff - Internal representation of a single NWC staff.

=head1 VERSION

0.001.1

=head1 SYNOPSIS

    use Music::NWC2MusicXML::Staff;

    my $staff = Music::NWC2MusicXML::Staff->new(
        name       => 'Violin I',
        group      => 'Standard',
        instrument => { name => 'String Ensemble 1', patch => 48 },
    );

    $staff->add_event($note_event);
    my $events = $staff->events;

=head1 DESCRIPTION

C<Music::NWC2MusicXML::Staff> holds all information about one NWC staff: its
properties (name, visibility, number of lines), instrument data, and the
ordered sequence of C<Music::NWC2MusicXML::Event> objects that constitute its
musical content.

The event list is in parse order.  MusicXML generation iterates over it
to produce C<< <measure> >> elements.

=cut
56
57sub new {
58
550
61696
        my ($class, %input) = @_;
59
550
3309
        my $args = validate_strict(
60                schema => {
61                        name          => { type => 'scalar',  optional => 1, default  => 'Staff' },
62                        group         => { type => 'scalar',  optional => 1, default  => 'Standard' },
63                        lines         => { type => 'scalar',  optional => 1, default  => 5 },
64                        visible       => { type => 'scalar',  optional => 1, default  => 1 },
65                        ending_bar    => { type => 'scalar',  optional => 1 },
66                        instrument    => { type => 'hashref', optional => 1, default  => {} },
67                        initial_clef  => { type => 'scalar',  optional => 1 },
68                        initial_key   => { type => 'hashref', optional => 1 },
69                        initial_timesig => { type => 'hashref', optional => 1 },
70                },
71                input => \%input,
72        );
73
550
61988
        croak $@ unless defined $args;
74
75        my $self = bless {
76                _name            => $args->{name},
77                _group           => $args->{group},
78                _lines           => $args->{lines},
79                _visible         => $args->{visible},
80                _ending_bar      => $args->{ending_bar},
81                _instrument      => $args->{instrument},
82                _initial_clef    => $args->{initial_clef},
83                _initial_key     => $args->{initial_key},
84                _initial_timesig => $args->{initial_timesig},
85
550
1419
                _events          => [],
86        }, $class;
87
88
550
565
        return $self;
89}
90
91# ---------------------------------------------------------------------------
92# Accessors
93# ---------------------------------------------------------------------------
94
95 - 99
=head2 name

Return the staff name string.

=cut
100
101
417
668
sub name            { return $_[0]->{_name} }
102
103 - 107
=head2 group

Return the group name string.

=cut
108
109
5
6
sub group           { return $_[0]->{_group} }
110
111 - 115
=head2 lines

Return the number of staff lines (usually 5).

=cut
116
117
6
115
sub lines           { return $_[0]->{_lines} }
118
119 - 123
=head2 visible

Return 1 if the staff is visible, 0 otherwise.

=cut
124
125
6
10
sub visible         { return $_[0]->{_visible} }
126
127 - 133
=head2 instrument

Return the instrument information hashref.

Keys: C<name> (string), C<patch> (MIDI patch number 0-127).

=cut
134
135
902
805
sub instrument      { return $_[0]->{_instrument} }
136
137 - 141
=head2 initial_clef

Return the initial clef string (e.g. C<Treble>), or undef if none recorded.

=cut
142
143
775
1238
sub initial_clef    { return $_[0]->{_initial_clef} }
144
145 - 149
=head2 set_initial_clef

Set the initial clef.

=cut
150
151sub set_initial_clef {
152
264
275
        my ($self, $clef) = @_;
153
264
171
        $self->{_initial_clef} = $clef;
154
264
254
        return $self;
155}
156
157 - 161
=head2 initial_key

Return the initial key hashref (C<signature>, C<tonic>, C<mode>), or undef.

=cut
162
163
774
936
sub initial_key     { return $_[0]->{_initial_key} }
164
165 - 169
=head2 set_initial_key

Set the initial key.

=cut
170
171sub set_initial_key {
172
230
229
        my ($self, $key) = @_;
173
230
124
        $self->{_initial_key} = $key;
174
230
237
        return $self;
175}
176
177 - 181
=head2 initial_timesig

Return the initial time-signature hashref (C<beats>, C<beat_type>), or undef.

=cut
182
183
392
330
sub initial_timesig { return $_[0]->{_initial_timesig} }
184
185 - 189
=head2 set_initial_timesig

Set the initial time signature.

=cut
190
191sub set_initial_timesig {
192
229
197
        my ($self, $ts) = @_;
193
229
102
        $self->{_initial_timesig} = $ts;
194
229
208
        return $self;
195}
196
197# ---------------------------------------------------------------------------
198# Event management
199# ---------------------------------------------------------------------------
200
201 - 258
=head2 add_event

Append a C<Music::NWC2MusicXML::Event> to this staff's event list.

=head3 Purpose

Used by the parser to build the ordered event sequence as it processes NWCTXT
records belonging to this staff.

=head3 Arguments

=over 4

=item C<$event> -- a blessed C<Music::NWC2MusicXML::Event> object (required).

=back

=head3 Returns

C<$self> (for chaining).

=head3 Side Effects

Appends to C<_events> array.

=head3 Usage Example

    $staff->add_event(
        Music::NWC2MusicXML::Event->new(type => 'Note', ...)
    );

=head3 API SPECIFICATION

=head4 Input

    $event : Music::NWC2MusicXML::Event (required)

=head4 Output

    $self (Music::NWC2MusicXML::Staff)

=head3 MESSAGES

| Code           | Meaning                                | Resolution                      |
|----------------|----------------------------------------|---------------------------------|
| error_bad_event| Argument is not a Music::NWC2MusicXML::Event  | Construct event before adding   |

=head3 FORMAL SPECIFICATION

 [AddEvent]
   DeltaStaff
   event? : Event
   ---------
   events' = events ^ <event?>

 (placeholder)

=cut
259
260sub add_event {
261
1216248
380020
        my ($self, $event) = @_;
262
1216248
987000
        croak _fmt_msg('error_bad_event', ref($event) // 'SCALAR')
263                unless blessed($event) && $event->isa('Music::NWC2MusicXML::Event');
264
1216235
1216235
351325
405597
        push @{ $self->{_events} }, $event;
265
1216235
1039190
        return $self;
266}
267
268 - 286
=head2 events

Return an arrayref of all C<Music::NWC2MusicXML::Event> objects in parse order.

=head3 Returns

Arrayref of C<Music::NWC2MusicXML::Event>.

=head3 API SPECIFICATION

=head4 Input

    (none)

=head4 Output

    ARRAYREF of Music::NWC2MusicXML::Event

=cut
287
288sub events {
289
1947
980
        my ($self) = @_;
290
1947
1878
        return $self->{_events};
291}
292
293 - 298
=head2 musical_events

Return an arrayref containing only the events where C<is_musical_event> is
true.  Metadata records are excluded.

=cut
299
300sub musical_events {
301
5
138
        my ($self) = @_;
302
5
10
5
3
18
3
        return [ grep { $_->is_musical_event } @{ $self->{_events} } ];
303}
304
305 - 309
=head2 event_count

Return the total number of events (musical + metadata).

=cut
310
311sub event_count {
312
28
924
        my ($self) = @_;
313
28
28
16
31
        return scalar @{ $self->{_events} };
314}
315
316 - 322
=head2 has_notes

Return true if any note, rest, chord, or bar event has been added to this
staff.  Used by the parser to distinguish staff-header records (before any
sounding content) from mid-staff change records.

=cut
323
324sub has_notes {
325
3780
1756
        my ($self) = @_;
326
3780
3780
1146
1842
        for my $ev (@{ $self->{_events} }) {
327
4828
4764
                return 1 if $SOUNDING_EVENT_TYPES{ $ev->type };
328        }
329
550
453
        return 0;
330}
331
332# ---------------------------------------------------------------------------
333# Private
334# ---------------------------------------------------------------------------
335
336sub _fmt_msg {
337
13
10
        my ($key, @args) = @_;
338
13
20
        croak "Unknown message key: $key" unless exists $MESSAGES{$key};
339
13
39
        return sprintf $MESSAGES{$key}, @args;
340}
341
3421;
343