File Coverage

blib/lib/Music/VoiceGen.pm
Criterion Covered Total %
statement 102 102 100.0
branch 28 36 77.7
condition 10 23 43.4
subroutine 15 15 100.0
pod 4 5 80.0
total 159 181 87.8


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   106934 use 5.10.0;
  2         5  
10 2     2   6 use strict;
  2         2  
  2         29  
11 2     2   6 use warnings;
  2         6  
  2         50  
12              
13 2     2   5 use Carp qw(croak);
  2         2  
  2         81  
14 2     2   6 use List::Util qw(min);
  2         2  
  2         128  
15 2     2   831 use Math::Random::Discrete;
  2         677  
  2         43  
16 2     2   861 use Moo;
  2         18742  
  2         8  
17 2     2   2949 use namespace::clean;
  2         16119  
  2         7  
18 2     2   347 use Scalar::Util qw(looks_like_number);
  2         3  
  2         1981  
19              
20             our $VERSION = '0.02';
21              
22             has _choices => ( is => 'rwp', );
23             has _context => (
24             is => 'rwp',
25             clearer => 'clear_context',
26             coerce => sub { ref $_[0] eq 'ARRAY' ? $_[0] : \@_ },
27             default => sub { [] },
28             );
29             has contextfn => (
30             is => 'rw',
31             isa => sub {
32             die "context function must be a code ref"
33             unless defined $_[0] and ref $_[0] eq 'CODE';
34             },
35             default => sub {
36             sub { $_[1]->rand, 1 }
37             },
38             );
39             has intervals => ( is => 'rwp', );
40             has MAX_CONTEXT => (
41             is => 'rw',
42             coerce => sub {
43             croak "MAX_CONTEXT must be positive integer"
44             if !defined $_[0]
45             or !looks_like_number $_[0]
46             or $_[0] < 1;
47             int $_[0];
48             },
49             default => sub {
50             1;
51             },
52             );
53             has pitches => ( is => 'rwp', );
54             # NOTE use the ->update method to set these after ->new
55             has possibles => ( is => 'rwp', );
56             has startfn => (
57             is => 'rw',
58             isa => sub {
59             die "start function must be a code ref"
60             unless defined $_[0] and ref $_[0] eq 'CODE';
61             },
62             default => sub {
63             sub {
64             $_[0]->[ CORE::rand @{ $_[0] } ];
65             };
66             },
67             );
68              
69             sub BUILD {
70 9     9 0 62 my ( $self, $param ) = @_;
71              
72 9 100 66     32 if ( exists $param->{pitches} and exists $param->{intervals} ) {
    100          
73             croak "have no pitches to work with"
74             if !defined $param->{pitches}
75             or ref $param->{pitches} ne 'ARRAY'
76 3 50 33     17 or !@{ $param->{pitches} };
  3   33     8  
77             croak "have no intervals to work with"
78             if !defined $param->{intervals}
79             or ref $param->{intervals} ne 'ARRAY'
80 3 50 33     13 or !@{ $param->{intervals} };
  3   33     8  
81              
82 3   100 102   10 my $weightfn = $param->{weightfn} // sub { 1 };
  102         166  
83              
84 3         4 my ( %allowed_pitches, %allowed_intervals );
85 3         2 @allowed_pitches{ map int, @{ $param->{pitches} } } = ();
  3         81  
86 3         8 @allowed_intervals{ map int, @{ $param->{intervals} } } = ();
  3         26  
87              
88 3         14 for my $pitch ( keys %allowed_pitches ) {
89 106         92 for my $interval ( keys %allowed_intervals ) {
90 118         100 my $newpitch = $pitch + $interval;
91 118 100       143 if ( exists $allowed_pitches{$newpitch} ) {
92 106         89 $param->{possibles}{$pitch}{$newpitch} =
93             $weightfn->( $pitch, $newpitch, $interval );
94             }
95             }
96             }
97 3         12 $self->_set_intervals( $param->{intervals} );
98 3         13 $self->_set_pitches( $param->{pitches} );
99              
100             } elsif ( exists $param->{possibles} ) {
101             croak "possibles must be hash reference"
102             if !defined $param->{possibles}
103 5 50 33     22 or ref $param->{possibles} ne 'HASH';
104 5         9 $self->_set_intervals( [] );
105 5         9 $self->_set_pitches( [] );
106             } else {
107 1         16 croak "need 'pitches' and 'intervals' or 'possibles'";
108             }
109              
110 8         19 $self->update( $param->{possibles}, preserve_pitches => 1 );
111             }
112              
113             sub context {
114 44     44 1 3869 my ( $self, $context ) = @_;
115 44 100       121 return $self->_context if !defined $context;
116 18 100       35 $context = [ @_[ 1 .. $#_ ] ] if ref $context ne 'ARRAY';
117 18         219 my $mc = $self->MAX_CONTEXT;
118 18 100       451 if ( @$context > $mc ) {
119 6         17 @$context = @$context[ -$mc .. -1 ];
120             }
121 18         215 $self->_set__context($context);
122 18         102 return $self;
123             }
124              
125             sub rand {
126 17     17 1 84 my ($self) = @_;
127 17         25 my $choices = $self->_choices;
128 17         14 my $choice;
129 17         21 my $context = $self->context;
130 17 100       27 if ( !@$context ) {
131 4         3 my @possibles = keys %{ $self->possibles };
  4         42  
132 4 50       12 croak "no keys in possibles" if !@possibles;
133 4         50 $choice = $self->startfn->( \@possibles );
134             } else {
135 13         12 my $count = 1;
136 13         29 for my $i ( 0 .. $#$context ) {
137 23         42 my $key = join ".", @$context[ $i .. $#$context ];
138 23 100       40 if ( exists $choices->{$key} ) {
139             ( $choice, my $abort ) =
140 16         246 $self->contextfn->( $choice, $choices->{$key}, $count );
141 16 100       137 last if $abort;
142 6         7 $count++;
143             }
144             }
145             }
146              
147             # see "Known Issues" in docs for ideas on how to workaround
148 16 50       38 croak "could not find a choice" if !defined $choice;
149              
150 16         17 push @$context, $choice;
151 16         19 $self->context($context);
152              
153 16         33 return $choice;
154             }
155              
156             sub subsets {
157 1     1 1 11 my ( $self, $min, $max, $fn, $list ) = @_;
158 1 50       4 croak "subsets needs min,max,coderef,list" if @_ < 5;
159 1 50       3 $list = [ @_[ 4 .. $#_ ] ] if ref $list ne 'ARRAY';
160 1         3 for my $lo ( 0 .. @$list - $min ) {
161 4         18 for my $hi ( $lo + $min - 1 .. min( $lo + $max - 1, $#$list ) ) {
162 9         19 $fn->( @$list[ $lo .. $hi ] );
163             }
164             }
165 1         4 return $self;
166             }
167              
168             sub update {
169 11     11 1 3873 my ( $self, $possibles, %param ) = @_;
170              
171 11 50 33     40 croak "possibles must be hash reference"
172             if !defined $possibles
173             or ref $possibles ne 'HASH';
174              
175 11         33 $self->_set_possibles($possibles);
176              
177 11         7 my %choices;
178 11         30 for my $fromval ( keys %$possibles ) {
179 123         1964 my ( @choices, @weights );
180 123         64 for my $toval ( keys %{ $possibles->{$fromval} } ) {
  123         200  
181 126         109 push @choices, $toval;
182 126         120 push @weights, $possibles->{$fromval}{$toval};
183             }
184 123         201 $choices{$fromval} = Math::Random::Discrete->new( \@weights, \@choices );
185             }
186 11         258 $self->_set__choices( \%choices );
187              
188 11 100       23 unless ( $param{preserve_pitches} ) {
189 2         6 $self->_set_intervals( [] );
190 2         5 $self->_set_pitches( [] );
191             }
192              
193 11         141 return $self;
194             }
195              
196             1;
197             __END__