File Coverage

blib/lib/Music/Chord/Progression/Transform.pm
Criterion Covered Total %
statement 131 134 97.7
branch 26 36 72.2
condition 3 3 100.0
subroutine 19 20 95.0
pod 2 2 100.0
total 181 195 92.8


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.0304';
7              
8 2     2   580901 use Moo;
  2         14299  
  2         13  
9 2     2   3947 use strictures 2;
  2         2785  
  2         61  
10 2     2   1670 use Algorithm::Combinatorics qw(variations);
  2         6360  
  2         175  
11 2     2   11 use Carp qw(croak);
  2         5  
  2         79  
12 2     2   1093 use Data::Dumper::Compact qw(ddc);
  2         25633  
  2         8  
13 2     2   1127 use Music::NeoRiemannianTonnetz ();
  2         7470  
  2         64  
14 2     2   827 use Music::Chord::Note ();
  2         2517  
  2         64  
15 2     2   950 use Music::Chord::Namer qw(chordname);
  2         4672  
  2         111  
16 2     2   1027 use Music::MelodicDevice::Transposition ();
  2         162606  
  2         79  
17 2     2   19 use namespace::clean;
  2         4  
  2         17  
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   101 my ($self) = @_;
49 8         67 my $cn = Music::Chord::Note->new;
50 8         129 my @chord = $cn->chord_with_octave(
51             $self->base_note . $self->chord_quality,
52             $self->base_octave
53             );
54 8         1255 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   114 return Music::NeoRiemannianTonnetz->new;
105             }
106              
107             has _mdt => (
108             is => 'lazy',
109             );
110              
111             sub _build__mdt {
112 6     6   207 return Music::MelodicDevice::Transposition->new;
113             }
114              
115              
116             sub generate {
117 7     7 1 4703 my ($self) = @_;
118              
119             # get the pitch-nums of the base_chord - static and mutable
120 7         35 my ($pitches, $notes) = $self->_get_pitches;
121              
122             # get either the defined transformations or a random set
123 7         36 my @transforms = $self->_build_transform;
124              
125 7 50       36 $self->_initial_conditions(@transforms) if $self->verbose;
126              
127 7         16 my @chords;
128             my @generated;
129 7         76 my $i = 0;
130              
131 7         22 for my $token (@transforms) {
132 23         48 $i++;
133              
134             # perform the transformation
135 23         89 my $transformed = $self->_build_chord($token, $pitches, $notes);
136              
137 23         72 my @notes = map { $self->pitchname($_) } @$transformed; # for ISO
  77         1032  
138 23         278 my @base = map { s/^([A-G][#b]?)\d/$1/r } @notes; # for chord-name
  77         348  
139              
140             # tally what has been generated
141 23 100       152 push @generated, $self->format eq 'ISO' ? \@notes : $transformed;
142              
143 23         73 my $chord = _sanitize_chordname(@base);
144 23         65 push @chords, $chord;
145              
146 23 50       152 printf "%d. %s: %s %s %s\n",
147             $i, $token,
148             ddc($transformed), ddc(\@notes),
149             $chord
150             if $self->verbose;
151              
152             # "increment" our pitches
153 23         108 $notes = $transformed;
154             }
155              
156 7         57 return \@generated, \@transforms, \@chords;
157             }
158              
159              
160             sub circular {
161 1     1 1 649 my ($self) = @_;
162              
163             # get the pitch-nums of the base_chord - static and mutable
164 1         6 my ($pitches, $notes) = $self->_get_pitches;
165              
166             # get either the defined transformations or a random set
167 1         9 my @transforms = $self->_build_transform;
168              
169 1 50       7 $self->_initial_conditions(@transforms) if $self->verbose;
170              
171 1         3 my @chords;
172             my @generated;
173 1         3 my $posn = 0;
174              
175 1         7 for my $i (1 .. $self->max) {
176 4         15 my $token = $transforms[ $posn % @transforms ];
177              
178             # perform the transformation
179 4         16 my $transformed = $self->_build_chord($token, $pitches, $notes);
180              
181 4         14 my @notes = map { $self->pitchname($_) } @$transformed; # for ISO
  12         146  
182 4         47 my @base = map { s/^([A-G][#b]?)\d/$1/r } @notes; # for chord-name
  12         58  
183              
184             # tally what has been generated
185 4 50       25 push @generated, $self->format eq 'ISO' ? \@notes : $transformed;
186              
187 4         13 my $chord = _sanitize_chordname(@base);
188 4         12 push @chords, $chord;
189              
190 4 50       20 printf "%d. %s (%d): %s %s %s\n",
191             $i, $token, $posn % @transforms,
192             ddc($transformed), ddc(\@notes),
193             $chord
194             if $self->verbose;
195              
196             # "increment" our pitches
197 4         11 $notes = $transformed;
198              
199             # move left or right at random
200 4 100       33 $posn = int rand 2 ? $posn + 1 : $posn - 1;
201             }
202              
203 1         10 return \@generated, \@transforms, \@chords;
204             }
205              
206             sub _sanitize_chordname {
207 27     27   134 my (@notes) = @_;
208              
209 27         137 my $chord = chordname(@notes);
210              
211             # fix mangled/unknown chordnames
212 27         152711 $chord =~ s/\s+//;
213 27         93 $chord =~ s/o/dim/;
214 27         69 $chord =~ s/maj/M/;
215 27         51 $chord =~ s/sus7/7sus4/;
216 27         53 $chord =~ s/7adda5/7(#5)/;
217 27         74 $chord =~ s/7addb2/7(b9,13)/;
218 27         69 $chord =~ s/9add13/7(9,13)/;
219 27         56 $chord =~ s/7addm10/7(#9)/;
220 27 50       156 $chord = $1 . $2 if $chord =~ /^(.+)\/(\d+)$/;
221             # ...and there are probably more to come...
222              
223 27         125 return $chord;
224             }
225              
226             sub _get_pitches {
227 8     8   23 my ($self) = @_;
228 8         20 my @pitches = map { $self->pitchnum($_) } @{ $self->base_chord };
  26         1331  
  8         357  
229 8         553 return \@pitches, [ @pitches ];
230             }
231              
232             sub _initial_conditions {
233 0     0   0 my ($self, @transforms) = @_;
234 0         0 printf "Initial: %s%s %s\nTransforms: %s\n",
235             $self->base_note, $self->base_octave, $self->chord_quality,
236             join(',', @transforms);
237             }
238              
239             sub _build_transform {
240 8     8   27 my ($self) = @_;
241              
242 8         17 my @t; # the transformations to return
243              
244 8 100       63 if (ref $self->transforms eq 'ARRAY') {
    50          
245 5         11 @t = @{ $self->transforms };
  5         27  
246             }
247             elsif ($self->transforms =~ /^\d+$/) {
248 3         12 my @transforms = qw(O I);
249              
250 3 50       9 if (grep { $_ eq 'T' } @{ $self->allowed }) {
  6         59  
  3         12  
251 3         18 push @transforms, (map { 'T' . $_ } 1 .. $self->semitones); # positive
  21         62  
252 3         15 push @transforms, (map { 'T-' . $_ } 1 .. $self->semitones); # negative
  21         93  
253             }
254 3 50       9 if (grep { $_ eq 'N' } @{ $self->allowed }) {
  6         20  
  3         16  
255 3 50       16 if ($self->chord_quality eq 7) {
256 0         0 push @transforms, qw(
257             S23 S32 S34 S43 S56 S65
258             C32 C34 C65
259             );
260             }
261             else {
262 3         10 my @alphabet = qw(P R L);
263 3         11 push @transforms, @alphabet;
264              
265 3         23 my $iter = variations(\@alphabet, 2);
266 3         241 while (my $v = $iter->next) {
267 18         293 push @transforms, join('', @$v);
268             }
269              
270 3         33 $iter = variations(\@alphabet, 3);
271 3         236 while (my $v = $iter->next) {
272 18         238 push @transforms, join('', @$v);
273             }
274             }
275             }
276              
277 3         50 @t = map { $transforms[ int rand @transforms ] }
  11         103  
278             1 .. $self->transforms;
279             }
280              
281 8         39 return @t;
282             }
283              
284             sub _build_chord {
285 27     27   88 my ($self, $token, $pitches, $notes) = @_;
286              
287 27         49 my $chord;
288              
289 27 100       210 if ($token =~ /^O$/) {
    100          
    100          
290 2         7 $chord = $pitches; # return to the original chord
291             }
292             elsif ($token =~ /^I$/) {
293 5         13 $chord = $notes; # no transformation
294             }
295             elsif ($token =~ /^T(-?\d+)$/) {
296 9         37 my $semitones = $1;
297 9         391 $chord = $self->_mdt->transpose($semitones, $notes);
298             }
299             else {
300 11 100 100     315 my $task = $self->_nrt->taskify_tokens($token)
301             if length $token > 1 && $token !~ /\d/;
302 11 100       353 my $op = defined $task ? $task : $token;
303              
304 11         409 $chord = $self->_nrt->transform($op, $notes);
305             }
306              
307 27         30310 return $chord;
308             }
309              
310             1;
311              
312             __END__