File Coverage

blib/lib/Music/Chord/Progression/NRO.pm
Criterion Covered Total %
statement 87 89 97.7
branch 19 26 73.0
condition n/a
subroutine 15 16 93.7
pod 2 2 100.0
total 123 133 92.4


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.0308';
7              
8 1     1   1336 use Moo;
  1         11304  
  1         5  
9 1     1   1867 use strictures 2;
  1         1724  
  1         39  
10 1     1   291 use Carp qw(croak);
  1         3  
  1         45  
11 1     1   607 use Data::Dumper::Compact qw(ddc);
  1         13974  
  1         5  
12 1     1   641 use Music::NeoRiemannianTonnetz ();
  1         3716  
  1         24  
13 1     1   514 use Music::Chord::Note ();
  1         1113  
  1         30  
14 1     1   463 use Music::Chord::Namer qw(chordname);
  1         2461  
  1         57  
15 1     1   489 use namespace::clean;
  1         9589  
  1         19  
16              
17             with 'Music::PitchNum';
18              
19              
20             has base_note => (
21             is => 'ro',
22             isa => sub { croak "$_[0] is not a valid note" unless $_[0] =~ /^[A-G][#b]?$/ },
23             default => sub { 'C' },
24             );
25              
26              
27             has base_octave => (
28             is => 'ro',
29             isa => sub { croak "$_[0] is not a valid octave" unless $_[0] =~ /^[1-8]$/ },
30             default => sub { 4 },
31             );
32              
33              
34             has base_scale => (
35             is => 'ro',
36             isa => sub { croak "$_[0] is not a valid scale" unless $_[0] =~ /^(?:major|minor)$/ },
37             default => sub { 'major' },
38             );
39              
40              
41             has base_chord => (
42             is => 'lazy',
43             );
44              
45             sub _build_base_chord {
46 6     6   53 my ($self) = @_;
47 6         28 my $cn = Music::Chord::Note->new;
48 6 50       52 my $quality = $self->base_scale eq 'major' ? '' : 'm';
49 6         30 my @chord = $cn->chord_with_octave($self->base_note . $quality, $self->base_octave);
50 6         622 return \@chord;
51             }
52              
53              
54             has format => (
55             is => 'ro',
56             isa => sub { croak "$_[0] is not a valid format" unless $_[0] =~ /^(?:ISO|midinum)$/ },
57             default => sub { 'ISO' },
58             );
59              
60              
61             has max => (
62             is => 'ro',
63             isa => sub { croak "$_[0] is not a valid maximum" unless $_[0] =~ /^[1-9]\d*$/ },
64             default => sub { 4 },
65             );
66              
67              
68             has transform => (
69             is => 'ro',
70             isa => sub { croak "$_[0] is not a valid transform" unless ref $_[0] eq 'ARRAY' || $_[0] =~ /^[1-9]\d*$/ },
71             default => sub { 4 },
72             );
73              
74              
75             has verbose => (
76             is => 'ro',
77             isa => sub { croak "$_[0] is not a boolean" unless $_[0] =~ /^[01]$/ },
78             default => sub { 0 },
79             );
80              
81              
82             has nrt => (
83             is => 'lazy',
84             );
85              
86             sub _build_nrt {
87 6     6   63 return Music::NeoRiemannianTonnetz->new;
88             }
89              
90              
91             sub generate {
92 5     5 1 2156 my ($self) = @_;
93              
94 5         17 my ($pitches, $notes) = $self->_get_pitches;
95              
96 5         17 my @transform = $self->_build_transform;
97              
98 5 50       18 $self->_initial_conditions(@transform) if $self->verbose;
99              
100 5         7 my @generated;
101 5         11 my $i = 0;
102              
103 5         12 for my $token (@transform) {
104 18         28 $i++;
105              
106 18         39 my $transformed = $self->_build_chord($token, $pitches, $notes);
107              
108 18         40 my @notes = map { $self->pitchname($_) } @$transformed;
  54         459  
109 18         178 my @base = map { s/^([A-G][#b]?)\d/$1/r } @notes; # for chord-name
  54         202  
110              
111 18 100       99 push @generated, $self->format eq 'ISO' ? \@notes : $transformed;
112              
113 18 50       69 printf "%d. %s: %s %s %s\n",
114             $i, $token,
115             ddc($transformed), ddc(\@notes),
116             scalar chordname(@base)
117             if $self->verbose;
118              
119 18         54 $notes = $transformed;
120             }
121              
122 5         22 return \@generated;
123             }
124              
125              
126             sub circular {
127 1     1 1 417 my ($self) = @_;
128              
129 1         4 my ($pitches, $notes) = $self->_get_pitches;
130              
131 1         6 my @transform = $self->_build_transform;
132              
133 1 50       5 $self->_initial_conditions(@transform) if $self->verbose;
134              
135 1         3 my @generated;
136 1         2 my $posn = 0;
137              
138 1         7 for my $i (1 .. $self->max) {
139 4         9 my $token = $transform[ $posn % @transform ];
140              
141 4         12 my $transformed = $self->_build_chord($token, $pitches, $notes);
142              
143 4         9 my @notes = map { $self->pitchname($_) } @$transformed;
  12         97  
144 4         41 my @base = map { s/^([A-G][#b]?)\d/$1/r } @notes; # for chord-name
  12         41  
145              
146 4 50       18 push @generated, $self->format eq 'ISO' ? \@notes : $transformed;
147              
148 4 50       13 printf "%d. %s (%d): %s %s %s\n",
149             $i, $token, $posn % @transform,
150             ddc($transformed), ddc(\@notes),
151             scalar chordname(@base)
152             if $self->verbose;
153              
154 4         8 $notes = $transformed;
155              
156 4 100       19 $posn = int rand 2 ? $posn + 1 : $posn - 1;
157             }
158              
159 1         4 return \@generated;
160             }
161              
162             sub _get_pitches {
163 6     6   12 my ($self) = @_;
164 6         38 my @pitches = map { $self->pitchnum($_) } @{ $self->base_chord };
  18         619  
  6         139  
165 6         254 return \@pitches, [ @pitches ];
166             }
167              
168             sub _initial_conditions {
169 0     0   0 my ($self, @transform) = @_;
170 0         0 printf "Initial: %s%s %s\nTransforms: %s\n",
171             $self->base_note, $self->base_octave, $self->base_scale,
172             join(',', @transform);
173             }
174              
175             sub _build_transform {
176 6     6   12 my ($self) = @_;
177              
178 6         11 my @transform;
179              
180 6 100       47 if (ref $self->transform eq 'ARRAY') {
    50          
181 2         5 @transform = @{ $self->transform };
  2         8  
182             }
183             elsif ($self->transform =~ /^\d+$/) {
184 4         14 my @nro = qw(L P R N S H PRL);
185              
186 4         12 @transform = ('X', map { $nro[ int rand @nro ] } 1 .. $self->transform - 1);
  11         84  
187             }
188              
189 6         19 return @transform;
190             }
191              
192             sub _build_chord {
193 22     22   47 my ($self, $token, $pitches, $notes) = @_;
194              
195 22         35 my $chord;
196              
197 22 100       66 if ($token =~ /^X$/) {
198 7         14 $chord = $pitches; # no transformation
199             }
200             else {
201 15 100       91 my $task = $self->nrt->taskify_tokens($token) if length $token > 1;
202 15 100       157 my $tx = defined $task ? $task : $token;
203              
204 15         316 $chord = $self->nrt->transform($tx, $notes);
205             }
206              
207 22         6211 return $chord;
208             }
209              
210             1;
211              
212             __END__