File Coverage

blib/lib/Music/VoiceGen.pm
Criterion Covered Total %
statement 90 90 100.0
branch 25 32 78.1
condition 10 23 43.4
subroutine 13 13 100.0
pod 3 4 75.0
total 141 162 87.0


line stmt bran cond sub pod time code
1             # -*- Perl -*-
2             #
3             # Musical voice generation.
4             #
5             # Run perldoc(1) on this file for additional documentation.
6              
7             package Music::VoiceGen;
8              
9 2     2   106150 use 5.10.0;
  2         5  
10 2     2   10 use strict;
  2         3  
  2         46  
11 2     2   9 use warnings;
  2         6  
  2         61  
12              
13 2     2   8 use Carp qw(croak);
  2         3  
  2         113  
14 2     2   1122 use Math::Random::Discrete;
  2         832  
  2         48  
15 2     2   1149 use Moo;
  2         19531  
  2         10  
16 2     2   2848 use namespace::clean;
  2         15920  
  2         6  
17 2     2   293 use Scalar::Util qw(looks_like_number);
  2         2  
  2         1633  
18              
19             our $VERSION = '0.01';
20              
21             has _choices => ( is => 'rwp', );
22             has _context => (
23             is => 'rwp',
24             clearer => 'clear_context',
25             coerce => sub { ref $_[0] eq 'ARRAY' ? $_[0] : \@_ },
26             default => sub { [] },
27             );
28             has intervals => ( is => 'rwp', );
29             has MAX_CONTEXT => (
30             is => 'rw',
31             coerce => sub {
32             croak "MAX_CONTEXT must be positive integer"
33             if !defined $_[0]
34             or !looks_like_number $_[0]
35             or $_[0] < 1;
36             int $_[0];
37             },
38             default => sub {
39             1;
40             },
41             );
42             has pitches => ( is => 'rwp', );
43             has possibles => ( is => 'rwp', );
44              
45             sub BUILD {
46 7     7 0 83 my ( $self, $param ) = @_;
47              
48 7 100 66     26 if ( exists $param->{pitches} and exists $param->{intervals} ) {
    100          
49             croak "have no pitches to work with"
50             if !defined $param->{pitches}
51             or ref $param->{pitches} ne 'ARRAY'
52 2 50 33     16 or !@{ $param->{pitches} };
  2   33     9  
53             croak "have no intervals to work with"
54             if !defined $param->{intervals}
55             or ref $param->{intervals} ne 'ARRAY'
56 2 50 33     13 or !@{ $param->{intervals} };
  2   33     8  
57              
58 2   100 3   13 my $weightfn = $param->{weightfn} // sub { 1 };
  3         8  
59              
60 2         4 my ( %allowed_pitches, %allowed_intervals );
61 2         3 @allowed_pitches{ map int, @{ $param->{pitches} } } = ();
  2         16  
62 2         5 @allowed_intervals{ map int, @{ $param->{intervals} } } = ();
  2         12  
63              
64 2         10 for my $pitch ( keys %allowed_pitches ) {
65 6         11 for my $interval ( keys %allowed_intervals ) {
66 18         28 my $newpitch = $pitch + $interval;
67 18 100       33 if ( exists $allowed_pitches{$newpitch} ) {
68 7         11 $param->{possibles}{$pitch}{$newpitch} =
69             $weightfn->( $pitch, $newpitch, $interval );
70             }
71             }
72             }
73 2         16 $self->_set_intervals( $param->{intervals} );
74 2         9 $self->_set_pitches( $param->{pitches} );
75              
76             } elsif ( exists $param->{possibles} ) {
77             croak "possibles must be hash reference"
78             if !defined $param->{possibles}
79 4 50 33     21 or ref $param->{possibles} ne 'HASH';
80 4         10 $self->_set_intervals( [] );
81 4         7 $self->_set_pitches( [] );
82             } else {
83 1         21 croak "need 'pitches' and 'intervals' or 'possibles'";
84             }
85              
86 6         30 $self->update( $param->{possibles}, preserve_pitches => 1 );
87             }
88              
89             sub context {
90 34     34 1 4700 my ( $self, $context ) = @_;
91 34 100       105 return $self->_context if !defined $context;
92 13 100       25 $context = [ @_[ 1 .. $#_ ] ] if ref $context ne 'ARRAY';
93 13         207 my $mc = $self->MAX_CONTEXT;
94 13 100       581 if ( @$context > $mc ) {
95 6         18 @$context = @$context[ -$mc .. -1 ];
96             }
97 13         152 $self->_set__context($context);
98 13         78 return $self;
99             }
100              
101             sub rand {
102 12     12 1 28 my ($self) = @_;
103 12         23 my $choices = $self->_choices;
104 12         10 my $choice;
105 12         16 my $context = $self->context;
106 12 100       19 if ( !@$context ) {
107 2         2 my @possibles = keys %{ $self->possibles };
  2         7  
108 2 50       8 croak "no keys in possibles" if !@possibles;
109 2         73 $choice = $possibles[ rand @possibles ];
110             } else {
111 10         28 for my $i ( 0 .. $#$context ) {
112 17         33 my $key = join ".", @$context[ $i .. $#$context ];
113 17 100       28 if ( exists $choices->{$key} ) {
114 10         26 $choice = $choices->{$key}->rand;
115 10 50       92 last if defined $choice;
116             }
117             }
118             }
119              
120             # see "Known Issues" in docs for ideas on how to workaround
121 12 50       21 die "could not find a choice" if !defined $choice;
122              
123 12         15 push @$context, $choice;
124 12         14 $self->context($context);
125              
126 12         18 return $choice;
127             }
128              
129             sub update {
130 8     8 1 4852 my ( $self, $possibles, %param ) = @_;
131              
132 8 50 33     38 croak "possibles must be hash reference"
133             if !defined $possibles
134             or ref $possibles ne 'HASH';
135              
136 8         16 $self->_set_possibles($possibles);
137              
138 8         8 my %choices;
139 8         20 for my $fromval ( keys %$possibles ) {
140 17         210 my ( @choices, @weights );
141 17         12 for my $toval ( keys %{ $possibles->{$fromval} } ) {
  17         34  
142 20         20 push @choices, $toval;
143 20         24 push @weights, $possibles->{$fromval}{$toval};
144             }
145 17         46 $choices{$fromval} = Math::Random::Discrete->new( \@weights, \@choices );
146             }
147 8         170 $self->_set__choices( \%choices );
148              
149 8 100       16 unless ( $param{preserve_pitches} ) {
150 1         4 $self->_set_intervals( [] );
151 1         7 $self->_set_pitches( [] );
152             }
153              
154 8         105 return $self;
155             }
156              
157             1;
158             __END__