File Coverage

blib/lib/Music/Chord/Progression/NRO.pm
Criterion Covered Total %
statement 97 99 97.9
branch 18 26 69.2
condition n/a
subroutine 16 17 94.1
pod 2 2 100.0
total 133 144 92.3


line stmt bran cond sub pod time code
1             package Music::Chord::Progression::NRO;
2             our $AUTHORITY = 'cpan:GENE';
3              
4             # ABSTRACT: Generate Neo-Riemann chord progressions
5              
6             our $VERSION = '0.0400';
7              
8 1     1   1445 use Moo;
  1         11985  
  1         7  
9 1     1   2092 use strictures 2;
  1         1717  
  1         44  
10 1     1   791 use Algorithm::Combinatorics qw(variations);
  1         3863  
  1         65  
11 1     1   8 use Carp qw(croak);
  1         3  
  1         42  
12 1     1   605 use Data::Dumper::Compact qw(ddc);
  1         13973  
  1         3  
13 1     1   597 use Music::NeoRiemannianTonnetz ();
  1         3749  
  1         24  
14 1     1   529 use Music::Chord::Note ();
  1         1207  
  1         29  
15 1     1   498 use Music::Chord::Namer qw(chordname);
  1         2547  
  1         63  
16 1     1   494 use namespace::clean;
  1         10161  
  1         10  
17              
18             with 'Music::PitchNum';
19              
20              
21             has base_note => (
22             is => 'ro',
23             isa => sub { croak "$_[0] is not a valid note" unless $_[0] =~ /^[A-G][#b]?$/ },
24             default => sub { 'C' },
25             );
26              
27              
28             has base_octave => (
29             is => 'ro',
30             isa => sub { croak "$_[0] is not a valid octave" unless $_[0] =~ /^[1-8]$/ },
31             default => sub { 4 },
32             );
33              
34              
35             has base_scale => (
36             is => 'ro',
37             isa => sub { croak "$_[0] is not a valid scale" unless $_[0] =~ /^(?:major|minor)$/ },
38             default => sub { 'major' },
39             );
40              
41              
42             has base_chord => (
43             is => 'lazy',
44             );
45              
46             sub _build_base_chord {
47 6     6   52 my ($self) = @_;
48 6         23 my $cn = Music::Chord::Note->new;
49 6 50       48 my $quality = $self->base_scale eq 'major' ? '' : 'm';
50 6         29 my @chord = $cn->chord_with_octave($self->base_note . $quality, $self->base_octave);
51 6         667 return \@chord;
52             }
53              
54              
55             has format => (
56             is => 'ro',
57             isa => sub { croak "$_[0] is not a valid format" unless $_[0] =~ /^(?:ISO|midinum)$/ },
58             default => sub { 'ISO' },
59             );
60              
61              
62             has max => (
63             is => 'ro',
64             isa => sub { croak "$_[0] is not a valid maximum" unless $_[0] =~ /^[1-9]\d*$/ },
65             default => sub { 4 },
66             );
67              
68              
69             has transform => (
70             is => 'ro',
71             isa => sub { croak "$_[0] is not a valid transform" unless ref $_[0] eq 'ARRAY' || $_[0] =~ /^[1-9]\d*$/ },
72             default => sub { 4 },
73             );
74              
75              
76             has verbose => (
77             is => 'ro',
78             isa => sub { croak "$_[0] is not a boolean" unless $_[0] =~ /^[01]$/ },
79             default => sub { 0 },
80             );
81              
82              
83             has nrt => (
84             is => 'lazy',
85             );
86              
87             sub _build_nrt {
88 6     6   60 return Music::NeoRiemannianTonnetz->new;
89             }
90              
91              
92             sub generate {
93 5     5 1 2319 my ($self) = @_;
94              
95 5         14 my ($pitches, $notes) = $self->_get_pitches;
96              
97 5         26 my @transform = $self->_build_transform;
98              
99 5 50       17 $self->_initial_conditions(@transform) if $self->verbose;
100              
101 5         8 my @generated;
102 5         9 my $i = 0;
103              
104 5         12 for my $token (@transform) {
105 18         28 $i++;
106              
107 18         40 my $transformed = $self->_build_chord($token, $pitches, $notes);
108              
109 18         38 my @notes = map { $self->pitchname($_) } @$transformed;
  54         479  
110 18         176 my @base = map { s/^([A-G][#b]?)\d/$1/r } @notes; # for chord-name
  54         206  
111              
112 18 100       76 push @generated, $self->format eq 'ISO' ? \@notes : $transformed;
113              
114 18 50       65 printf "%d. %s: %s %s %s\n",
115             $i, $token,
116             ddc($transformed), ddc(\@notes),
117             scalar chordname(@base)
118             if $self->verbose;
119              
120 18         52 $notes = $transformed;
121             }
122              
123 5         20 return \@generated;
124             }
125              
126              
127             sub circular {
128 1     1 1 424 my ($self) = @_;
129              
130 1         4 my ($pitches, $notes) = $self->_get_pitches;
131              
132 1         4 my @transform = $self->_build_transform;
133              
134 1 50       7 $self->_initial_conditions(@transform) if $self->verbose;
135              
136 1         3 my @generated;
137 1         2 my $posn = 0;
138              
139 1         9 for my $i (1 .. $self->max) {
140 4         11 my $token = $transform[ $posn % @transform ];
141              
142 4         11 my $transformed = $self->_build_chord($token, $pitches, $notes);
143              
144 4         12 my @notes = map { $self->pitchname($_) } @$transformed;
  12         97  
145 4         53 my @base = map { s/^([A-G][#b]?)\d/$1/r } @notes; # for chord-name
  12         45  
146              
147 4 50       16 push @generated, $self->format eq 'ISO' ? \@notes : $transformed;
148              
149 4 50       25 printf "%d. %s (%d): %s %s %s\n",
150             $i, $token, $posn % @transform,
151             ddc($transformed), ddc(\@notes),
152             scalar chordname(@base)
153             if $self->verbose;
154              
155 4         8 $notes = $transformed;
156              
157 4 50       18 $posn = int rand 2 ? $posn + 1 : $posn - 1;
158             }
159              
160 1         4 return \@generated;
161             }
162              
163             sub _get_pitches {
164 6     6   11 my ($self) = @_;
165 6         9 my @pitches = map { $self->pitchnum($_) } @{ $self->base_chord };
  18         600  
  6         136  
166 6         260 return \@pitches, [ @pitches ];
167             }
168              
169             sub _initial_conditions {
170 0     0   0 my ($self, @transform) = @_;
171 0         0 printf "Initial: %s%s %s\nTransforms: %s\n",
172             $self->base_note, $self->base_octave, $self->base_scale,
173             join(',', @transform);
174             }
175              
176             sub _build_transform {
177 6     6   12 my ($self) = @_;
178              
179 6         17 my @transform;
180              
181 6 100       52 if (ref $self->transform eq 'ARRAY') {
    50          
182 2         5 @transform = @{ $self->transform };
  2         7  
183             }
184             elsif ($self->transform =~ /^\d+$/) {
185 4         12 my @alphabet = qw(P R L);
186 4         7 my @nro = @alphabet;
187              
188 4         17 my $iter = variations(\@alphabet, 2);
189 4         243 while (my $v = $iter->next) {
190 24         286 push @nro, join('', @$v);
191             }
192 4         34 $iter = variations(\@alphabet, 3);
193 4         315 while (my $v = $iter->next) {
194 24         266 push @nro, join('', @$v);
195             }
196              
197 4         53 @transform = ('I', map { $nro[ int rand @nro ] } 1 .. $self->transform - 1);
  11         94  
198             }
199              
200 6         19 return @transform;
201             }
202              
203             sub _build_chord {
204 22     22   46 my ($self, $token, $pitches, $notes) = @_;
205              
206 22         34 my $chord;
207              
208 22 100       99 if ($token =~ /^I$/) {
209 7         13 $chord = $pitches; # no transformation
210             }
211             else {
212 15 100       324 my $task = $self->nrt->taskify_tokens($token) if length $token > 1;
213 15 100       526 my $tx = defined $task ? $task : $token;
214              
215 15         279 $chord = $self->nrt->transform($tx, $notes);
216             }
217              
218 22         5651 return $chord;
219             }
220              
221             1;
222              
223             __END__