File Coverage

blib/lib/Music/AtonalUtil.pm
Criterion Covered Total %
statement 579 587 98.6
branch 294 328 89.6
condition 80 87 91.9
subroutine 56 56 100.0
pod 45 45 100.0
total 1054 1103 95.5


line stmt bran cond sub pod time code
1             # -*- Perl -*-
2             #
3             # Code for atonal music analysis and composition (and a certain
4             # accumulation of somewhat related utility code, in the best fashion of
5             # that kitchen drawer).
6              
7             package Music::AtonalUtil;
8              
9 3     3   330355 use 5.010;
  3         58  
10 3     3   15 use strict;
  3         6  
  3         69  
11 3     3   14 use warnings;
  3         6  
  3         87  
12              
13             # as Math::Combinatorics does not preserve input order in return values
14 3     3   1314 use Algorithm::Combinatorics qw/combinations/;
  3         9820  
  3         219  
15 3     3   27 use Carp qw/croak/;
  3         5  
  3         127  
16 3     3   16 use List::Util qw/shuffle uniqnum/;
  3         6  
  3         292  
17 3     3   18 use Scalar::Util qw/looks_like_number refaddr/;
  3         6  
  3         22395  
18              
19             our $VERSION = '1.17';
20              
21             my $DEG_IN_SCALE = 12;
22              
23             # Forte Number to prime form mapping. These are mostly in agreement with
24             # Appendix 2, Table II in "Basic Atonal Theory" (rahn1980) by John Rahn
25             # (p.140-143), and also against Appendix 1 in "The Structure of Atonal
26             # Music" (forte1973) by Allen Forte (p.179-181), though Rahn and Forte use
27             # different methods and thus calculate different prime forms in a few
28             # cases. See t/forte2pcs2forte.t for tests of these against what
29             # prime_form() calculates. This code uses the Rahn method (though still
30             # calls them "Forte Numbers" instead of the perhaps more appropriate
31             # "Rahn Number").
32             #
33             # By mostly, my calculation disagrees with rahn1980 for 7-Z18, 7-20, and
34             # 8-26 (by eyeball inspection). These three look to be typos in
35             # rahn1980, as in each case Rahn used the Forte form.
36             #
37             # sorting is to align with the table in rahn1980
38             our %FORTE2PCS = (
39             # trichords (complement nonachords)
40             '3-1' => [ 0, 1, 2 ],
41             '3-2' => [ 0, 1, 3 ],
42             '3-3' => [ 0, 1, 4 ],
43             '3-4' => [ 0, 1, 5 ],
44             '3-5' => [ 0, 1, 6 ],
45             '3-6' => [ 0, 2, 4 ],
46             '3-7' => [ 0, 2, 5 ],
47             '3-8' => [ 0, 2, 6 ],
48             '3-9' => [ 0, 2, 7 ],
49             '3-10' => [ 0, 3, 6 ],
50             '3-11' => [ 0, 3, 7 ],
51             '3-12' => [ 0, 4, 8 ],
52             # nonachords (trichords)
53             '9-1' => [ 0, 1, 2, 3, 4, 5, 6, 7, 8 ],
54             '9-2' => [ 0, 1, 2, 3, 4, 5, 6, 7, 9 ],
55             '9-3' => [ 0, 1, 2, 3, 4, 5, 6, 8, 9 ],
56             '9-4' => [ 0, 1, 2, 3, 4, 5, 7, 8, 9 ],
57             '9-5' => [ 0, 1, 2, 3, 4, 6, 7, 8, 9 ],
58             '9-6' => [ 0, 1, 2, 3, 4, 5, 6, 8, 10 ],
59             '9-7' => [ 0, 1, 2, 3, 4, 5, 7, 8, 10 ],
60             '9-8' => [ 0, 1, 2, 3, 4, 6, 7, 8, 10 ],
61             '9-9' => [ 0, 1, 2, 3, 5, 6, 7, 8, 10 ],
62             '9-10' => [ 0, 1, 2, 3, 4, 6, 7, 9, 10 ],
63             '9-11' => [ 0, 1, 2, 3, 5, 6, 7, 9, 10 ],
64             '9-12' => [ 0, 1, 2, 4, 5, 6, 8, 9, 10 ],
65             # tetrachords (octachords)
66             '4-1' => [ 0, 1, 2, 3 ],
67             '4-2' => [ 0, 1, 2, 4 ],
68             '4-4' => [ 0, 1, 2, 5 ],
69             '4-5' => [ 0, 1, 2, 6 ],
70             '4-6' => [ 0, 1, 2, 7 ],
71             '4-3' => [ 0, 1, 3, 4 ],
72             '4-11' => [ 0, 1, 3, 5 ],
73             '4-13' => [ 0, 1, 3, 6 ],
74             '4-Z29' => [ 0, 1, 3, 7 ],
75             '4-7' => [ 0, 1, 4, 5 ],
76             '4-Z15' => [ 0, 1, 4, 6 ],
77             '4-18' => [ 0, 1, 4, 7 ],
78             '4-19' => [ 0, 1, 4, 8 ],
79             '4-8' => [ 0, 1, 5, 6 ],
80             '4-16' => [ 0, 1, 5, 7 ],
81             '4-20' => [ 0, 1, 5, 8 ],
82             '4-9' => [ 0, 1, 6, 7 ],
83             '4-10' => [ 0, 2, 3, 5 ],
84             '4-12' => [ 0, 2, 3, 6 ],
85             '4-14' => [ 0, 2, 3, 7 ],
86             '4-21' => [ 0, 2, 4, 6 ],
87             '4-22' => [ 0, 2, 4, 7 ],
88             '4-24' => [ 0, 2, 4, 8 ],
89             '4-23' => [ 0, 2, 5, 7 ],
90             '4-27' => [ 0, 2, 5, 8 ],
91             '4-25' => [ 0, 2, 6, 8 ],
92             '4-17' => [ 0, 3, 4, 7 ],
93             '4-26' => [ 0, 3, 5, 8 ],
94             '4-28' => [ 0, 3, 6, 9 ],
95             # octachords (tetrachords)
96             '8-1' => [ 0, 1, 2, 3, 4, 5, 6, 7 ],
97             '8-2' => [ 0, 1, 2, 3, 4, 5, 6, 8 ],
98             '8-4' => [ 0, 1, 2, 3, 4, 5, 7, 8 ],
99             '8-5' => [ 0, 1, 2, 3, 4, 6, 7, 8 ],
100             '8-6' => [ 0, 1, 2, 3, 5, 6, 7, 8 ],
101             '8-3' => [ 0, 1, 2, 3, 4, 5, 6, 9 ],
102             '8-11' => [ 0, 1, 2, 3, 4, 5, 7, 9 ],
103             '8-13' => [ 0, 1, 2, 3, 4, 6, 7, 9 ],
104             '8-Z29' => [ 0, 1, 2, 3, 5, 6, 7, 9 ],
105             '8-7' => [ 0, 1, 2, 3, 4, 5, 8, 9 ],
106             '8-Z15' => [ 0, 1, 2, 3, 4, 6, 8, 9 ],
107             '8-18' => [ 0, 1, 2, 3, 5, 6, 8, 9 ],
108             '8-19' => [ 0, 1, 2, 4, 5, 6, 8, 9 ],
109             '8-8' => [ 0, 1, 2, 3, 4, 7, 8, 9 ],
110             '8-16' => [ 0, 1, 2, 3, 5, 7, 8, 9 ],
111             '8-20' => [ 0, 1, 2, 4, 5, 7, 8, 9 ],
112             '8-9' => [ 0, 1, 2, 3, 6, 7, 8, 9 ],
113             '8-10' => [ 0, 2, 3, 4, 5, 6, 7, 9 ],
114             '8-12' => [ 0, 1, 3, 4, 5, 6, 7, 9 ],
115             '8-14' => [ 0, 1, 2, 4, 5, 6, 7, 9 ],
116             '8-21' => [ 0, 1, 2, 3, 4, 6, 8, 10 ],
117             '8-22' => [ 0, 1, 2, 3, 5, 6, 8, 10 ],
118             '8-24' => [ 0, 1, 2, 4, 5, 6, 8, 10 ],
119             '8-23' => [ 0, 1, 2, 3, 5, 7, 8, 10 ],
120             '8-27' => [ 0, 1, 2, 4, 5, 7, 8, 10 ],
121             '8-25' => [ 0, 1, 2, 4, 6, 7, 8, 10 ],
122             '8-17' => [ 0, 1, 3, 4, 5, 6, 8, 9 ],
123             '8-26' => [ 0, 1, 3, 4, 5, 7, 8, 10 ],
124             # '8-26' => [ 0, 1, 2, 4, 5, 7, 9, 10 ], # rahn1980
125             '8-28' => [ 0, 1, 3, 4, 6, 7, 9, 10 ],
126             # pentachords (septachords)
127             '5-1' => [ 0, 1, 2, 3, 4 ],
128             '5-2' => [ 0, 1, 2, 3, 5 ],
129             '5-4' => [ 0, 1, 2, 3, 6 ],
130             '5-5' => [ 0, 1, 2, 3, 7 ],
131             '5-3' => [ 0, 1, 2, 4, 5 ],
132             '5-9' => [ 0, 1, 2, 4, 6 ],
133             '5-Z36' => [ 0, 1, 2, 4, 7 ],
134             '5-13' => [ 0, 1, 2, 4, 8 ],
135             '5-6' => [ 0, 1, 2, 5, 6 ],
136             '5-14' => [ 0, 1, 2, 5, 7 ],
137             '5-Z38' => [ 0, 1, 2, 5, 8 ],
138             '5-7' => [ 0, 1, 2, 6, 7 ],
139             '5-15' => [ 0, 1, 2, 6, 8 ],
140             '5-10' => [ 0, 1, 3, 4, 6 ],
141             '5-16' => [ 0, 1, 3, 4, 7 ],
142             '5-Z17' => [ 0, 1, 3, 4, 8 ],
143             '5-Z12' => [ 0, 1, 3, 5, 6 ],
144             '5-24' => [ 0, 1, 3, 5, 7 ],
145             '5-27' => [ 0, 1, 3, 5, 8 ],
146             '5-19' => [ 0, 1, 3, 6, 7 ],
147             '5-29' => [ 0, 1, 3, 6, 8 ],
148             '5-31' => [ 0, 1, 3, 6, 9 ],
149             '5-Z18' => [ 0, 1, 4, 5, 7 ],
150             '5-21' => [ 0, 1, 4, 5, 8 ],
151             '5-30' => [ 0, 1, 4, 6, 8 ],
152             '5-32' => [ 0, 1, 4, 6, 9 ],
153             '5-22' => [ 0, 1, 4, 7, 8 ],
154             '5-20' => [ 0, 1, 5, 6, 8 ], # 0,1,3,7,8 forte1973
155             '5-8' => [ 0, 2, 3, 4, 6 ],
156             '5-11' => [ 0, 2, 3, 4, 7 ],
157             '5-23' => [ 0, 2, 3, 5, 7 ],
158             '5-25' => [ 0, 2, 3, 5, 8 ],
159             '5-28' => [ 0, 2, 3, 6, 8 ],
160             '5-26' => [ 0, 2, 4, 5, 8 ],
161             '5-33' => [ 0, 2, 4, 6, 8 ],
162             '5-34' => [ 0, 2, 4, 6, 9 ],
163             '5-35' => [ 0, 2, 4, 7, 9 ],
164             '5-Z37' => [ 0, 3, 4, 5, 8 ],
165             # septachords (pentachords)
166             '7-1' => [ 0, 1, 2, 3, 4, 5, 6 ],
167             '7-2' => [ 0, 1, 2, 3, 4, 5, 7 ],
168             '7-4' => [ 0, 1, 2, 3, 4, 6, 7 ],
169             '7-5' => [ 0, 1, 2, 3, 5, 6, 7 ],
170             '7-3' => [ 0, 1, 2, 3, 4, 5, 8 ],
171             '7-9' => [ 0, 1, 2, 3, 4, 6, 8 ],
172             '7-Z36' => [ 0, 1, 2, 3, 5, 6, 8 ],
173             '7-13' => [ 0, 1, 2, 4, 5, 6, 8 ],
174             '7-6' => [ 0, 1, 2, 3, 4, 7, 8 ],
175             '7-14' => [ 0, 1, 2, 3, 5, 7, 8 ],
176             '7-Z38' => [ 0, 1, 2, 4, 5, 7, 8 ],
177             '7-7' => [ 0, 1, 2, 3, 6, 7, 8 ],
178             '7-15' => [ 0, 1, 2, 4, 6, 7, 8 ],
179             '7-10' => [ 0, 1, 2, 3, 4, 6, 9 ],
180             '7-16' => [ 0, 1, 2, 3, 5, 6, 9 ],
181             '7-Z17' => [ 0, 1, 2, 4, 5, 6, 9 ],
182             '7-Z12' => [ 0, 1, 2, 3, 4, 7, 9 ],
183             '7-24' => [ 0, 1, 2, 3, 5, 7, 9 ],
184             '7-27' => [ 0, 1, 2, 4, 5, 7, 9 ],
185             '7-19' => [ 0, 1, 2, 3, 6, 7, 9 ],
186             '7-29' => [ 0, 1, 2, 4, 6, 7, 9 ],
187             '7-31' => [ 0, 1, 3, 4, 6, 7, 9 ],
188             '7-Z18' => [ 0, 1, 4, 5, 6, 7, 9 ], # 0,1,2,3,5,8,9 forte1973
189             # '7-Z18' => [ 0, 1, 2, 3, 5, 8, 9 ], # rahn1980
190             '7-21' => [ 0, 1, 2, 4, 5, 8, 9 ],
191             '7-30' => [ 0, 1, 2, 4, 6, 8, 9 ],
192             '7-32' => [ 0, 1, 3, 4, 6, 8, 9 ],
193             '7-22' => [ 0, 1, 2, 5, 6, 8, 9 ],
194             '7-20' => [ 0, 1, 2, 5, 6, 7, 9 ], # 0,1,2,4,7,8,9 forte1973
195             # '7-20' => [ 0, 1, 2, 4, 7, 8, 9 ], # rahn1980
196             '7-8' => [ 0, 2, 3, 4, 5, 6, 8 ],
197             '7-11' => [ 0, 1, 3, 4, 5, 6, 8 ],
198             '7-23' => [ 0, 2, 3, 4, 5, 7, 9 ],
199             '7-25' => [ 0, 2, 3, 4, 6, 7, 9 ],
200             '7-28' => [ 0, 1, 3, 5, 6, 7, 9 ],
201             '7-26' => [ 0, 1, 3, 4, 5, 7, 9 ],
202             '7-33' => [ 0, 1, 2, 4, 6, 8, 10 ],
203             '7-34' => [ 0, 1, 3, 4, 6, 8, 10 ],
204             '7-35' => [ 0, 1, 3, 5, 6, 8, 10 ],
205             '7-Z37' => [ 0, 1, 3, 4, 5, 7, 8 ],
206             # hexachords
207             '6-1' => [ 0, 1, 2, 3, 4, 5 ],
208             '6-2' => [ 0, 1, 2, 3, 4, 6 ],
209             '6-Z36' => [ 0, 1, 2, 3, 4, 7 ],
210             '6-Z3' => [ 0, 1, 2, 3, 4, 7 ], # 0,1,2,3,5,6 forte1973
211             '6-Z37' => [ 0, 1, 2, 3, 4, 8 ],
212             '6-Z4' => [ 0, 1, 2, 3, 4, 8 ], # 0,1,2,4,5,6 forte1973
213             '6-9' => [ 0, 1, 2, 3, 5, 7 ],
214             '6-Z40' => [ 0, 1, 2, 3, 5, 8 ],
215             '6-Z11' => [ 0, 1, 2, 3, 5, 8 ], # 0,1,2,4,5,7 forte1973
216             '6-5' => [ 0, 1, 2, 3, 6, 7 ],
217             '6-Z41' => [ 0, 1, 2, 3, 6, 8 ],
218             '6-Z12' => [ 0, 1, 2, 3, 6, 8 ], # 0,1,2,4,6,7 forte1973
219             '6-Z42' => [ 0, 1, 2, 3, 6, 9 ],
220             '6-Z13' => [ 0, 1, 2, 3, 6, 9 ], # 0,1,3,4,6,7 forte1973
221             '6-Z38' => [ 0, 1, 2, 3, 7, 8 ],
222             '6-Z6' => [ 0, 1, 2, 3, 7, 8 ], # 0,1,3,5,6,7 forte1973
223             '6-15' => [ 0, 1, 2, 4, 5, 8 ],
224             '6-22' => [ 0, 1, 2, 4, 6, 8 ],
225             '6-Z46' => [ 0, 1, 2, 4, 6, 9 ],
226             '6-Z24' => [ 0, 1, 2, 4, 6, 9 ], # 0,1,3,4,6,8 forte1973
227             '6-Z17' => [ 0, 1, 2, 4, 7, 8 ],
228             '6-Z43' => [ 0, 1, 2, 4, 7, 8 ], # 0,1,2,5,6,8 forte1973
229             '6-Z47' => [ 0, 1, 2, 4, 7, 9 ],
230             '6-Z25' => [ 0, 1, 2, 4, 7, 9 ], # 0,1,3,5,6,8 forte1973
231             '6-Z44' => [ 0, 1, 2, 5, 6, 9 ],
232             '6-Z19' => [ 0, 1, 2, 5, 6, 9 ], # 0,1,3,4,7,8 forte1973
233             '6-18' => [ 0, 1, 2, 5, 7, 8 ],
234             '6-Z48' => [ 0, 1, 2, 5, 7, 9 ],
235             '6-Z26' => [ 0, 1, 2, 5, 7, 9 ], # 0,1,3,5,7,8 forte1973
236             '6-7' => [ 0, 1, 2, 6, 7, 8 ],
237             '6-Z10' => [ 0, 1, 3, 4, 5, 7 ],
238             '6-Z39' => [ 0, 1, 3, 4, 5, 7 ], # 0,2,3,4,5,8 forte1973
239             '6-14' => [ 0, 1, 3, 4, 5, 8 ],
240             '6-27' => [ 0, 1, 3, 4, 6, 9 ],
241             '6-Z49' => [ 0, 1, 3, 4, 7, 9 ],
242             '6-Z28' => [ 0, 1, 3, 4, 7, 9 ], # 0,1,3,5,6,9 forte1973
243             '6-34' => [ 0, 1, 3, 5, 7, 9 ],
244             '6-31' => [ 0, 1, 4, 5, 7, 9 ], # 0,1,3,5,8,9 forte1973
245             '6-30' => [ 0, 1, 3, 6, 7, 9 ],
246             '6-Z29' => [ 0, 2, 3, 6, 7, 9 ], # 0,1,3,6,8,9 forte1973
247             '6-Z50' => [ 0, 2, 3, 6, 7, 9 ], # 0,1,4,6,7,9 forte1973
248             '6-16' => [ 0, 1, 4, 5, 6, 8 ],
249             '6-20' => [ 0, 1, 4, 5, 8, 9 ],
250             '6-8' => [ 0, 2, 3, 4, 5, 7 ],
251             '6-21' => [ 0, 2, 3, 4, 6, 8 ],
252             '6-Z45' => [ 0, 2, 3, 4, 6, 9 ],
253             '6-Z23' => [ 0, 2, 3, 4, 6, 9 ], # 0,2,3,5,6,8 forte1973
254             '6-33' => [ 0, 2, 3, 5, 7, 9 ],
255             '6-32' => [ 0, 2, 4, 5, 7, 9 ],
256             '6-35' => [ 0, 2, 4, 6, 8, 10 ],
257             );
258              
259             # Hexchords here are problematic on account of mutual complementary sets
260             # (different Forte Numbers for the same pitch set).
261             # TODO review and use what Rahn lists as first in table on p.142-3.
262             # TODO Rahn puts 6-Z36 and 6-Z3 together, but my code is producing
263             # two different prime forms for those...
264             #
265             # sorting is to align with the table in rahn1980
266             our %PCS2FORTE = (
267             # trichords (complement nonachords)
268             '0,1,2' => '3-1',
269             '0,1,3' => '3-2',
270             '0,1,4' => '3-3',
271             '0,1,5' => '3-4',
272             '0,1,6' => '3-5',
273             '0,2,4' => '3-6',
274             '0,2,5' => '3-7',
275             '0,2,6' => '3-8',
276             '0,2,7' => '3-9',
277             '0,3,6' => '3-10',
278             '0,3,7' => '3-11',
279             '0,4,8' => '3-12',
280             # nonachords (trichords)
281             '0,1,2,3,4,5,6,7,8' => '9-1',
282             '0,1,2,3,4,5,6,7,9' => '9-2',
283             '0,1,2,3,4,5,6,8,9' => '9-3',
284             '0,1,2,3,4,5,7,8,9' => '9-4',
285             '0,1,2,3,4,6,7,8,9' => '9-5',
286             '0,1,2,3,4,5,6,8,10' => '9-6',
287             '0,1,2,3,4,5,7,8,10' => '9-7',
288             '0,1,2,3,4,6,7,8,10' => '9-8',
289             '0,1,2,3,5,6,7,8,10' => '9-9',
290             '0,1,2,3,4,6,7,9,10' => '9-10',
291             '0,1,2,3,5,6,7,9,10' => '9-11',
292             '0,1,2,4,5,6,8,9,10' => '9-12',
293             # tetrachords (octachords)
294             '0,1,2,3' => '4-1',
295             '0,1,2,4' => '4-2',
296             '0,1,2,5' => '4-4',
297             '0,1,2,6' => '4-5',
298             '0,1,2,7' => '4-6',
299             '0,1,3,4' => '4-3',
300             '0,1,3,5' => '4-11',
301             '0,1,3,6' => '4-13',
302             '0,1,3,7' => '4-Z29',
303             '0,1,4,5' => '4-7',
304             '0,1,4,6' => '4-Z15',
305             '0,1,4,7' => '4-18',
306             '0,1,4,8' => '4-19',
307             '0,1,5,6' => '4-8',
308             '0,1,5,7' => '4-16',
309             '0,1,5,8' => '4-20',
310             '0,1,6,7' => '4-9',
311             '0,2,3,5' => '4-10',
312             '0,2,3,6' => '4-12',
313             '0,2,3,7' => '4-14',
314             '0,2,4,6' => '4-21',
315             '0,2,4,7' => '4-22',
316             '0,2,4,8' => '4-24',
317             '0,2,5,7' => '4-23',
318             '0,2,5,8' => '4-27',
319             '0,2,6,8' => '4-25',
320             '0,3,4,7' => '4-17',
321             '0,3,5,8' => '4-26',
322             '0,3,6,9' => '4-28',
323             # octachords (tetrachords)
324             '0,1,2,3,4,5,6,7' => '8-1',
325             '0,1,2,3,4,5,6,8' => '8-2',
326             '0,1,2,3,4,5,7,8' => '8-4',
327             '0,1,2,3,4,6,7,8' => '8-5',
328             '0,1,2,3,5,6,7,8' => '8-6',
329             '0,1,2,3,4,5,6,9' => '8-3',
330             '0,1,2,3,4,5,7,9' => '8-11',
331             '0,1,2,3,4,6,7,9' => '8-13',
332             '0,1,2,3,5,6,7,9' => '8-Z29',
333             '0,1,2,3,4,5,8,9' => '8-7',
334             '0,1,2,3,4,6,8,9' => '8-Z15',
335             '0,1,2,3,5,6,8,9' => '8-18',
336             '0,1,2,4,5,6,8,9' => '8-19',
337             '0,1,2,3,4,7,8,9' => '8-8',
338             '0,1,2,3,5,7,8,9' => '8-16',
339             '0,1,2,4,5,7,8,9' => '8-20',
340             '0,1,2,3,6,7,8,9' => '8-9',
341             '0,2,3,4,5,6,7,9' => '8-10',
342             '0,1,3,4,5,6,7,9' => '8-12',
343             '0,1,2,4,5,6,7,9' => '8-14',
344             '0,1,2,3,4,6,8,10' => '8-21',
345             '0,1,2,3,5,6,8,10' => '8-22',
346             '0,1,2,4,5,6,8,10' => '8-24',
347             '0,1,2,3,5,7,8,10' => '8-23',
348             '0,1,2,4,5,7,8,10' => '8-27',
349             '0,1,2,4,6,7,8,10' => '8-25',
350             '0,1,3,4,5,6,8,9' => '8-17',
351             '0,1,3,4,5,7,8,10' => '8-26', # buggy in rahn1980
352             '0,1,3,4,6,7,9,10' => '8-28',
353             # pentachords (septachords)
354             '0,1,2,3,4' => '5-1',
355             '0,1,2,3,5' => '5-2',
356             '0,1,2,3,6' => '5-4',
357             '0,1,2,3,7' => '5-5',
358             '0,1,2,4,5' => '5-3',
359             '0,1,2,4,6' => '5-9',
360             '0,1,2,4,7' => '5-Z36',
361             '0,1,2,4,8' => '5-13',
362             '0,1,2,5,6' => '5-6',
363             '0,1,2,5,7' => '5-14',
364             '0,1,2,5,8' => '5-Z38',
365             '0,1,2,6,7' => '5-7',
366             '0,1,2,6,8' => '5-15',
367             '0,1,3,4,6' => '5-10',
368             '0,1,3,4,7' => '5-16',
369             '0,1,3,4,8' => '5-Z17',
370             '0,1,3,5,6' => '5-Z12',
371             '0,1,3,5,7' => '5-24',
372             '0,1,3,5,8' => '5-27',
373             '0,1,3,6,7' => '5-19',
374             '0,1,3,6,8' => '5-29',
375             '0,1,3,6,9' => '5-31',
376             '0,1,4,5,7' => '5-Z18',
377             '0,1,4,5,8' => '5-21',
378             '0,1,4,6,8' => '5-30',
379             '0,1,4,6,9' => '5-32',
380             '0,1,4,7,8' => '5-22',
381             '0,1,5,6,8' => '5-20',
382             '0,2,3,4,6' => '5-8',
383             '0,2,3,4,7' => '5-11',
384             '0,2,3,5,7' => '5-23',
385             '0,2,3,5,8' => '5-25',
386             '0,2,3,6,8' => '5-28',
387             '0,2,4,5,8' => '5-26',
388             '0,2,4,6,8' => '5-33',
389             '0,2,4,6,9' => '5-34',
390             '0,2,4,7,9' => '5-35',
391             '0,3,4,5,8' => '5-Z37',
392             # septachords (pentachords)
393             '0,1,2,3,4,5,6' => '7-1',
394             '0,1,2,3,4,5,7' => '7-2',
395             '0,1,2,3,4,6,7' => '7-4',
396             '0,1,2,3,5,6,7' => '7-5',
397             '0,1,2,3,4,5,8' => '7-3',
398             '0,1,2,3,4,6,8' => '7-9',
399             '0,1,2,3,5,6,8' => '7-Z36',
400             '0,1,2,4,5,6,8' => '7-13',
401             '0,1,2,3,4,7,8' => '7-6',
402             '0,1,2,3,5,7,8' => '7-14',
403             '0,1,2,4,5,7,8' => '7-Z38',
404             '0,1,2,3,6,7,8' => '7-7',
405             '0,1,2,4,6,7,8' => '7-15',
406             '0,1,2,3,4,6,9' => '7-10',
407             '0,1,2,3,5,6,9' => '7-16',
408             '0,1,2,4,5,6,9' => '7-Z17',
409             '0,1,2,3,4,7,9' => '7-Z12',
410             '0,1,2,3,5,7,9' => '7-24',
411             '0,1,2,4,5,7,9' => '7-27',
412             '0,1,2,3,6,7,9' => '7-19',
413             '0,1,2,4,6,7,9' => '7-29',
414             '0,1,3,4,6,7,9' => '7-31',
415             '0,1,4,5,6,7,9' => '7-Z18', # buggy in rahn1980
416             '0,1,2,4,5,8,9' => '7-21',
417             '0,1,2,4,6,8,9' => '7-30',
418             '0,1,3,4,6,8,9' => '7-32',
419             '0,1,2,5,6,8,9' => '7-22',
420             '0,1,2,5,6,7,9' => '7-20', # buggy in rahn1980
421             '0,2,3,4,5,6,8' => '7-8',
422             '0,1,3,4,5,6,8' => '7-11',
423             '0,2,3,4,5,7,9' => '7-23',
424             '0,2,3,4,6,7,9' => '7-25',
425             '0,1,3,5,6,7,9' => '7-28',
426             '0,1,3,4,5,7,9' => '7-26',
427             '0,1,2,4,6,8,10' => '7-33',
428             '0,1,3,4,6,8,10' => '7-34',
429             '0,1,3,5,6,8,10' => '7-35',
430             '0,1,3,4,5,7,8' => '7-Z37',
431             # hexachords, by first column and then sparse 2nd column
432             '0,1,2,3,4,5' => '6-1',
433             '0,1,2,3,4,6' => '6-2',
434             '0,1,2,3,4,7' => '6-Z36',
435             '0,1,2,3,4,8' => '6-Z37',
436             '0,1,2,3,5,7' => '6-9',
437             '0,1,2,3,5,8' => '6-Z40',
438             '0,1,2,3,6,7' => '6-5',
439             '0,1,2,3,6,8' => '6-Z41',
440             '0,1,2,3,6,9' => '6-Z42',
441             '0,1,2,3,7,8' => '6-Z38',
442             '0,1,2,4,5,8' => '6-15',
443             '0,1,2,4,6,8' => '6-22',
444             '0,1,2,4,6,9' => '6-Z46',
445             '0,1,2,4,7,8' => '6-Z17',
446             '0,1,2,4,7,9' => '6-Z47',
447             '0,1,2,5,6,9' => '6-Z44',
448             '0,1,2,5,7,8' => '6-18',
449             '0,1,2,5,7,9' => '6-Z48',
450             '0,1,2,6,7,8' => '6-7',
451             '0,1,3,4,5,7' => '6-Z10',
452             '0,1,3,4,5,8' => '6-14',
453             '0,1,3,4,6,9' => '6-27',
454             '0,1,3,4,7,9' => '6-Z49',
455             '0,1,3,5,7,9' => '6-34',
456             '0,1,4,5,7,9' => '6-31',
457             '0,1,3,6,7,9' => '6-30',
458             '0,2,3,6,7,9' => '6-Z29',
459             '0,1,4,5,6,8' => '6-16',
460             '0,1,4,5,8,9' => '6-20',
461             '0,2,3,4,5,7' => '6-8',
462             '0,2,3,4,6,8' => '6-21',
463             '0,2,3,4,6,9' => '6-Z45',
464             '0,2,3,5,7,9' => '6-33',
465             '0,2,4,5,7,9' => '6-32',
466             '0,2,4,6,8,10' => '6-35',
467             '0,1,2,3,5,6' => '6-Z3',
468             '0,1,2,4,5,6' => '6-Z4',
469             '0,1,2,4,5,7' => '6-Z11',
470             '0,1,2,4,6,7' => '6-Z12',
471             '0,1,3,4,6,7' => '6-Z13',
472             '0,1,2,5,6,7' => '6-Z6',
473             '0,1,3,4,6,8' => '6-Z24',
474             '0,1,2,5,6,8' => '6-Z43',
475             '0,1,3,5,6,8' => '6-Z25',
476             '0,1,3,4,7,8' => '6-Z19',
477             '0,1,3,5,7,8' => '6-Z26',
478             '0,2,3,4,5,8' => '6-Z39',
479             '0,1,3,5,6,9' => '6-Z28',
480             '0,1,4,6,7,9' => '6-Z50',
481             '0,2,3,5,6,8' => '6-Z23',
482             );
483              
484             # NOTE may need [AB]? at end for what I call "half prime" forms, as
485             # wikipedia has switched to using that form.
486             my $FORTE_NUMBER_RE = qr/[3-9]-[zZ]?\d{1,2}/;
487              
488             ########################################################################
489             #
490             # SUBROUTINES
491              
492             # Utility method for check_melody - takes melody, a list of pitches,
493             # optionally how many notes (beyond that of pitches to audit) to check,
494             # and a code reference that will accept a selection of the melody and
495             # return something that will be tested against the list of pitches
496             # (second argument) for equality: true if match, false if not (and then
497             # a bunch of references containing what failed).
498             sub _apply_melody_rule {
499 246     246   1261 my ( $self, $melody, $check_set, $note_count, $code, $flag_sort ) = @_;
500 246   100     973 $flag_sort //= 0;
501              
502             # make equal to the set if less than the set. no high value test as
503             # loop will abort if note_count exceeds the length of melody, below.
504 246   100     920 $note_count //= 0;
505 246 100       763 $note_count = @$check_set if $note_count < @$check_set;
506              
507             # rule is too large for the melody, skip
508 246 50       605 return 1, {} if @$check_set > @$melody;
509              
510 246         617 for my $i ( 0 .. @$melody - @$check_set ) {
511 1632         5353 my @selection = @{$melody}[ $i .. $i + @$check_set - 1 ];
  1632         3138  
512              
513 1632         3234 my $sel_audit = $code->( $self, \@selection );
514 1632 100       3097 @$sel_audit = sort { $a <=> $b } @$sel_audit if $flag_sort;
  1         6  
515 1632 100       3958 if ( "@$sel_audit" eq "@$check_set" ) {
516 21         179 return 0, { index => $i, selection => \@selection };
517             }
518              
519 1611 100       3744 if ( $note_count > @$check_set ) {
520 306         718 for my $count ( @$check_set + 1 .. $note_count ) {
521 311 100       939 last if $i + $count - 1 > $#$melody;
522              
523 303         663 @selection = @{$melody}[ $i .. $i + $count - 1 ];
  303         706  
524 303         1560 my $iter = combinations( \@selection, scalar @$check_set );
525              
526 303         16727 while ( my $subsel = $iter->next ) {
527 1189         12101 $sel_audit = $code->( $self, $subsel );
528 1189 50       2475 @$sel_audit = sort { $a <=> $b } @$sel_audit if $flag_sort;
  0         0  
529 1189 100       5095 if ( "@$sel_audit" eq "@$check_set" ) {
530 77         1045 return 0, { context => \@selection, index => $i, selection => $subsel };
531             }
532             }
533             }
534             }
535             }
536              
537 148         623 return 1, {};
538             }
539              
540             # Like interval class content (ICC) but instead only calculates adjacent
541             # intervals. -- "The Geometry of Musical Rhythm", G.T. Toussaint.
542             # (Perhaps more suitable for rhythm as the adjacent intervals there are
543             # probably more audible than some harmonic between inner voices.)
544             sub adjacent_interval_content {
545 8     8 1 69 my $self = shift;
546 8 100       29 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
547              
548 8         86 my @nset = sort { $a <=> $b } uniqnum @$pset;
  56         75  
549 8 100       58 croak 'pitch set must contain at least two elements' if @nset < 2;
550              
551 7         11 my %aic;
552 7         23 for my $i ( 1 .. $#nset ) {
553 28         90 $aic{ ( $nset[$i] - $nset[ $i - 1 ] ) % $self->{_DEG_IN_SCALE} }++;
554             }
555             # and the wrap-around adjacent interval
556 7 50       23 if ( @nset > 2 ) {
557             $aic{ ( $nset[0] + $self->{_DEG_IN_SCALE} - $nset[-1] )
558 7         12 % $self->{_DEG_IN_SCALE} }++;
559             }
560              
561 7         16 my @aiv;
562 7         20 for my $ics ( 1 .. int( $self->{_DEG_IN_SCALE} / 2 ) ) {
563 56   100     126 push @aiv, $aic{$ics} || 0;
564             }
565              
566 7 100       51 return wantarray ? ( \@aiv, \%aic ) : \@aiv;
567             }
568              
569             # what bands of the Bark scale do the given frequencies belong to? there
570             # are several formula for this in the Wikipedia article, this one
571             # follows Traunmüller 1990 to avoid pulling in trig functions. not sure
572             # if one is supposed to int() or sprintf() round the result to get to an
573             # integer value
574             sub bark_scale {
575 1     1 1 573 shift;
576 1         4 map { ( 26.81 * $_ ) / ( 1960 + $_ ) - 0.53 } @_;
  1         31  
577             }
578              
579             # Utility, converts a scale_degrees-bit number into a pitch set.
580             # 7 3 0
581             # 137 -> 000010001001 -> [0,3,7]
582             sub bits2pcs {
583 1     1 1 659 my ( $self, $bs ) = @_;
584              
585 1         4 my @pset;
586 1         5 for my $p ( 0 .. $self->{_DEG_IN_SCALE} - 1 ) {
587 12 100       22 push @pset, $p if $bs & ( 1 << $p );
588             }
589 1         9 return \@pset;
590             }
591              
592             # Audits a sequence of pitches for suitability, per various checks
593             # passed in via the params hash (based on Smith-Brindle Reginald's
594             # "Serial Composition" discussion of atonal melody construction).
595             sub check_melody {
596 101     101 1 331 my $self = shift;
597 101         385 my $params = shift;
598 101 100       685 my $melody = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
599              
600 101         300 my $rules_applied = 0;
601              
602 101         328 my ( %intervals, @intervals );
603 101         444 for my $i ( 1 .. $#$melody ) {
604 1069         1613 my $ival = abs $melody->[$i] - $melody->[ $i - 1 ];
605 1069         2006 $intervals{$ival}++;
606 1069         1794 push @intervals, $ival;
607             }
608              
609 101 100       479 if ( exists $params->{dup_interval_limit} ) {
610 1         4 for my $icount ( values %intervals ) {
611 1 50       4 if ( $icount >= $params->{dup_interval_limit} ) {
612 1 50       10 return wantarray ? ( 0, "dup_interval_limit" ) : 0;
613             }
614             }
615 0         0 $rules_applied++;
616             }
617              
618 100 100       189 for my $ruleset ( @{ $params->{exclude_interval} || [] } ) {
  100         742  
619             croak "no interval set in exclude_interval rule"
620             if not exists $ruleset->{iset}
621 97 50 33     1029 or ref $ruleset->{iset} ne 'ARRAY';
622 97 50       232 next if @{ $ruleset->{iset} } > @intervals;
  97         314  
623              
624             # check (magnitude of the) intervals of the melody. code ref just
625             # returns the literal intervals to compare against what is in the
626             # iset. (other options might be to ICC the intervals, or fold them
627             # into a single register, etc. but that would take more coding.)
628             my ( $ret, $results ) = $self->_apply_melody_rule(
629             \@intervals, $ruleset->{iset}, $ruleset->{in},
630 986     986   1080 sub { [ @{ $_[1] } ] },
  986         1813  
631 97 100       2023 $ruleset->{sort} ? 1 : 0
632             );
633 97 100       635 if ( $ret != 1 ) {
634 8 50       98 return wantarray ? ( 0, "exclude_interval", $results ) : 0;
635             }
636 89         374 $rules_applied++;
637             }
638              
639 92         439 for my $ps_ref ( [qw/exclude_prime prime_form/],
640             [qw/exclude_half_prime half_prime_form/] ) {
641 96         314 my $ps_rule = $ps_ref->[0];
642 96         219 my $ps_method = $ps_ref->[1];
643              
644 96 100       199 for my $ruleset ( @{ $params->{$ps_rule} || [] } ) {
  96         500  
645             croak "no pitch set in $ps_rule rule"
646             if not exists $ruleset->{ps}
647 108 50 33     853 or ref $ruleset->{ps} ne 'ARRAY';
648              
649             # for intervals code, not necessary for pitch set operations, all of
650             # which sort the pitches as part of the calculations involved
651 108         361 delete $ruleset->{sort};
652              
653             # excludes from *any* subset for the given subset magnitudes of the
654             # parent pitch set
655 108 100       280 for my $ss_mag ( @{ $ruleset->{subsets} || [] } ) {
  108         601  
656             croak "subset must be of lesser magnitude than pitch set"
657 8 50       14 if $ss_mag >= @{ $ruleset->{ps} };
  8         34  
658 8   50     39 my $in_ss = $ruleset->{in} // 0;
659 8         22 $in_ss = @{ $ruleset->{ps} }
660 8 50       14 if $in_ss < @{ $ruleset->{ps} };
  8         32  
661             # except scale down to fit smaller subset pitch set
662 8         15 $in_ss -= @{ $ruleset->{ps} } - $ss_mag;
  8         15  
663              
664 8 100       21 next if $in_ss > @$melody;
665              
666 7         39 my $all_subpsets = $self->subsets( $ss_mag, $ruleset->{ps} );
667 7         1413 my %seen_s_pset;
668 7         14 for my $s_pset (@$all_subpsets) {
669 87         229 my $s_prime = $self->$ps_method($s_pset);
670 87 100       413 next if $seen_s_pset{"@$s_prime"}++;
671             my ( $ret, $results ) =
672             $self->_apply_melody_rule( $melody, $s_prime,
673 42     303   396 $in_ss, sub { $_[0]->$ps_method( $_[1] ) } );
  303         833  
674 42 100       397 if ( $ret != 1 ) {
675 1 50       31 return wantarray ? ( 0, $ps_rule, $results ) : 0;
676             }
677             }
678 6         59 $rules_applied++;
679             }
680              
681             my ( $ret, $results ) =
682             $self->_apply_melody_rule( $melody, $ruleset->{ps}, $ruleset->{in},
683 107     1532   852 sub { $_[0]->$ps_method( $_[1] ) } );
  1532         4310  
684 107 100       652 if ( $ret != 1 ) {
685 89 50       879 return wantarray ? ( 0, $ps_rule, $results ) : 0;
686             }
687              
688 18         72 $rules_applied++;
689             }
690             }
691              
692 2 50       76 if ( $rules_applied == 0 ) {
693 0 0       0 return wantarray ? ( 0, "no rules applied" ) : 0;
694             }
695 2 50       28 return wantarray ? ( 1, "ok" ) : 1;
696             }
697              
698             sub circular_permute {
699 4248     4248 1 5590 my $self = shift;
700 4248 100       7157 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
701 4248 100       6428 croak 'pitch set must contain something' if !@$pset;
702              
703 4247         4872 my @perms;
704 4247         8029 for my $i ( 0 .. $#$pset ) {
705 16344         21166 for my $j ( 0 .. $#$pset ) {
706 69894         101972 $perms[$i][$j] = $pset->[ ( $i + $j ) % @$pset ];
707             }
708             }
709 4247         7037 return \@perms;
710             }
711              
712             sub complement {
713 2     2 1 5 my $self = shift;
714 2 100       9 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
715              
716 2         4 my %seen;
717 2         11 @seen{@$pset} = ();
718 2         8 return [ grep { !exists $seen{$_} } 0 .. $self->{_DEG_IN_SCALE} - 1 ];
  24         51  
719             }
720              
721 1     1 1 11 sub fnums { \%FORTE2PCS }
722              
723             sub forte_number_re {
724 1     1 1 18 return $FORTE_NUMBER_RE;
725             }
726              
727             sub forte2pcs {
728 2     2 1 8 my ( $self, $forte_number ) = @_;
729 2         14 return $FORTE2PCS{ uc $forte_number };
730             }
731              
732             # simple wrapper around check_melody to create something to work with,
733             # depending on the params.
734             sub gen_melody {
735 2     2 1 8 my ( $self, %params ) = @_;
736              
737 2         5 my $attempts = 1000; # enough for Helen, enough for us
738 2   100     12 my $max_interval = $params{melody_max_interval} || 16; # tessitura of a 10th
739 2         4 delete $params{melody_max_interval};
740              
741 2 50       8 if ( !keys %params ) {
742             # based on Reginald's ideas (insofar as those can be represented by
743             # the rules system I've cobbled together)
744 2         42 %params = (
745             exclude_half_prime => [
746             { ps => [ 0, 4, 5 ] }, # leading tone/tonic/dominant
747             ],
748             exclude_interval => [
749             { iset => [ 5, 5 ], }, # adjacent fourths ("cadential basses")
750             ],
751             exclude_prime => [
752             { ps => [ 0, 3, 7 ], in => 4 }, # major or minor triad, any guise
753             { ps => [ 0, 2, 5, 8 ], }, # 7th, any guise, exact
754             { ps => [ 0, 2, 4, 6 ], in => 5 }, # whole tone formation
755             # 7-35 (major/minor scale) but also excluding from all 5-x or
756             # 6-x subsets of said set
757             { ps => [ 0, 1, 3, 5, 6, 8, 10 ], subsets => [ 6, 5 ] },
758             ],
759             );
760             }
761              
762 2         11 my $got_melody = 0;
763 2         4 my @melody;
764 2         4 eval {
765 2         8 ATTEMPT: while ( $attempts-- > 0 ) {
766 94         201 my %seen;
767 94         302 my @pitches = 0 .. $self->{_DEG_IN_SCALE} - 1;
768 94         639 @melody = splice @pitches, rand @pitches, 1;
769 94         367 $seen{ $melody[0] } = 1;
770 94         157 my $melody_low = $melody[0];
771 94         126 my $melody_high = $melody[0];
772              
773 94         230 while (@pitches) {
774             my @potential = grep {
775 1034         2093 my $base_pitch = $_ % 12;
  18558         21168  
776 18558         19041 my $ret = 0;
777 18558         20974 for my $p (@pitches) {
778 86474 100       117896 if ( $base_pitch == $p ) { $ret = 1; last }
  9515         9818  
  9515         9902  
779             }
780             $ret
781 18558         23240 } $melody_high - $max_interval .. $melody_low + $max_interval;
782 1034         2288 my $choice = $potential[ rand @potential ];
783 1034         1399 my $base_choice = $choice % 12;
784 1034         2230 @pitches = grep $_ != $base_choice, @pitches;
785 1034         1291 push @melody, $choice;
786              
787 1034 100       1546 $melody_low = $choice if $choice < $melody_low;
788 1034 100       2397 $melody_high = $choice if $choice > $melody_high;
789             }
790              
791             # but negative pitches are awkward for various reasons
792 94 100       274 if ( $melody_low < 0 ) {
793 51         123 $melody_low = abs $melody_low;
794 51         204 $_ += $melody_low for @melody;
795             }
796              
797 94         947 ( $got_melody, my $msg ) = $self->check_melody( \%params, \@melody );
798 94 100       893 next ATTEMPT if $got_melody != 1;
799              
800 2         11 last;
801             }
802             };
803 2 50       5 croak $@ if $@;
804 2 50       5 croak "could not generate a melody" unless $got_melody;
805              
806 2         49 return \@melody;
807             }
808              
809             # copied from Music::NeoRiemannianTonnetz 'normalize', see perldocs
810             # for differences between this and prime_form and normal_form
811             sub half_prime_form {
812 30     30 1 92 my $self = shift;
813 30 100       66 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
814              
815 30 100       83 croak 'pitch set must contain something' if !@$pset;
816              
817 29         35 my %origmap;
818 29         44 for my $p (@$pset) {
819 87         94 push @{ $origmap{ $p % $self->{_DEG_IN_SCALE} } }, $p;
  87         189  
820             }
821 29 50       58 if ( keys %origmap == 1 ) {
822 0 0       0 return wantarray ? ( keys %origmap, \%origmap ) : keys %origmap;
823             }
824 29         80 my @nset = sort { $a <=> $b } keys %origmap;
  80         135  
825              
826 29         40 my @equivs;
827 29         55 for my $i ( 0 .. $#nset ) {
828 87         126 for my $j ( 0 .. $#nset ) {
829 261         441 $equivs[$i][$j] = $nset[ ( $i + $j ) % @nset ];
830             }
831             }
832 29         54 my @order = reverse 1 .. $#nset;
833              
834 29         37 my @normal;
835 29         43 for my $i (@order) {
836 29         37 my $min_span = $self->{_DEG_IN_SCALE};
837 29         31 my @min_span_idx;
838              
839 29         44 for my $eidx ( 0 .. $#equivs ) {
840             my $span =
841 87         108 ( $equivs[$eidx][$i] - $equivs[$eidx][0] ) % $self->{_DEG_IN_SCALE};
842 87 100       154 if ( $span < $min_span ) {
    100          
843 44         47 $min_span = $span;
844 44         68 @min_span_idx = $eidx;
845             } elsif ( $span == $min_span ) {
846 1         3 push @min_span_idx, $eidx;
847             }
848             }
849              
850 29 50       45 if ( @min_span_idx == 1 ) {
851 29         44 @normal = @{ $equivs[ $min_span_idx[0] ] };
  29         56  
852 29         44 last;
853             } else {
854 0         0 @equivs = @equivs[@min_span_idx];
855             }
856             }
857              
858 29 50       50 if ( !@normal ) {
859             # nothing unique, pick lowest starting pitch, which is first index
860             # by virtue of the numeric sort performed above.
861 0         0 @normal = @{ $equivs[0] };
  0         0  
862             }
863              
864             # but must map (and anything else not ) so b is 0,
865             # dis 4, etc. and also update the original pitch mapping - this is
866             # the major addition to the otherwise stock normal_form code.
867 29 100       53 if ( $normal[0] != 0 ) {
868 28         40 my $trans = $self->{_DEG_IN_SCALE} - $normal[0];
869 28         31 my %newmap;
870 28         39 for my $i (@normal) {
871 84         89 my $prev = $i;
872 84         97 $i = ( $i + $trans ) % $self->{_DEG_IN_SCALE};
873 84         137 $newmap{$i} = $origmap{$prev};
874             }
875 28         91 %origmap = %newmap;
876             }
877              
878 29 100       155 return wantarray ? ( \@normal, \%origmap ) : \@normal;
879             }
880              
881             sub interval_class_content {
882 7     7 1 62 my $self = shift;
883 7 100       26 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
884              
885 7         137 my @nset = sort { $a <=> $b } uniqnum @$pset;
  34         54  
886 7 100       44 croak 'pitch set must contain at least two elements' if @nset < 2;
887              
888 6         10 my %icc;
889 6         19 for my $i ( 1 .. $#nset ) {
890 19         33 for my $j ( 0 .. $i - 1 ) {
891             $icc{
892             $self->pitch2intervalclass(
893             ( $nset[$i] - $nset[$j] ) % $self->{_DEG_IN_SCALE}
894             )
895 49         94 }++;
896             }
897             }
898              
899 6         14 my @icv;
900 6         13 for my $ics ( 1 .. int( $self->{_DEG_IN_SCALE} / 2 ) ) {
901 36   100     83 push @icv, $icc{$ics} || 0;
902             }
903              
904 6 100       87 return wantarray ? ( \@icv, \%icc ) : \@icv;
905             }
906              
907             sub intervals2pcs {
908 4     4 1 51 my $self = shift;
909 4         6 my $start_pitch = shift;
910 4 100       15 my $iset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
911 4 100       31 croak 'interval set must contain something' if !@$iset;
912              
913 3   100     15 $start_pitch //= 0;
914 3         5 $start_pitch = int $start_pitch;
915              
916 3         8 my @pset = $start_pitch;
917 3         8 for my $i (@$iset) {
918 13         30 push @pset, ( $pset[-1] + $i ) % $self->{_DEG_IN_SCALE};
919             }
920              
921 3         54 return \@pset;
922             }
923              
924             sub invariance_matrix {
925 3     3 1 55 my $self = shift;
926 3 100       18 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
927 3 100       32 croak 'pitch set must contain something' if !@$pset;
928              
929 2         4 my @ivm;
930 2         8 for my $i ( 0 .. $#$pset ) {
931 8         14 for my $j ( 0 .. $#$pset ) {
932 32         52 $ivm[$i][$j] = ( $pset->[$i] + $pset->[$j] ) % $self->{_DEG_IN_SCALE};
933             }
934             }
935              
936 2         16 return \@ivm;
937             }
938              
939             sub invert {
940 2141     2141 1 2869 my $self = shift;
941 2141         2682 my $axis = shift;
942 2141 100       4017 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
943 2141 100       3641 croak 'pitch set must contain something' if !@$pset;
944              
945 2140   100     3228 $axis //= 0;
946 2140         2398 $axis = int $axis;
947              
948 2140         3802 my @inverse = @$pset;
949 2140         3164 for my $p (@inverse) {
950 8249         10515 $p = ( $axis - $p ) % $self->{_DEG_IN_SCALE};
951             }
952              
953 2140         4211 return \@inverse;
954             }
955              
956             # Utility routine to get the last few elements of a list (but never more
957             # than the whole list, etc).
958             sub lastn {
959 7     7 1 121 my ( $self, $pset, $n ) = @_;
960 7 100 100     49 croak 'cannot get elements of nothing'
961             if !defined $pset
962             or ref $pset ne 'ARRAY';
963              
964 5 100       17 return unless @$pset;
965              
966 4   66     13 $n //= $self->{_lastn};
967 4 100       32 croak 'n of lastn must be number' unless looks_like_number $n;
968              
969 3         4 my $len = @$pset;
970 3 100       9 $len = $n if $len > $n;
971 3         6 $len *= -1;
972 3         8 return @{$pset}[ $len .. -1 ];
  3         21  
973             }
974              
975             sub mininterval {
976 9     9 1 95 my ( $self, $from, $to ) = @_;
977 9         12 my $dir = 1;
978              
979 9 100       50 croak 'from pitch must be a number' unless looks_like_number $from;
980 8 100       24 croak 'to pitch must be a number' unless looks_like_number $to;
981              
982 7         13 $from %= $self->{_DEG_IN_SCALE};
983 7         14 $to %= $self->{_DEG_IN_SCALE};
984              
985 7 100       93 if ( $from > $to ) {
986 3         6 ( $from, $to ) = ( $to, $from );
987 3         5 $dir = -1;
988             }
989 7         42 my $interval = $to - $from;
990 7 100       22 if ( $interval > $self->{_DEG_IN_SCALE} / 2 ) {
991 4         5 $dir *= -1;
992 4         7 $from += $self->{_DEG_IN_SCALE};
993 4         6 $interval = $from - $to;
994             }
995              
996 7         32 return $interval * $dir;
997             }
998              
999             sub multiply {
1000 3     3 1 46 my $self = shift;
1001 3         5 my $factor = shift;
1002 3 100       11 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1003 3 100       25 croak 'pitch set must contain something' if !@$pset;
1004              
1005 2   100     8 $factor //= 1;
1006 2         4 $factor = int $factor;
1007              
1008 2         6 return [ map { my $p = $_ * $factor % $self->{_DEG_IN_SCALE}; $p } @$pset ];
  8         13  
  8         25  
1009             }
1010              
1011             # Utility methods for get/check/reset of each element in turn of a given
1012             # array reference, with wrap-around. Handy if pulling sequential
1013             # elements off a list, but have much code between the successive calls.
1014             {
1015             my %seen;
1016              
1017             # get the iterator value for a ref
1018             sub geti {
1019 4     4 1 79 my ( $self, $ref ) = @_;
1020 4 100 100     47 croak 'need an array reference' if !defined $ref or ref $ref ne 'ARRAY';
1021 2   100     40 return $seen{ refaddr $ref} || 0;
1022             }
1023              
1024             # grabi(42, \@array) obtains 42 elements from array, looping to
1025             # fill if necessary
1026             sub grabi {
1027 8     8 1 123 my ( $self, $count, $ref ) = @_;
1028 8 100 100     57 croak 'need an array reference' if !defined $ref or ref $ref ne 'ARRAY';
1029 6 100 100     51 croak 'count must be non-negative integer'
1030             if !looks_like_number($count)
1031             or $count < 0;
1032 4 100 100     24 return if @$ref == 0 or $count == 0;
1033 2   100     23 $seen{ refaddr $ref} ||= 0;
1034 2         3 my @results;
1035 2         6 while ( $count > 0 ) {
1036 8         60 push @results, $ref->[ $seen{ refaddr $ref} ];
1037 8         21 $seen{ refaddr $ref } = ( $seen{ refaddr $ref } + 1 ) % @$ref;
1038 8         13 $count--;
1039             }
1040 2         18 return @results;
1041             }
1042              
1043             # nexti(\@array) - returns subsequent elements of array on each
1044             # successive call
1045             sub nexti {
1046 5     5 1 94 my ( $self, $ref ) = @_;
1047 5 100 100     57 croak 'need an array reference' if !defined $ref or ref $ref ne 'ARRAY';
1048 3   100     23 $seen{ refaddr $ref} ||= 0;
1049 3         17 $seen{ refaddr $ref } = ( $seen{ refaddr $ref } + 1 ) % @$ref;
1050 3         14 $ref->[ $seen{ refaddr $ref} ];
1051             }
1052              
1053             # reseti(\@array) - resets counter
1054             sub reseti {
1055 3     3 1 91 my ( $self, $ref ) = @_;
1056 3 100 100     129 croak 'need an array reference' if !defined $ref or ref $ref ne 'ARRAY';
1057 1         8 $seen{ refaddr $ref} = 0;
1058             }
1059              
1060             # set the iterator for a ref
1061             sub seti {
1062 4     4 1 90 my ( $self, $ref, $i ) = @_;
1063 4 100 100     38 croak 'need an array reference' if !defined $ref or ref $ref ne 'ARRAY';
1064 2 100       22 croak 'iterator must be number'
1065             unless looks_like_number($i);
1066 1         6 $seen{ refaddr $ref} = $i;
1067             }
1068              
1069             # returns current element, but does not advance pointer
1070             sub whati {
1071 4     4 1 64 my ( $self, $ref ) = @_;
1072 4 100 100     37 croak 'need an array reference' if !defined $ref or ref $ref ne 'ARRAY';
1073 2   100     15 $seen{ refaddr $ref} ||= 0;
1074 2         15 $ref->[ $seen{ refaddr $ref} % @$ref ];
1075             }
1076             }
1077              
1078             sub new {
1079 7     7 1 670 my ( $class, %param ) = @_;
1080 7         22 my $self = {};
1081              
1082 7   66     46 $self->{_DEG_IN_SCALE} = int( $param{DEG_IN_SCALE} // $DEG_IN_SCALE );
1083 7 100       26 if ( $self->{_DEG_IN_SCALE} < 2 ) {
1084 1         18 croak 'degrees in scale must be greater than one';
1085             }
1086              
1087 6 100       24 if ( exists $param{lastn} ) {
1088             croak 'lastn must be number'
1089 2 100       18 unless looks_like_number $param{lastn};
1090 1         5 $self->{_lastn} = $param{lastn};
1091             } else {
1092 4         11 $self->{_lastn} = 2;
1093             }
1094              
1095             # XXX packing not implemented beyond "right" method (via www.mta.ca docs)
1096 5         14 $self->{_packing} = 'right'; # $param{PACKING} // 'right';
1097              
1098 5         8 bless $self, $class;
1099 5         48 return $self;
1100             }
1101              
1102             sub normal_form {
1103 4246     4246 1 5284 my $self = shift;
1104 4246 100       6331 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1105              
1106 4246 100       6788 croak 'pitch set must contain something' if !@$pset;
1107              
1108 4245         5288 my %origmap;
1109 4245         6068 for my $p (@$pset) {
1110 16347         17870 push @{ $origmap{ $p % $self->{_DEG_IN_SCALE} } }, $p;
  16347         33752  
1111             }
1112 4245 50       8304 if ( keys %origmap == 1 ) {
1113 0 0       0 return wantarray ? ( [ keys %origmap ], \%origmap ) : [ keys %origmap ];
1114             }
1115 4245         12062 my @nset = sort { $a <=> $b } keys %origmap;
  19898         29805  
1116              
1117 4245         9176 my $equivs = $self->circular_permute( \@nset );
1118 4245         7669 my @order = 1 .. $#nset;
1119             # NOTE this only performs 'right' packing, see commits
1120             # 9f0c33f8260af9584d38c92af4e7a6a39f7e2769 and prior for long since
1121             # unimplemented notes on this topic
1122 4245         5634 @order = reverse @order;
1123              
1124 4245         5086 my @normal;
1125 4245         5575 for my $i (@order) {
1126 4786         6132 my $min_span = $self->{_DEG_IN_SCALE};
1127 4786         5036 my @min_span_idx;
1128              
1129 4786         7191 for my $eidx ( 0 .. $#$equivs ) {
1130             my $span =
1131 17613         23201 ( $equivs->[$eidx][$i] - $equivs->[$eidx][0] ) % $self->{_DEG_IN_SCALE};
1132 17613 100       28881 if ( $span < $min_span ) {
    100          
1133 7236         7504 $min_span = $span;
1134 7236         10258 @min_span_idx = $eidx;
1135             } elsif ( $span == $min_span ) {
1136 1143         1764 push @min_span_idx, $eidx;
1137             }
1138             }
1139              
1140 4786 100       7637 if ( @min_span_idx == 1 ) {
1141 4204         5534 @normal = @{ $equivs->[ $min_span_idx[0] ] };
  4204         7936  
1142 4204         6388 last;
1143             } else {
1144 582         686 @$equivs = @{$equivs}[@min_span_idx];
  582         1472  
1145             }
1146             }
1147              
1148 4245 100       7300 if ( !@normal ) {
1149             # nothing unique, pick lowest starting pitch, which is first index
1150             # by virtue of the numeric sort performed above.
1151 41         57 @normal = @{ $equivs->[0] };
  41         89  
1152             }
1153              
1154 4245         9553 $_ += 0 for @normal; # KLUGE avoid Test::Differences seeing '4' vs. 4
1155              
1156 4245 100       16278 return wantarray ? ( \@normal, \%origmap ) : \@normal;
1157             }
1158              
1159             # Utility, converts a pitch set into a scale_degrees-bit number:
1160             # 7 3 0
1161             # [0,3,7] -> 000010001001 -> 137
1162             sub pcs2bits {
1163 4     4 1 52 my $self = shift;
1164 4 100       15 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1165              
1166 4 100       29 croak 'pitch set must contain something' if !@$pset;
1167              
1168 3         6 my $bs = 0;
1169 3         15 for my $p ( map $_ % $self->{_DEG_IN_SCALE}, @$pset ) {
1170 9         16 $bs |= 1 << $p;
1171             }
1172 3         16 return $bs;
1173             }
1174              
1175             sub pcs2forte {
1176 6     6 1 60 my $self = shift;
1177 6 100       56 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1178              
1179 6 100       36 croak 'pitch set must contain something' if !@$pset;
1180              
1181 5         8 return $PCS2FORTE{ join ',', @{ $self->prime_form($pset) } };
  5         14  
1182             }
1183              
1184             sub pcs2intervals {
1185 1     1 1 27 my $self = shift;
1186 1 50       7 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1187              
1188 1 50       5 croak 'pitch set must contain at least two elements' if @$pset < 2;
1189              
1190 1         2 my @intervals;
1191 1         2 for my $i ( 1 .. $#{$pset} ) {
  1         4  
1192 2         7 push @intervals, $pset->[$i] - $pset->[ $i - 1 ];
1193             }
1194              
1195 1         10 return \@intervals;
1196             }
1197              
1198             sub pcs2str {
1199 4     4 1 51 my $self = shift;
1200 4 100       30 croak 'must supply a pitch set' if !defined $_[0];
1201              
1202 3         4 my $str;
1203 3 100       17 if ( ref $_[0] eq 'ARRAY' ) {
    100          
1204 1         2 $str = '[' . join( ',', @{ $_[0] } ) . ']';
  1         5  
1205             } elsif ( $_[0] =~ m/,/ ) {
1206 1         5 $str = '[' . $_[0] . ']';
1207             } else {
1208 1         5 $str = '[' . join( ',', @_ ) . ']';
1209             }
1210 3         15 return $str;
1211             }
1212              
1213             sub pitch2intervalclass {
1214 56     56 1 87 my ( $self, $pitch ) = @_;
1215              
1216             # ensure member of the tone system, otherwise strange results
1217 56         114 $pitch %= $self->{_DEG_IN_SCALE};
1218              
1219             return $pitch > int( $self->{_DEG_IN_SCALE} / 2 )
1220 56 100       179 ? $self->{_DEG_IN_SCALE} - $pitch
1221             : $pitch;
1222             }
1223              
1224             # XXX tracking of original pitches would be nice, though complicated, as
1225             # ->invert would need to be modified or a non-modulating version used
1226             sub prime_form {
1227 2121     2121 1 326507 my $self = shift;
1228 2121 100       4170 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1229              
1230 2121 100       3574 croak 'pitch set must contain something' if !@$pset;
1231              
1232 2120         4131 my @forms = scalar $self->normal_form($pset);
1233 2120         4763 push @forms, scalar $self->normal_form( $self->invert( 0, $forms[0] ) );
1234              
1235 2120         4098 for my $set (@forms) {
1236 4240 100       10557 $set = $self->transpose( $self->{_DEG_IN_SCALE} - $set->[0], $set )
1237             if $set->[0] != 0;
1238             }
1239              
1240 2120         2454 my @prime;
1241 2120 100       2609 if ( "@{$forms[0]}" eq "@{$forms[1]}" ) {
  2120         4496  
  2120         4546  
1242 600         969 @prime = @{ $forms[0] };
  600         1370  
1243             } else {
1244             # look for most compact to the left
1245 1520         2090 my @sums = ( 0, 0 );
1246             PITCH:
1247 1520         2615 for my $i ( 0 .. $#$pset ) {
1248 3223         4170 for my $j ( 0 .. 1 ) {
1249 6446         8372 $sums[$j] += $forms[$j][$i];
1250             }
1251 3223 100       6098 if ( $sums[0] < $sums[1] ) {
    100          
1252 751         936 @prime = @{ $forms[0] };
  751         1904  
1253 751         1428 last PITCH;
1254             } elsif ( $sums[0] > $sums[1] ) {
1255 769         988 @prime = @{ $forms[1] };
  769         1533  
1256 769         1273 last PITCH;
1257             }
1258             }
1259             }
1260              
1261 2120         5428 return \@prime;
1262             }
1263              
1264             # Utility, "mirrors" a pitch to be within supplied min/max values as
1265             # appropriate for how many times the pitch "reflects" back within those
1266             # limits, which will depend on which limit is broken and by how much.
1267             sub reflect_pitch {
1268 25     25 1 4733 my ( $self, $v, $min, $max ) = @_;
1269 25 100       99 croak 'pitch must be a number' if !looks_like_number $v;
1270 24 100 100     113 croak 'limits must be numbers and min less than max'
      100        
1271             if !looks_like_number $min
1272             or !looks_like_number $max
1273             or $min >= $max;
1274 21 100 100     58 return $v if $v <= $max and $v >= $min;
1275              
1276 17         20 my ( @origins, $overshoot, $direction );
1277 17 100       27 if ( $v > $max ) {
1278 5         7 @origins = ( $max, $min );
1279 5         7 $overshoot = abs( $v - $max );
1280 5         6 $direction = -1;
1281             } else {
1282 12         16 @origins = ( $min, $max );
1283 12         28 $overshoot = abs( $min - $v );
1284 12         15 $direction = 1;
1285             }
1286 17         20 my $range = abs( $max - $min );
1287 17         28 my $register = int( $overshoot / $range );
1288 17 100       35 if ( $register % 2 == 1 ) {
1289 9         11 @origins = reverse @origins;
1290 9         11 $direction *= -1;
1291             }
1292 17         20 my $remainder = $overshoot % $range;
1293              
1294 17         42 return $origins[0] + $remainder * $direction;
1295             }
1296              
1297             sub retrograde {
1298 3     3 1 47 my $self = shift;
1299 3 100       12 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1300              
1301 3 100       26 croak 'pitch set must contain something' if !@$pset;
1302              
1303 2         13 return [ reverse @$pset ];
1304             }
1305              
1306             sub rotate {
1307 10     10 1 113 my $self = shift;
1308 10         16 my $r = shift;
1309 10 100       30 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1310              
1311 10 100 100     110 croak 'rotate value must be integer'
1312             if !defined $r
1313             or $r !~ /^-?\d+$/;
1314 8 100       26 croak 'pitch set must contain something' if !@$pset;
1315              
1316 7         11 my @rot;
1317 7 100       18 if ( $r == 0 ) {
1318 1         3 @rot = @$pset;
1319             } else {
1320 6         19 for my $i ( 0 .. $#$pset ) {
1321 30         54 $rot[$i] = $pset->[ ( $i - $r ) % @$pset ];
1322             }
1323             }
1324              
1325 7         46 return \@rot;
1326             }
1327              
1328             # Utility method to rotate a list to a named element (for example "gis"
1329             # in a list of note names, see my etude no.2 for results of heavy use of
1330             # such rotations).
1331             sub rotateto {
1332 6     6 1 141 my $self = shift;
1333 6         10 my $what = shift;
1334 6         12 my $dir = shift;
1335 6 100       20 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1336              
1337 6 100       79 croak 'nothing to search on' unless defined $what;
1338 5 100       21 croak 'nothing to rotate on' if !@$pset;
1339              
1340 4         15 my @idx = 0 .. $#$pset;
1341              
1342 4   100     14 $dir //= 1;
1343 4 100       12 @idx = reverse @idx if $dir < 0;
1344              
1345 4         8 for my $i (@idx) {
1346 12 100       23 next unless $pset->[$i] eq $what;
1347 3         13 return $self->rotate( -$i, $pset );
1348             }
1349 1         13 croak "no such element $what";
1350             }
1351              
1352             # XXX probably should disallow changing this on the fly, esp. if allow
1353             # method chaining, as it could throw off results in wacky ways.
1354             sub scale_degrees {
1355 9     9 1 1471 my ( $self, $dis ) = @_;
1356 9 100       33 if ( defined $dis ) {
1357 4 100 100     64 croak 'scale degrees value must be positive integer greater than 1'
1358             if $dis !~ /^\d+$/
1359             or $dis < 2;
1360 2         5 $self->{_DEG_IN_SCALE} = $dis;
1361             }
1362 7         30 return $self->{_DEG_IN_SCALE};
1363             }
1364              
1365             sub set_complex {
1366 3     3 1 59 my $self = shift;
1367 3 100       17 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1368              
1369 3 100       30 croak 'pitch set must contain something' if !@$pset;
1370              
1371 2         10 my $iset = $self->invert( 0, $pset );
1372 2         8 my $dis = $self->scale_degrees;
1373              
1374 2         6 my @plex = $pset;
1375 2         9 for my $i ( 1 .. $#$pset ) {
1376 22         31 for my $j ( 0 .. $#$pset ) {
1377 264 100       319 if ( $j == 0 ) {
1378 22         58 $plex[$i][0] = $iset->[$i];
1379             } else {
1380 242         317 $plex[$i][$j] = ( $pset->[$j] + $iset->[$i] ) % $dis;
1381             }
1382             }
1383             }
1384              
1385 2         55 return \@plex;
1386             }
1387              
1388             sub subsets {
1389 14     14 1 159 my $self = shift;
1390 14         23 my $len = shift;
1391 14 100       56 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1392              
1393 14         37 my @nset = uniqnum map { $_ % $self->{_DEG_IN_SCALE} } @$pset;
  67         266  
1394 14 100       81 croak 'pitch set must contain two or more unique pitches' if @nset < 2;
1395              
1396 13 100       33 if ( defined $len ) {
1397 12 100 100     76 croak 'length must be less than size of pitch set (but not zero)'
1398             if $len >= @nset
1399             or $len == 0;
1400 10 100       33 if ( $len < 0 ) {
1401 1         3 $len = @nset + $len;
1402 1 50       11 croak 'negative length exceeds magnitude of pitch set' if $len < 1;
1403             }
1404             } else {
1405 1         3 $len = @nset - 1;
1406             }
1407              
1408 10         67 return [ combinations( \@nset, $len ) ];
1409             }
1410              
1411             sub tcis {
1412 1     1 1 4 my $self = shift;
1413 1 50       6 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1414              
1415 1 50       6 croak 'pitch set must contain something' if !@$pset;
1416              
1417 1         3 my %seen;
1418 1         7 @seen{@$pset} = ();
1419              
1420 1         3 my @tcis;
1421 1         5 for my $i ( 0 .. $self->{_DEG_IN_SCALE} - 1 ) {
1422 12         17 $tcis[$i] = 0;
1423 12         12 for my $p ( @{ $self->transpose_invert( $i, 0, $pset ) } ) {
  12         23  
1424 48 100       81 $tcis[$i]++ if exists $seen{$p};
1425             }
1426             }
1427 1         11 return \@tcis;
1428             }
1429              
1430             sub tcs {
1431 2     2 1 51 my $self = shift;
1432 2 50       10 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1433              
1434 2 100       28 croak 'pitch set must contain something' if !@$pset;
1435              
1436 1         3 my %seen;
1437 1         5 @seen{@$pset} = ();
1438              
1439 1         5 my @tcs = scalar @$pset;
1440 1         6 for my $i ( 1 .. $self->{_DEG_IN_SCALE} - 1 ) {
1441 11         15 $tcs[$i] = 0;
1442 11         14 for my $p ( @{ $self->transpose( $i, $pset ) } ) {
  11         18  
1443 44 100       75 $tcs[$i]++ if exists $seen{$p};
1444             }
1445             }
1446 1         7 return \@tcs;
1447             }
1448              
1449             sub transpose {
1450 3631     3631 1 4602 my $self = shift;
1451 3631         3949 my $t = shift;
1452 3631 100       6173 my @tset = ref $_[0] eq 'ARRAY' ? @{ $_[0] } : @_;
  3628         6456  
1453              
1454 3631 100       6442 croak 'transpose value not set' if !defined $t;
1455 3630 100       5358 croak 'pitch set must contain something' if !@tset;
1456              
1457 3629         4561 $t = int $t;
1458 3629         4796 for my $p (@tset) {
1459 13641         17366 $p = ( $p + $t ) % $self->{_DEG_IN_SCALE};
1460             }
1461 3629         7412 return \@tset;
1462             }
1463              
1464             sub transpose_invert {
1465 17     17 1 116 my $self = shift;
1466 17         23 my $t = shift;
1467 17         20 my $axis = shift;
1468 17 100       40 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1469              
1470 17 100       51 croak 'transpose value not set' if !defined $t;
1471 16 100       34 croak 'pitch set must contain something' if !@$pset;
1472              
1473 15   100     26 $axis //= 0;
1474 15         32 my $tset = $self->invert( $axis, $pset );
1475              
1476 15         21 $t = int $t;
1477 15         23 for my $p (@$tset) {
1478 58         72 $p = ( $p + $t ) % $self->{_DEG_IN_SCALE};
1479             }
1480 15         35 return $tset;
1481             }
1482              
1483             sub variances {
1484 6     6 1 137 my ( $self, $pset1, $pset2 ) = @_;
1485              
1486 6 100       51 croak 'pitch set must be array ref' unless ref $pset1 eq 'ARRAY';
1487 5 100       19 croak 'pitch set must contain something' if !@$pset1;
1488 4 100       19 croak 'pitch set must be array ref' unless ref $pset2 eq 'ARRAY';
1489 3 100       18 croak 'pitch set must contain something' if !@$pset2;
1490              
1491 2         5 my ( @union, @intersection, @difference, %count );
1492 2         6 for my $p ( @$pset1, @$pset2 ) {
1493 16         31 $count{$p}++;
1494             }
1495 2         15 for my $p ( sort { $a <=> $b } keys %count ) {
  17         26  
1496 12         20 push @union, $p;
1497 12 100       11 push @{ $count{$p} > 1 ? \@intersection : \@difference }, $p;
  12         27  
1498             }
1499 2 100       18 return wantarray ? ( \@intersection, \@difference, \@union ) : \@intersection;
1500             }
1501              
1502             sub zrelation {
1503 6     6 1 137 my ( $self, $pset1, $pset2 ) = @_;
1504              
1505 6 100       29 croak 'pitch set must be array ref' unless ref $pset1 eq 'ARRAY';
1506 5 100       18 croak 'pitch set must contain something' if !@$pset1;
1507 4 100       20 croak 'pitch set must be array ref' unless ref $pset2 eq 'ARRAY';
1508 3 100       18 croak 'pitch set must contain something' if !@$pset2;
1509              
1510 2         3 my @ic_vecs;
1511 2         3 for my $ps ( $pset1, $pset2 ) {
1512 4         14 push @ic_vecs, scalar $self->interval_class_content($ps);
1513             }
1514 2 100       4 return ( "@{$ic_vecs[0]}" eq "@{$ic_vecs[1]}" ) ? 1 : 0;
  2         10  
  2         21  
1515             }
1516              
1517             1;
1518             __END__