File Coverage

blib/lib/Music/Chord/Progression/Transform.pm
Criterion Covered Total %
statement 123 126 97.6
branch 26 36 72.2
condition 3 3 100.0
subroutine 18 19 94.7
pod 2 2 100.0
total 172 186 92.4


line stmt bran cond sub pod time code
1             package Music::Chord::Progression::Transform;
2             our $AUTHORITY = 'cpan:GENE';
3              
4             # ABSTRACT: Generate transformed chord progressions
5              
6             our $VERSION = '0.0301';
7              
8 1     1   1249 use Moo;
  1         11049  
  1         6  
9 1     1   1891 use strictures 2;
  1         1641  
  1         41  
10 1     1   684 use Algorithm::Combinatorics qw(variations);
  1         3644  
  1         67  
11 1     1   9 use Carp qw(croak);
  1         2  
  1         43  
12 1     1   535 use Data::Dumper::Compact qw(ddc);
  1         13170  
  1         5  
13 1     1   581 use Music::NeoRiemannianTonnetz ();
  1         3564  
  1         25  
14 1     1   453 use Music::Chord::Note ();
  1         1097  
  1         29  
15 1     1   437 use Music::Chord::Namer qw(chordname);
  1         2469  
  1         73  
16 1     1   435 use Music::MelodicDevice::Transposition ();
  1         68790  
  1         35  
17 1     1   12 use namespace::clean;
  1         3  
  1         13  
18              
19             with 'Music::PitchNum';
20              
21              
22             has base_note => (
23             is => 'ro',
24             isa => sub { croak "$_[0] is not a valid note" unless $_[0] =~ /^[A-G][#b]?$/ },
25             default => sub { 'C' },
26             );
27              
28              
29             has base_octave => (
30             is => 'ro',
31             isa => sub { croak "$_[0] is not a valid octave" unless $_[0] =~ /^[1-8]$/ },
32             default => sub { 4 },
33             );
34              
35              
36             has chord_quality => (
37             is => 'ro',
38             default => sub { '' },
39             );
40              
41              
42             has base_chord => (
43             is => 'lazy',
44             init_arg => undef,
45             );
46              
47             sub _build_base_chord {
48 8     8   78 my ($self) = @_;
49 8         42 my $cn = Music::Chord::Note->new;
50 8         107 my @chord = $cn->chord_with_octave(
51             $self->base_note . $self->chord_quality,
52             $self->base_octave
53             );
54 8         926 return \@chord;
55             }
56              
57              
58             has format => (
59             is => 'ro',
60             isa => sub { croak "$_[0] is not a valid format" unless $_[0] =~ /^(?:ISO|midinum)$/ },
61             default => sub { 'midinum' },
62             );
63              
64              
65             has semitones => (
66             is => 'ro',
67             isa => sub { croak "$_[0] is not a valid number of semitones" unless $_[0] =~ /^[1-9]\d*$/ },
68             default => sub { 7 },
69             );
70              
71              
72             has max => (
73             is => 'ro',
74             isa => sub { croak "$_[0] is not a valid maximum" unless $_[0] =~ /^[1-9]\d*$/ },
75             default => sub { 4 },
76             );
77              
78              
79             has allowed => (
80             is => 'ro',
81             isa => sub { croak "$_[0] is not valid" unless ref $_[0] eq 'ARRAY' },
82             default => sub { [qw(T N)] },
83             );
84              
85              
86             has transforms => (
87             is => 'ro',
88             isa => sub { croak "$_[0] is not a valid transform" unless ref $_[0] eq 'ARRAY' || $_[0] =~ /^[1-9]\d*$/ },
89             default => sub { 4 },
90             );
91              
92              
93             has verbose => (
94             is => 'ro',
95             isa => sub { croak "$_[0] is not a boolean" unless $_[0] =~ /^[01]$/ },
96             default => sub { 0 },
97             );
98              
99             has _nrt => (
100             is => 'lazy',
101             );
102              
103             sub _build__nrt {
104 6     6   79 return Music::NeoRiemannianTonnetz->new;
105             }
106              
107             has _mdt => (
108             is => 'lazy',
109             );
110              
111             sub _build__mdt {
112 4     4   94 return Music::MelodicDevice::Transposition->new;
113             }
114              
115              
116             sub generate {
117 7     7 1 3082 my ($self) = @_;
118              
119 7         26 my ($pitches, $notes) = $self->_get_pitches;
120              
121 7         28 my @transforms = $self->_build_transform;
122              
123 7 50       27 $self->_initial_conditions(@transforms) if $self->verbose;
124              
125 7         13 my @chords;
126             my @generated;
127 7         15 my $i = 0;
128              
129 7         15 for my $token (@transforms) {
130 23         41 $i++;
131              
132 23         60 my $transformed = $self->_build_chord($token, $pitches, $notes);
133              
134 23         54 my @notes = map { $self->pitchname($_) } @$transformed;
  77         734  
135 23         236 my @base = map { s/^([A-G][#b]?)\d/$1/r } @notes; # for chord-name
  77         285  
136              
137 23 100       98 push @generated, $self->format eq 'ISO' ? \@notes : $transformed;
138              
139 23         69 my $chord = chordname(@base);
140 23         95433 $chord =~ s/\s+//;
141 23         59 $chord =~ s/-6/6/;
142 23         46 $chord =~ s/o/dim/;
143 23 50       95 $chord = $1 . $2 if $chord =~ /^(.+)\/(\d+)$/;
144 23         78 push @chords, $chord;
145              
146 23 50       91 printf "%d. %s: %s %s %s\n",
147             $i, $token,
148             ddc($transformed), ddc(\@notes),
149             $chord
150             if $self->verbose;
151              
152 23         76 $notes = $transformed;
153             }
154              
155 7         40 return \@generated, \@transforms, \@chords;
156             }
157              
158              
159             sub circular {
160 1     1 1 421 my ($self) = @_;
161              
162 1         6 my ($pitches, $notes) = $self->_get_pitches;
163              
164 1         5 my @transforms = $self->_build_transform;
165              
166 1 50       7 $self->_initial_conditions(@transforms) if $self->verbose;
167              
168 1         4 my @chords;
169             my @generated;
170 1         3 my $posn = 0;
171              
172 1         6 for my $i (1 .. $self->max) {
173 4         11 my $token = $transforms[ $posn % @transforms ];
174              
175 4         12 my $transformed = $self->_build_chord($token, $pitches, $notes);
176              
177 4         10 my @notes = map { $self->pitchname($_) } @$transformed;
  12         109  
178 4         41 my @base = map { s/^([A-G][#b]?)\d/$1/r } @notes; # for chord-name
  12         49  
179              
180 4 50       21 push @generated, $self->format eq 'ISO' ? \@notes : $transformed;
181              
182 4         14 my $chord = chordname(@base);
183 4         16243 push @chords, $chord;
184              
185 4 50       19 printf "%d. %s (%d): %s %s %s\n",
186             $i, $token, $posn % @transforms,
187             ddc($transformed), ddc(\@notes),
188             $chord
189             if $self->verbose;
190              
191 4         9 $notes = $transformed;
192              
193 4 100       24 $posn = int rand 2 ? $posn + 1 : $posn - 1;
194             }
195              
196 1         6 return \@generated, \@transforms, \@chords;
197             }
198              
199             sub _get_pitches {
200 8     8   21 my ($self) = @_;
201 8         17 my @pitches = map { $self->pitchnum($_) } @{ $self->base_chord };
  26         987  
  8         187  
202 8         395 return \@pitches, [ @pitches ];
203             }
204              
205             sub _initial_conditions {
206 0     0   0 my ($self, @transforms) = @_;
207 0         0 printf "Initial: %s%s %s\nTransforms: %s\n",
208             $self->base_note, $self->base_octave, $self->chord_quality,
209             join(',', @transforms);
210             }
211              
212             sub _build_transform {
213 8     8   15 my ($self) = @_;
214              
215 8         19 my @t; # the transformations to return
216              
217 8 100       50 if (ref $self->transforms eq 'ARRAY') {
    50          
218 5         9 @t = @{ $self->transforms };
  5         19  
219             }
220             elsif ($self->transforms =~ /^\d+$/) {
221 3         10 my @transforms = qw(O I);
222              
223 3 50       7 if (grep { $_ eq 'T' } @{ $self->allowed }) {
  6         19  
  3         15  
224 3         12 push @transforms, (map { 'T' . $_ } 1 .. $self->semitones); # positive
  21         50  
225 3         11 push @transforms, (map { 'T-' . $_ } 1 .. $self->semitones); # negative
  21         60  
226             }
227 3 50       10 if (grep { $_ eq 'N' } @{ $self->allowed }) {
  6         16  
  3         11  
228 3 50       12 if ($self->chord_quality eq 7) {
229 0         0 push @transforms, qw(
230             S23 S32 S34 S43 S56 S65
231             C32 C34 C65
232             );
233             }
234             else {
235 3         9 my @alphabet = qw(P R L);
236 3         7 push @transforms, @alphabet;
237              
238 3         16 my $iter = variations(\@alphabet, 2);
239 3         201 while (my $v = $iter->next) {
240 18         230 push @transforms, join('', @$v);
241             }
242              
243 3         28 $iter = variations(\@alphabet, 3);
244 3         191 while (my $v = $iter->next) {
245 18         228 push @transforms, join('', @$v);
246             }
247             }
248             }
249              
250 3         38 @t = map { $transforms[ int rand @transforms ] }
  11         80  
251             1 .. $self->transforms;
252             }
253              
254 8         29 return @t;
255             }
256              
257             sub _build_chord {
258 27     27   67 my ($self, $token, $pitches, $notes) = @_;
259              
260 27         43 my $chord;
261              
262 27 100       132 if ($token =~ /^O$/) {
    100          
    100          
263 2         4 $chord = $pitches; # return to the original chord
264             }
265             elsif ($token =~ /^I$/) {
266 4         25 $chord = $notes; # no transformation
267             }
268             elsif ($token =~ /^T(-?\d+)$/) {
269 8         26 my $semitones = $1;
270 8         190 $chord = $self->_mdt->transpose($semitones, $notes);
271             }
272             else {
273 13 100 100     232 my $task = $self->_nrt->taskify_tokens($token)
274             if length $token > 1 && $token !~ /\d/;
275 13 100       338 my $op = defined $task ? $task : $token;
276              
277 13         313 $chord = $self->_nrt->transform($op, $notes);
278             }
279              
280 27         14014 return $chord;
281             }
282              
283             1;
284              
285             __END__