File Coverage

blib/lib/Music/Dice.pm
Criterion Covered Total %
statement 233 244 95.4
branch 9 14 64.2
condition 10 15 66.6
subroutine 79 80 98.7
pod 33 34 97.0
total 364 387 94.0


line stmt bran cond sub pod time code
1             package Music::Dice;
2             our $AUTHORITY = 'cpan:GENE';
3              
4             # ABSTRACT: Define and roll musical dice
5              
6             our $VERSION = '0.0206';
7              
8 2     2   610297 use Moo;
  2         16255  
  2         12  
9 2     2   4204 use strictures 2;
  2         3499  
  2         87  
10 2     2   898 use Carp qw(croak);
  2         5  
  2         117  
11 2     2   827 use Games::Dice::Advanced ();
  2         2210  
  2         61  
12 2     2   868 use List::Util::WeightedChoice qw(choose_weighted);
  2         26203  
  2         141  
13 2     2   1026 use MIDI::Util qw(midi_dump);
  2         48811  
  2         153  
14 2     2   1153 use Music::Duration::Partition ();
  2         101233  
  2         138  
15 2     2   1462 use Music::Scales qw(get_scale_notes get_scale_nums);
  2         14532  
  2         247  
16 2     2   1403 use Music::ToRoman ();
  2         43316  
  2         145  
17 2     2   1668 use Types::Standard qw(ArrayRef Int Str);
  2         313744  
  2         27  
18 2     2   7697 use namespace::clean;
  2         6  
  2         20  
19              
20              
21             has semitones => (
22             is => 'ro',
23             isa => sub { croak "$_[0] is not an integer" unless $_[0] =~ /^\d+$/ },
24             default => sub { 12 },
25             );
26              
27              
28             has scale_note => (
29             is => 'ro',
30             isa => sub { croak "$_[0] is not a valid note name" unless $_[0] =~ /^[A-G][b#]?$/ },
31             default => sub { 'C' },
32             );
33              
34              
35             has scale_name => (
36             is => 'ro',
37             isa => sub { croak "$_[0] is not a valid scale name" unless $_[0] =~ /^[a-z]+$/ },
38             default => sub { 'chromatic' },
39             );
40              
41              
42             has flats => (
43             is => 'ro',
44             isa => sub { croak "$_[0] is not a boolean" unless $_[0] =~ /^[01]$/ },
45             default => sub { 1 },
46             );
47              
48              
49             has beats => (
50             is => 'ro',
51             isa => sub { croak "$_[0] is not a positive number" unless $_[0] =~ /^[1-9]\d*$/ },
52             default => sub { 4 },
53             );
54              
55              
56             has phrase_pool => (
57             is => 'rw',
58             );
59              
60              
61             has phrase_weights => (
62             is => 'rw',
63             );
64              
65              
66             has phrase_groups => (
67             is => 'rw',
68             );
69              
70              
71             has octaves => (
72             is => 'ro',
73             isa => ArrayRef[Int],
74             default => sub { [ 2 .. 6 ] },
75             );
76              
77              
78             has notes => (
79             is => 'lazy',
80             );
81              
82             sub _build_notes {
83 8     8   7613 my ($self) = @_;
84 8 100       36 my $keypref = $self->flats ? 'b' : '#';
85 8         50 my @notes = get_scale_notes($self->scale_note, $self->scale_name, 0, $keypref);
86 8         1661 return \@notes;
87             }
88              
89              
90             has intervals => (
91             is => 'lazy',
92             );
93              
94             sub _build_intervals {
95 9     9   2412 my ($self) = @_;
96 9         46 my @nums = get_scale_nums($self->scale_name);
97 9         242 my @intervals = map { $nums[$_] - $nums[$_ - 1] } 1 .. $#nums;
  74         170  
98 9         41 push @intervals, $self->semitones - $nums[-1];
99 9 100       32 if ($self->identity) {
100 2         6 unshift @intervals, 0;
101 2         7 push @intervals, $self->semitones;
102             }
103 9         68 return \@intervals;
104             }
105              
106              
107             has identity => (
108             is => 'ro',
109             isa => sub { croak "$_[0] is not a boolean" unless $_[0] =~ /^[01]$/ },
110             default => sub { 0 },
111             );
112              
113              
114             has chord_triads => (
115             is => 'ro',
116             isa => ArrayRef[Str],
117             default => sub {
118             [qw(
119             major
120             minor
121             diminished
122             augmented
123             custom
124             )],
125             },
126             );
127              
128              
129             has chord_triad_weights => (
130             is => 'ro',
131             isa => ArrayRef[Int],
132             default => sub { [qw(2 2 1 1 1)] },
133             );
134              
135              
136             has chord_qualities_major => (
137             is => 'ro',
138             isa => ArrayRef[Str],
139             default => sub {
140             [qw(
141             add2 sus2
142             add4 sus4
143             -5
144             -6 6
145             M7 7
146             add9
147             )],
148             },
149             );
150              
151              
152             has chord_qualities_major_7 => (
153             is => 'ro',
154             isa => ArrayRef[Str],
155             default => sub {
156 2     2   3503 no warnings 'qw';
  2         5  
  2         340  
157             [qw(
158             7sus4 7b5 7#5
159             69
160             M79
161             7b9 9 7#9
162             7(b9,13) 7(9,13)
163             9b5
164             M11 11 7#11
165             M13 13 7#13
166             )],
167             },
168             );
169              
170              
171             has chord_qualities_minor => (
172             is => 'ro',
173             isa => ArrayRef[Str],
174             default => sub {
175             [qw(
176             madd4
177             m6
178             m7
179             )],
180             },
181             );
182              
183              
184             has chord_qualities_minor_7 => (
185             is => 'ro',
186             isa => ArrayRef[Str],
187             default => sub {
188 2     2   15 no warnings 'qw';
  2         5  
  2         368  
189             [qw(
190             m7b5 m7#5
191             m9
192             m7(9,11)
193             m11
194             m13
195             )],
196             },
197             );
198              
199              
200             has chord_qualities_diminished => (
201             is => 'ro',
202             isa => ArrayRef[Str],
203             default => sub {
204             [qw(
205             dim6
206             dim7
207             )],
208             },
209             );
210              
211              
212             has chord_qualities_augmented => (
213             is => 'ro',
214             isa => ArrayRef[Str],
215             default => sub {
216             [qw(
217             augM7 aug7
218             )],
219             },
220             );
221              
222              
223             has chord_qualities_augmented_7 => (
224             is => 'ro',
225             isa => ArrayRef[Str],
226             default => sub {
227 2     2   14 no warnings 'qw';
  2         5  
  2         8145  
228             [qw(
229             aug9
230             )],
231             },
232             );
233              
234              
235             has modes => (
236             is => 'ro',
237             isa => ArrayRef[Str],
238             default => sub {
239             [qw(
240             ionian
241             dorian
242             phrygian
243             lydian
244             mixolydian
245             aeolian
246             locrian
247             )],
248             },
249             );
250              
251              
252             has ionian_mask => (
253             is => 'ro',
254             isa => ArrayRef[Str],
255             default => sub {
256             [qw(I ii iii IV V vi viio)],
257             },
258             );
259              
260              
261             has dorian_mask => (
262             is => 'ro',
263             isa => ArrayRef[Str],
264             default => sub {
265             [qw(i ii III IV v vio VII)],
266             },
267             );
268              
269              
270             has phrygian_mask => (
271             is => 'ro',
272             isa => ArrayRef[Str],
273             default => sub {
274             [qw(i II III iv vo VI vii)],
275             },
276             );
277              
278              
279             has lydian_mask => (
280             is => 'ro',
281             isa => ArrayRef[Str],
282             default => sub {
283             [qw(I II iii ivo V vi vii)],
284             },
285             );
286              
287              
288             has mixolydian_mask => (
289             is => 'ro',
290             isa => ArrayRef[Str],
291             default => sub {
292             [qw(I ii iiio IV v vi VII)],
293             },
294             );
295              
296              
297             has aeolian_mask => (
298             is => 'ro',
299             isa => ArrayRef[Str],
300             default => sub {
301             [qw(i iio III iv v VI VII)],
302             },
303             );
304              
305              
306             has locrian_mask => (
307             is => 'ro',
308             isa => ArrayRef[Str],
309             default => sub {
310             [qw(io II iii iv V VI vii)],
311             },
312             );
313              
314              
315             has tonnetzen => (
316             is => 'ro',
317             isa => ArrayRef[Str],
318             default => sub { [qw(P R L N S H)],
319             },
320             );
321              
322              
323             has tonnetzen_7 => (
324             is => 'ro',
325             isa => ArrayRef[Str],
326             default => sub { [qw(S23 S32 S34 S43 S56 S65 C32 C34 C65)],
327             },
328             );
329              
330              
331             has rhythmic_phrase_constraints => (
332             is => 'ro',
333             isa => ArrayRef[Int],
334             default => sub { [ 3, 4, 5 ] },
335             );
336              
337              
338             sub BUILD {
339 13     13 0 303 my ($self, $args) = @_;
340 13 100 66     55 if (exists $args->{phrase_pool} && !ref $args->{phrase_pool} && $args->{phrase_pool} eq 'all') {
      66        
341 1         3 $self->phrase_pool([ sort keys %{ midi_dump('length') } ]);
  1         9  
342             }
343             else {
344 12         62 $self->phrase_pool([qw(wn dhn hn dqn qn den en)]);
345             }
346 13         330 $self->phrase_weights([ (1) x @{ $self->phrase_pool } ]);
  13         58  
347 13         24 $self->phrase_groups([ (1) x @{ $self->phrase_pool } ]);
  13         96  
348             }
349              
350              
351             sub octave {
352 1     1 1 375 my ($self) = @_;
353             my $d = sub {
354 1     1   60 return choose_weighted($self->octaves, [ (1) x @{ $self->octaves } ])
  1         7  
355 1         5 };
356 1         11 return Games::Dice::Advanced->new($d);
357             }
358              
359              
360             sub note {
361 1     1 1 431 my ($self) = @_;
362             my $d = sub {
363 1     1   59 return choose_weighted($self->notes, [ (1) x @{ $self->notes } ])
  1         16  
364 1         5 };
365 1         5 return Games::Dice::Advanced->new($d);
366             }
367              
368              
369             sub interval {
370 1     1 1 392 my ($self) = @_;
371             my $d = sub {
372 1     1   70 return choose_weighted($self->intervals, [ (1) x @{ $self->intervals } ])
  1         14  
373 1         4 };
374 1         3 return Games::Dice::Advanced->new($d);
375             }
376              
377              
378             sub note_chromatic {
379 1     1 1 401 my ($self) = @_;
380             my $d = sub {
381 1 50   1   41 my $keypref = $self->flats ? 'b' : '#';
382 1         5 my $choices = [ get_scale_notes($self->scale_note, 'chromatic', 0, $keypref) ];
383 1         91 return choose_weighted($choices, [ (1) x @$choices ])
384 1         4 };
385 1         4 return Games::Dice::Advanced->new($d);
386             }
387              
388              
389             sub interval_chromatic {
390 1     1 1 378 my ($self) = @_;
391             my $d = sub {
392 1     1   39 my $choices = [ (1) x $self->semitones ];
393 1         3 return choose_weighted($choices, $choices);
394 1         3 };
395 1         4 return Games::Dice::Advanced->new($d);
396             }
397              
398              
399             sub note_major {
400 1     1 1 379 my ($self) = @_;
401             my $d = sub {
402 1 50   1   39 my $keypref = $self->flats ? 'b' : '#';
403 1         4 my $choices = [ get_scale_notes($self->scale_note, 'major', 0, $keypref) ];
404 1         139 return choose_weighted($choices, [ (1) x @$choices ])
405 1         4 };
406 1         4 return Games::Dice::Advanced->new($d);
407             }
408              
409              
410             sub interval_major {
411 1     1 1 381 my ($self) = @_;
412             my $d = sub {
413 1     1   38 my $choices = [qw(2 2 1 2 2 2 1)];
414 1         4 return choose_weighted($choices, [ (1) x @$choices ]);
415 1         4 };
416 1         4 return Games::Dice::Advanced->new($d);
417             }
418              
419              
420             sub note_minor {
421 1     1 1 354 my ($self) = @_;
422             my $d = sub {
423 1 50   1   39 my $keypref = $self->flats ? 'b' : '#';
424 1         4 my $choices = [ get_scale_notes($self->scale_note, 'minor', 0, $keypref) ];
425 1         136 return choose_weighted($choices, [ (1) x @$choices ])
426 1         4 };
427 1         3 return Games::Dice::Advanced->new($d);
428             }
429              
430              
431             sub interval_minor {
432 1     1 1 373 my ($self) = @_;
433             my $d = sub {
434 1     1   38 my $choices = [qw(2 1 2 2 1 2 2)];
435 1         4 return choose_weighted($choices, [ (1) x @$choices ]);
436 1         4 };
437 1         3 return Games::Dice::Advanced->new($d);
438             }
439              
440              
441             sub chord_triad {
442 1     1 1 359 my ($self) = @_;
443             my $d = sub {
444 1     1   41 return choose_weighted($self->chord_triads, $self->chord_triad_weights)
445 1         5 };
446 1         3 return Games::Dice::Advanced->new($d);
447             }
448              
449              
450             sub chord_quality_major {
451 1     1 1 324 my ($self) = @_;
452             my $d = sub {
453 1     1   37 return choose_weighted($self->chord_qualities_major, [ (1) x @{ $self->chord_qualities_major } ])
  1         5  
454 1         4 };
455 1         3 return Games::Dice::Advanced->new($d);
456             }
457              
458              
459             sub chord_quality_major_7 {
460 1     1 1 353 my ($self) = @_;
461             my $d = sub {
462 1     1   39 return choose_weighted($self->chord_qualities_major_7, [ (1) x @{ $self->chord_qualities_major_7 } ])
  1         4  
463 1         4 };
464 1         4 return Games::Dice::Advanced->new($d);
465             }
466              
467              
468             sub chord_quality_minor {
469 1     1 1 394 my ($self) = @_;
470             my $d = sub {
471 1     1   38 return choose_weighted($self->chord_qualities_minor, [ (1) x @{ $self->chord_qualities_minor } ])
  1         5  
472 1         4 };
473 1         3 return Games::Dice::Advanced->new($d);
474             }
475              
476              
477             sub chord_quality_minor_7 {
478 1     1 1 327 my ($self) = @_;
479             my $d = sub {
480 1     1   38 return choose_weighted($self->chord_qualities_minor_7, [ (1) x @{ $self->chord_qualities_minor_7 } ])
  1         4  
481 1         4 };
482 1         3 return Games::Dice::Advanced->new($d);
483             }
484              
485              
486             sub chord_quality_diminished {
487 1     1 1 441 my ($self) = @_;
488             my $d = sub {
489 1     1   40 return choose_weighted($self->chord_qualities_diminished, [ (1) x @{ $self->chord_qualities_diminished } ])
  1         5  
490 1         5 };
491 1         25 return Games::Dice::Advanced->new($d);
492             }
493              
494              
495             sub chord_quality_augmented {
496 1     1 1 346 my ($self) = @_;
497             my $d = sub {
498 1     1   37 return choose_weighted($self->chord_qualities_augmented, [ (1) x @{ $self->chord_qualities_augmented } ])
  1         5  
499 1         4 };
500 1         4 return Games::Dice::Advanced->new($d);
501             }
502              
503              
504             sub chord_quality_augmented_7 {
505 1     1 1 309 my ($self) = @_;
506             my $d = sub {
507 1     1   58 return choose_weighted($self->chord_qualities_augmented_7, [ (1) x @{ $self->chord_qualities_augmented_7 } ])
  1         4  
508 1         3 };
509 1         4 return Games::Dice::Advanced->new($d);
510             }
511              
512              
513             sub chord_quality_triad_roll {
514 0     0 1 0 my ($self, $note, $triad) = @_;
515 0         0 my $quality = '';
516 0 0       0 if ($triad eq 'custom') {
517 0         0 my @custom;
518 0         0 my $item = $self->unique_item([ $note ]);
519 0         0 push @custom, $item;
520 0         0 push @custom, $self->unique_item([ $note, $item ]);
521 0         0 $quality = " @custom";
522             }
523             else {
524 0         0 my $method = 'chord_quality_' . $triad;
525 0         0 $quality = $self->$method->roll;
526             }
527 0         0 return $quality;
528             }
529              
530              
531             sub mode {
532 1     1 1 309 my ($self) = @_;
533             my $d = sub {
534 1     1   39 return choose_weighted($self->modes, [ (1) x @{ $self->modes } ])
  1         5  
535 1         3 };
536 1         5 return Games::Dice::Advanced->new($d);
537             }
538              
539              
540             sub ionian {
541 2     2 1 345 my ($self) = @_;
542             my $d = sub {
543 2     2   74 return choose_weighted($self->ionian_mask, [ (1) x @{ $self->ionian_mask } ])
  2         6  
544 2         6 };
545 2         7 return Games::Dice::Advanced->new($d);
546             }
547              
548              
549             sub dorian {
550 1     1 1 344 my ($self) = @_;
551             my $d = sub {
552 1     1   37 return choose_weighted($self->dorian_mask, [ (1) x @{ $self->dorian_mask } ])
  1         4  
553 1         3 };
554 1         3 return Games::Dice::Advanced->new($d);
555             }
556              
557              
558             sub phrygian {
559 1     1 1 325 my ($self) = @_;
560             my $d = sub {
561 1     1   51 return choose_weighted($self->phrygian_mask, [ (1) x @{ $self->phrygian_mask } ])
  1         5  
562 1         3 };
563 1         4 return Games::Dice::Advanced->new($d);
564             }
565              
566              
567             sub lydian {
568 1     1 1 344 my ($self) = @_;
569             my $d = sub {
570 1     1   37 return choose_weighted($self->lydian_mask, [ (1) x @{ $self->lydian_mask } ])
  1         5  
571 1         4 };
572 1         3 return Games::Dice::Advanced->new($d);
573             }
574              
575              
576             sub mixolydian {
577 1     1 1 354 my ($self) = @_;
578             my $d = sub {
579 1     1   37 return choose_weighted($self->mixolydian_mask, [ (1) x @{ $self->mixolydian_mask } ])
  1         4  
580 1         4 };
581 1         3 return Games::Dice::Advanced->new($d);
582             }
583              
584              
585             sub aeolian {
586 1     1 1 346 my ($self) = @_;
587             my $d = sub {
588 1     1   38 return choose_weighted($self->aeolian_mask, [ (1) x @{ $self->aeolian_mask } ])
  1         5  
589 1         4 };
590 1         3 return Games::Dice::Advanced->new($d);
591             }
592              
593              
594             sub locrian {
595 1     1 1 461 my ($self) = @_;
596             my $d = sub {
597 1     1   38 return choose_weighted($self->locrian_mask, [ (1) x @{ $self->locrian_mask } ])
  1         18  
598 1         4 };
599 1         4 return Games::Dice::Advanced->new($d);
600             }
601              
602              
603             sub mode_degree_triad_roll {
604 1     1 1 360 my ($self, $mode) = @_;
605 1         4 my $roman = $self->$mode->roll;
606 1         68 my $mtr = Music::ToRoman->new(scale_name => $mode);
607 1         2304 my ($degree, $triad) = $mtr->get_scale_degree($roman);
608 1         23 return $degree, $triad;
609             }
610              
611              
612             sub tonnetz {
613 1     1 1 608 my ($self) = @_;
614             my $d = sub {
615 1     1   41 return choose_weighted($self->tonnetzen, [ (1) x @{ $self->tonnetzen } ])
  1         5  
616 1         4 };
617 1         4 return Games::Dice::Advanced->new($d);
618             }
619              
620              
621             sub tonnetz_7 {
622 1     1 1 370 my ($self) = @_;
623             my $d = sub {
624 1     1   40 return choose_weighted($self->tonnetzen_7, [ (1) x @{ $self->tonnetzen_7 } ])
  1         4  
625 1         4 };
626 1         4 return Games::Dice::Advanced->new($d);
627             }
628              
629             ## RHYTHMS ##
630              
631              
632             sub rhythmic_value {
633 1     1 1 411 my ($self) = @_;
634             my $d = sub {
635 1     1   41 return choose_weighted($self->phrase_pool, [ (1) x @{ $self->phrase_weights } ])
  1         5  
636 1         5 };
637 1         4 return Games::Dice::Advanced->new($d);
638             }
639              
640              
641             sub rhythmic_phrase {
642 1     1 1 392 my ($self) = @_;
643 1         15 my $mdp = Music::Duration::Partition->new(
644             size => $self->beats,
645             pool => $self->phrase_pool,
646             weights => $self->phrase_weights,
647             groups => $self->phrase_groups ,
648             );
649             my $d = sub {
650 1     1   42 return $mdp->motif;
651 1         2198 };
652 1         5 return Games::Dice::Advanced->new($d);
653             }
654              
655              
656             sub rhythmic_phrase_constrained {
657 1     1 1 1320 my ($self) = @_;
658 1         29 my $mdp = Music::Duration::Partition->new(
659             size => $self->beats,
660             pool => $self->phrase_pool,
661             weights => $self->phrase_weights,
662             groups => $self->phrase_groups ,
663             );
664             my $d = sub {
665 1     1   99 my $motif;
666 1   100     8 while (!$motif || !grep { $_ == @$motif } @{ $self->rhythmic_phrase_constraints }) {
  2         14  
  2         2743  
667 2         9 $motif = $mdp->motif;
668             }
669 1         4 return $motif;
670 1         318 };
671 1         6 return Games::Dice::Advanced->new($d);
672             }
673              
674             ## UTILITY ##
675              
676              
677             sub unique_item {
678 1     1 1 671 my ($self, $excludes, $items) = @_;
679 1   33     49 $items ||= $self->notes;
680 1         3 my $item = '';
681 1   66     7 while (!$item || grep { $_ eq $item } @$excludes) {
  1         7  
682 1         9 $item = $items->[ int rand @$items ];
683             }
684 1         6 return $item;
685             }
686              
687             1;
688              
689             __END__