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.0302';
7              
8 1     1   1249 use Moo;
  1         11289  
  1         5  
9 1     1   1977 use strictures 2;
  1         1586  
  1         40  
10 1     1   721 use Algorithm::Combinatorics qw(variations);
  1         3593  
  1         65  
11 1     1   8 use Carp qw(croak);
  1         2  
  1         44  
12 1     1   546 use Data::Dumper::Compact qw(ddc);
  1         13247  
  1         4  
13 1     1   670 use Music::NeoRiemannianTonnetz ();
  1         3675  
  1         25  
14 1     1   455 use Music::Chord::Note ();
  1         1135  
  1         29  
15 1     1   441 use Music::Chord::Namer qw(chordname);
  1         2596  
  1         79  
16 1     1   474 use Music::MelodicDevice::Transposition ();
  1         68469  
  1         46  
17 1     1   11 use namespace::clean;
  1         2  
  1         15  
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   76 my ($self) = @_;
49 8         37 my $cn = Music::Chord::Note->new;
50 8         91 my @chord = $cn->chord_with_octave(
51             $self->base_note . $self->chord_quality,
52             $self->base_octave
53             );
54 8         951 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 5     5   71 return Music::NeoRiemannianTonnetz->new;
105             }
106              
107             has _mdt => (
108             is => 'lazy',
109             );
110              
111             sub _build__mdt {
112 6     6   153 return Music::MelodicDevice::Transposition->new;
113             }
114              
115              
116             sub generate {
117 7     7 1 3103 my ($self) = @_;
118              
119 7         24 my ($pitches, $notes) = $self->_get_pitches;
120              
121 7         25 my @transforms = $self->_build_transform;
122              
123 7 50       27 $self->_initial_conditions(@transforms) if $self->verbose;
124              
125 7         16 my @chords;
126             my @generated;
127 7         13 my $i = 0;
128              
129 7         18 for my $token (@transforms) {
130 23         34 $i++;
131              
132 23         56 my $transformed = $self->_build_chord($token, $pitches, $notes);
133              
134 23         57 my @notes = map { $self->pitchname($_) } @$transformed;
  77         744  
135 23         256 my @base = map { s/^([A-G][#b]?)\d/$1/r } @notes; # for chord-name
  77         300  
136              
137 23 100       100 push @generated, $self->format eq 'ISO' ? \@notes : $transformed;
138              
139 23         55 my $chord = _sanitize_chordname(@base);
140 23         53 push @chords, $chord;
141              
142 23 50       90 printf "%d. %s: %s %s %s\n",
143             $i, $token,
144             ddc($transformed), ddc(\@notes),
145             $chord
146             if $self->verbose;
147              
148 23         82 $notes = $transformed;
149             }
150              
151 7         49 return \@generated, \@transforms, \@chords;
152             }
153              
154              
155             sub circular {
156 1     1 1 432 my ($self) = @_;
157              
158 1         4 my ($pitches, $notes) = $self->_get_pitches;
159              
160 1         5 my @transforms = $self->_build_transform;
161              
162 1 50       5 $self->_initial_conditions(@transforms) if $self->verbose;
163              
164 1         3 my @chords;
165             my @generated;
166 1         3 my $posn = 0;
167              
168 1         6 for my $i (1 .. $self->max) {
169 4         12 my $token = $transforms[ $posn % @transforms ];
170              
171 4         12 my $transformed = $self->_build_chord($token, $pitches, $notes);
172              
173 4         12 my @notes = map { $self->pitchname($_) } @$transformed;
  12         114  
174 4         43 my @base = map { s/^([A-G][#b]?)\d/$1/r } @notes; # for chord-name
  12         47  
175              
176 4 50       18 push @generated, $self->format eq 'ISO' ? \@notes : $transformed;
177              
178 4         14 my $chord = _sanitize_chordname(@base);
179 4         12 push @chords, $chord;
180              
181 4 50       17 printf "%d. %s (%d): %s %s %s\n",
182             $i, $token, $posn % @transforms,
183             ddc($transformed), ddc(\@notes),
184             $chord
185             if $self->verbose;
186              
187 4         7 $notes = $transformed;
188              
189 4 100       25 $posn = int rand 2 ? $posn + 1 : $posn - 1;
190             }
191              
192 1         54 return \@generated, \@transforms, \@chords;
193             }
194              
195             sub _sanitize_chordname {
196 27     27   71 my (@notes) = @_;
197              
198 27         86 my $chord = chordname(@notes);
199              
200             # fix mangled/unknown chordnames
201 27         112776 $chord =~ s/\s+//;
202 27         67 $chord =~ s/o/dim/;
203 27         64 $chord =~ s/maj/M/;
204 27         45 $chord =~ s/sus7/7sus4/;
205 27         42 $chord =~ s/7adda5/7(#5)/;
206 27         47 $chord =~ s/7addb2/7(b9,13)/;
207 27         38 $chord =~ s/9add13/7(9,13)/;
208 27         51 $chord =~ s/7addm10/7(#9)/;
209 27 50       107 $chord = $1 . $2 if $chord =~ /^(.+)\/(\d+)$/;
210             # ...and there are probably more to come...
211              
212 27         87 return $chord;
213             }
214              
215             sub _get_pitches {
216 8     8   18 my ($self) = @_;
217 8         15 my @pitches = map { $self->pitchnum($_) } @{ $self->base_chord };
  26         973  
  8         195  
218 8         398 return \@pitches, [ @pitches ];
219             }
220              
221             sub _initial_conditions {
222 0     0   0 my ($self, @transforms) = @_;
223 0         0 printf "Initial: %s%s %s\nTransforms: %s\n",
224             $self->base_note, $self->base_octave, $self->chord_quality,
225             join(',', @transforms);
226             }
227              
228             sub _build_transform {
229 8     8   19 my ($self) = @_;
230              
231 8         14 my @t; # the transformations to return
232              
233 8 100       47 if (ref $self->transforms eq 'ARRAY') {
    50          
234 5         13 @t = @{ $self->transforms };
  5         20  
235             }
236             elsif ($self->transforms =~ /^\d+$/) {
237 3         10 my @transforms = qw(O I);
238              
239 3 50       7 if (grep { $_ eq 'T' } @{ $self->allowed }) {
  6         19  
  3         18  
240 3         11 push @transforms, (map { 'T' . $_ } 1 .. $self->semitones); # positive
  21         50  
241 3         13 push @transforms, (map { 'T-' . $_ } 1 .. $self->semitones); # negative
  21         48  
242             }
243 3 50       10 if (grep { $_ eq 'N' } @{ $self->allowed }) {
  6         18  
  3         9  
244 3 50       12 if ($self->chord_quality eq 7) {
245 0         0 push @transforms, qw(
246             S23 S32 S34 S43 S56 S65
247             C32 C34 C65
248             );
249             }
250             else {
251 3         9 my @alphabet = qw(P R L);
252 3         8 push @transforms, @alphabet;
253              
254 3         18 my $iter = variations(\@alphabet, 2);
255 3         220 while (my $v = $iter->next) {
256 18         253 push @transforms, join('', @$v);
257             }
258              
259 3         28 $iter = variations(\@alphabet, 3);
260 3         207 while (my $v = $iter->next) {
261 18         197 push @transforms, join('', @$v);
262             }
263             }
264             }
265              
266 3         35 @t = map { $transforms[ int rand @transforms ] }
  11         105  
267             1 .. $self->transforms;
268             }
269              
270 8         25 return @t;
271             }
272              
273             sub _build_chord {
274 27     27   73 my ($self, $token, $pitches, $notes) = @_;
275              
276 27         42 my $chord;
277              
278 27 100       141 if ($token =~ /^O$/) {
    100          
    100          
279 2         7 $chord = $pitches; # return to the original chord
280             }
281             elsif ($token =~ /^I$/) {
282 4         9 $chord = $notes; # no transformation
283             }
284             elsif ($token =~ /^T(-?\d+)$/) {
285 11         35 my $semitones = $1;
286 11         276 $chord = $self->_mdt->transpose($semitones, $notes);
287             }
288             else {
289 10 100 100     186 my $task = $self->_nrt->taskify_tokens($token)
290             if length $token > 1 && $token !~ /\d/;
291 10 100       260 my $op = defined $task ? $task : $token;
292              
293 10         224 $chord = $self->_nrt->transform($op, $notes);
294             }
295              
296 27         16116 return $chord;
297             }
298              
299             1;
300              
301             __END__