File Coverage

blib/lib/Music/AtonalUtil.pm
Criterion Covered Total %
statement 512 576 88.8
branch 189 318 59.4
condition 35 66 53.0
subroutine 51 54 94.4
pod 43 43 100.0
total 830 1057 78.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 4     4   169489 use 5.010;
  4         9  
10 4     4   12 use strict;
  4         5  
  4         56  
11 4     4   14 use warnings;
  4         7  
  4         94  
12              
13             # as Math::Combinatorics does not preserve input order in return values
14 4     4   1696 use Algorithm::Combinatorics qw/combinations/;
  4         9648  
  4         207  
15 4     4   19 use Carp qw/croak/;
  4         4  
  4         127  
16 4     4   16 use List::Util qw/shuffle/;
  4         3  
  4         265  
17 4     4   14 use Scalar::Util qw/looks_like_number refaddr/;
  4         4  
  4         21676  
18              
19             our $VERSION = '1.15';
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 8     8   11 my ( $self, $melody, $check_set, $note_count, $code, $flag_sort ) = @_;
500 8   100     19 $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 8   100     17 $note_count //= 0;
505 8 100       14 $note_count = @$check_set if $note_count < @$check_set;
506              
507             # rule is too large for the melody, skip
508 8 50       37 return 1, {} if @$check_set > @$melody;
509              
510 8         14 for my $i ( 0 .. @$melody - @$check_set ) {
511 19         25 my @selection = @{$melody}[ $i .. $i + @$check_set - 1 ];
  19         28  
512              
513 19         21 my $sel_audit = $code->( $self, \@selection );
514 19 100       27 @$sel_audit = sort { $a <=> $b } @$sel_audit if $flag_sort;
  1         3  
515 19 100       40 if ( "@$sel_audit" eq "@$check_set" ) {
516 4         20 return 0, { index => $i, selection => \@selection };
517             }
518              
519 15 100       27 if ( $note_count > @$check_set ) {
520 2         3 for my $count ( @$check_set + 1 .. $note_count ) {
521 7 50       37 last if $i + $count - 1 > $#$melody;
522              
523 7         7 @selection = @{$melody}[ $i .. $i + $count - 1 ];
  7         18  
524 7         19 my $iter = combinations( \@selection, scalar @$check_set );
525              
526 7         165 while ( my $subsel = $iter->next ) {
527 64         296 $sel_audit = $code->( $self, $subsel );
528 64 50       78 @$sel_audit = sort { $a <=> $b } @$sel_audit if $flag_sort;
  0         0  
529 64 100       162 if ( "@$sel_audit" eq "@$check_set" ) {
530 2         11 return 0, { context => \@selection, index => $i, selection => $subsel };
531             }
532             }
533             }
534             }
535             }
536              
537 2         3 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 6     6 1 12 my $self = shift;
546 6 50       14 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
547              
548 6         5 my %seen;
549 6         8 my @nset = sort { $a <=> $b } grep { !$seen{$_}++ } @$pset;
  48         41  
  30         53  
550 6 50       11 croak 'pitch set must contain at least two elements' if @nset < 2;
551              
552 6         5 my %aic;
553 6         14 for my $i ( 1 .. $#nset ) {
554 24         37 $aic{ ( $nset[$i] - $nset[ $i - 1 ] ) % $self->{_DEG_IN_SCALE} }++;
555             }
556             # and the wrap-around adjacent interval
557 6 50       10 if ( @nset > 2 ) {
558             $aic{ ( $nset[0] + $self->{_DEG_IN_SCALE} - $nset[-1] )
559 6         11 % $self->{_DEG_IN_SCALE} }++;
560             }
561              
562 6         5 my @aiv;
563 6         22 for my $ics ( 1 .. int( $self->{_DEG_IN_SCALE} / 2 ) ) {
564 48   100     98 push @aiv, $aic{$ics} || 0;
565             }
566              
567 6 50       47 return wantarray ? ( \@aiv, \%aic ) : \@aiv;
568             }
569              
570             # Utility, converts a scale_degrees-bit number into a pitch set.
571             # 7 3 0
572             # 137 -> 000010001001 -> [0,3,7]
573             sub bits2pcs {
574 1     1 1 643 my ( $self, $bs ) = @_;
575              
576 1         1 my @pset;
577 1         3 for my $p ( 0 .. $self->{_DEG_IN_SCALE} - 1 ) {
578 12 100       18 push @pset, $p if $bs & ( 1 << $p );
579             }
580 1         4 return \@pset;
581             }
582              
583             # Audits a sequence of pitches for suitability, per various checks
584             # passed in via the params hash (based on Smith-Brindle Reginald's
585             # "Serial Composition" discussion of atonal melody construction).
586             sub check_melody {
587 7     7 1 8 my $self = shift;
588 7         5 my $params = shift;
589 7 50       17 my $melody = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
590              
591 7         6 my $rules_applied = 0;
592              
593 7         6 my ( %intervals, @intervals );
594 7         17 for my $i ( 1 .. $#$melody ) {
595 35         43 my $ival = abs $melody->[$i] - $melody->[ $i - 1 ];
596 35         31 $intervals{$ival}++;
597 35         39 push @intervals, $ival;
598             }
599              
600 7 100       16 if ( exists $params->{dup_interval_limit} ) {
601 1         3 for my $icount ( values %intervals ) {
602 1 50       4 if ( $icount >= $params->{dup_interval_limit} ) {
603 1 50       6 return wantarray ? ( 0, "dup_interval_limit" ) : 0;
604             }
605             }
606 0         0 $rules_applied++;
607             }
608              
609 6 100       6 for my $ruleset ( @{ $params->{exclude_interval} || [] } ) {
  6         22  
610             croak "no interval set in exclude_interval rule"
611             if not exists $ruleset->{iset}
612 3 50 33     21 or ref $ruleset->{iset} ne 'ARRAY';
613 3 50       2 next if @{ $ruleset->{iset} } > @intervals;
  3         7  
614              
615             # check (magnitude of the) intervals of the melody. code ref just
616             # returns the literal intervals to compare against what is in the
617             # iset. (other options might be to ICC the intervals, or fold them
618             # into a single register, etc. but that would take more coding.)
619             my ( $ret, $results ) = $self->_apply_melody_rule(
620             \@intervals, $ruleset->{iset}, $ruleset->{in},
621 73     73   44 sub { [ @{ $_[1] } ] },
  73         77  
622 3 100       18 $ruleset->{sort} ? 1 : 0
623             );
624 3 50       10 if ( $ret != 1 ) {
625 3 50       26 return wantarray ? ( 0, "exclude_interval", $results ) : 0;
626             }
627 0         0 $rules_applied++;
628             }
629              
630 3         7 for my $ps_ref ( [qw/exclude_prime prime_form/],
631             [qw/exclude_half_prime half_prime_form/] ) {
632 4         4 my $ps_rule = $ps_ref->[0];
633 4         4 my $ps_method = $ps_ref->[1];
634              
635 4 100       3 for my $ruleset ( @{ $params->{$ps_rule} || [] } ) {
  4         14  
636             croak "no pitch set in $ps_rule rule"
637             if not exists $ruleset->{ps}
638 3 50 33     15 or ref $ruleset->{ps} ne 'ARRAY';
639              
640             # for intervals code, not necessary for pitch set operations, all of
641             # which sort the pitches as part of the calculations involved
642 3         4 delete $ruleset->{sort};
643              
644             # excludes from *any* subset for the given subset magnitudes of the
645             # parent pitch set
646 3 100       1 for my $ss_mag ( @{ $ruleset->{subsets} || [] } ) {
  3         10  
647             croak "subset must be of lesser magnitude than pitch set"
648 2 50       2 if $ss_mag >= @{ $ruleset->{ps} };
  2         6  
649 2   50     9 my $in_ss = $ruleset->{in} // 0;
650 2         2 $in_ss = @{ $ruleset->{ps} }
651 2 50       2 if $in_ss < @{ $ruleset->{ps} };
  2         6  
652             # except scale down to fit smaller subset pitch set
653 2         2 $in_ss -= @{ $ruleset->{ps} } - $ss_mag;
  2         3  
654              
655 2 100       5 next if $in_ss > @$melody;
656              
657 1         5 my $all_subpsets = $self->subsets( $ss_mag, $ruleset->{ps} );
658 1         147 my %seen_s_pset;
659 1         2 for my $s_pset (@$all_subpsets) {
660 3         7 my $s_prime = $self->$ps_method($s_pset);
661 3 50       13 next if $seen_s_pset{"@$s_prime"}++;
662             my ( $ret, $results ) =
663             $self->_apply_melody_rule( $melody, $s_prime,
664 3     3   9 $in_ss, sub { $_[0]->$ps_method( $_[1] ) } );
  3         8  
665 3 100       26 if ( $ret != 1 ) {
666 1 50       16 return wantarray ? ( 0, $ps_rule, $results ) : 0;
667             }
668             }
669 0         0 $rules_applied++;
670             }
671              
672             my ( $ret, $results ) =
673             $self->_apply_melody_rule( $melody, $ruleset->{ps}, $ruleset->{in},
674 2     7   9 sub { $_[0]->$ps_method( $_[1] ) } );
  7         20  
675 2 50       7 if ( $ret != 1 ) {
676 2 50       22 return wantarray ? ( 0, $ps_rule, $results ) : 0;
677             }
678              
679 0         0 $rules_applied++;
680             }
681             }
682              
683 0 0       0 if ( $rules_applied == 0 ) {
684 0 0       0 return wantarray ? ( 0, "no rules applied" ) : 0;
685             }
686 0 0       0 return wantarray ? ( 1, "ok" ) : 1;
687             }
688              
689             sub circular_permute {
690 460     460 1 334 my $self = shift;
691 460 50       636 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
692 460 50       596 croak 'pitch set must contain something' if !@$pset;
693              
694 460         303 my @perms;
695 460         701 for my $i ( 0 .. $#$pset ) {
696 2673         2374 for my $j ( 0 .. $#$pset ) {
697 16809         16416 $perms[$i][$j] = $pset->[ ( $i + $j ) % @$pset ];
698             }
699             }
700 460         543 return \@perms;
701             }
702              
703             sub complement {
704 1     1 1 2 my $self = shift;
705 1 50       4 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
706              
707 1         2 my %seen;
708 1         4 @seen{@$pset} = ();
709 1         4 return [ grep { !exists $seen{$_} } 0 .. $self->{_DEG_IN_SCALE} - 1 ];
  12         16  
710             }
711              
712 0     0 1 0 sub fnums { \%FORTE2PCS }
713              
714             sub forte_number_re {
715 1     1 1 10 return $FORTE_NUMBER_RE;
716             }
717              
718             sub forte2pcs {
719 2     2 1 4 my ( $self, $forte_number ) = @_;
720 2         12 return $FORTE2PCS{ uc $forte_number };
721             }
722              
723             # simple wrapper around check_melody to create something to work with,
724             # depending on the params.
725             sub gen_melody {
726 0     0 1 0 my ( $self, %params ) = @_;
727              
728 0         0 my $attempts = 1000; # enough for Helen, enough for us
729 0   0     0 my $max_interval = $params{melody_max_interval} || 16; # tessitura of a 10th
730 0         0 delete $params{melody_max_interval};
731              
732 0 0       0 if ( !keys %params ) {
733             # based on Reginald's ideas (insofar as those can be represented by
734             # the rules system I've cobbled together)
735 0         0 %params = (
736             exclude_half_prime => [
737             { ps => [ 0, 4, 5 ] }, # leading tone/tonic/dominant
738             ],
739             exclude_interval => [
740             { iset => [ 5, 5 ], }, # adjacent fourths ("cadential basses")
741             ],
742             exclude_prime => [
743             { ps => [ 0, 3, 7 ], in => 4 }, # major or minor triad, any guise
744             { ps => [ 0, 2, 5, 8 ], }, # 7th, any guise, exact
745             { ps => [ 0, 2, 4, 6 ], in => 5 }, # whole tone formation
746             # 7-35 (major/minor scale) but also excluding from all 5-x or
747             # 6-x subsets of said set
748             { ps => [ 0, 1, 3, 5, 6, 8, 10 ], subsets => [ 6, 5 ] },
749             ],
750             );
751             }
752              
753 0         0 my $got_melody = 0;
754 0         0 my @melody;
755 0         0 eval {
756 0         0 ATTEMPT: while ( $attempts-- > 0 ) {
757 0         0 my %seen;
758 0         0 my @pitches = 0 .. $self->{_DEG_IN_SCALE} - 1;
759 0         0 @melody = splice @pitches, rand @pitches, 1;
760 0         0 $seen{ $melody[0] } = 1;
761 0         0 my $melody_low = $melody[0];
762 0         0 my $melody_high = $melody[0];
763              
764 0         0 while (@pitches) {
765             my @potential = grep {
766 0         0 my $base_pitch = $_ % 12;
  0         0  
767 0         0 my $ret = 0;
768 0         0 for my $p (@pitches) {
769 0 0       0 if ( $base_pitch == $p ) { $ret = 1; last }
  0         0  
  0         0  
770             }
771             $ret
772 0         0 } $melody_high - $max_interval .. $melody_low + $max_interval;
773 0         0 my $choice = $potential[ rand @potential ];
774 0         0 my $base_choice = $choice % 12;
775 0         0 @pitches = grep $_ != $base_choice, @pitches;
776 0         0 push @melody, $choice;
777              
778 0 0       0 $melody_low = $choice if $choice < $melody_low;
779 0 0       0 $melody_high = $choice if $choice > $melody_high;
780             }
781              
782             # but negative pitches are awkward for various reasons
783 0 0       0 if ( $melody_low < 0 ) {
784 0         0 $melody_low = abs $melody_low;
785 0         0 $_ += $melody_low for @melody;
786             }
787              
788 0         0 ( $got_melody, my $msg ) = $self->check_melody( \%params, \@melody );
789 0 0       0 next ATTEMPT if $got_melody != 1;
790              
791 0         0 last;
792             }
793             };
794 0 0       0 croak $@ if $@;
795 0 0       0 croak "could not generate a melody" unless $got_melody;
796              
797 0         0 return \@melody;
798             }
799              
800             # copied from Music::NeoRiemannianTonnetz 'normalize', see perldocs
801             # for differences between this and prime_form and normal_form
802             sub half_prime_form {
803 6     6 1 5 my $self = shift;
804 6 100       15 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
805              
806 6 50       11 croak 'pitch set must contain something' if !@$pset;
807              
808 6         3 my %origmap;
809 6         8 for my $p (@$pset) {
810 18         13 push @{ $origmap{ $p % $self->{_DEG_IN_SCALE} } }, $p;
  18         31  
811             }
812 6 50       11 if ( keys %origmap == 1 ) {
813 0 0       0 return wantarray ? ( keys %origmap, \%origmap ) : keys %origmap;
814             }
815 6         16 my @nset = sort { $a <=> $b } keys %origmap;
  17         21  
816              
817 6         5 my @equivs;
818 6         11 for my $i ( 0 .. $#nset ) {
819 18         19 for my $j ( 0 .. $#nset ) {
820 54         62 $equivs[$i][$j] = $nset[ ( $i + $j ) % @nset ];
821             }
822             }
823 6         7 my @order = reverse 1 .. $#nset;
824              
825 6         7 my @normal;
826 6         8 for my $i (@order) {
827 6         6 my $min_span = $self->{_DEG_IN_SCALE};
828 6         4 my @min_span_idx;
829              
830 6         7 for my $eidx ( 0 .. $#equivs ) {
831             my $span =
832 18         18 ( $equivs[$eidx][$i] - $equivs[$eidx][0] ) % $self->{_DEG_IN_SCALE};
833 18 100       25 if ( $span < $min_span ) {
    50          
834 10         6 $min_span = $span;
835 10         11 @min_span_idx = $eidx;
836             } elsif ( $span == $min_span ) {
837 0         0 push @min_span_idx, $eidx;
838             }
839             }
840              
841 6 50       10 if ( @min_span_idx == 1 ) {
842 6         4 @normal = @{ $equivs[ $min_span_idx[0] ] };
  6         9  
843 6         7 last;
844             } else {
845 0         0 @equivs = @equivs[@min_span_idx];
846             }
847             }
848              
849 6 50       8 if ( !@normal ) {
850             # nothing unique, pick lowest starting pitch, which is first index
851             # by virtue of the numeric sort performed above.
852 0         0 @normal = @{ $equivs[0] };
  0         0  
853             }
854              
855             # but must map (and anything else not ) so b is 0,
856             # dis 4, etc. and also update the original pitch mapping - this is
857             # the major addition to the otherwise stock normal_form code.
858 6 100       12 if ( $normal[0] != 0 ) {
859 5         4 my $trans = $self->{_DEG_IN_SCALE} - $normal[0];
860 5         4 my %newmap;
861 5         5 for my $i (@normal) {
862 15         11 my $prev = $i;
863 15         7 $i = ( $i + $trans ) % $self->{_DEG_IN_SCALE};
864 15         19 $newmap{$i} = $origmap{$prev};
865             }
866 5         13 %origmap = %newmap;
867             }
868              
869 6 50       27 return wantarray ? ( \@normal, \%origmap ) : \@normal;
870             }
871              
872             sub interval_class_content {
873 6     6 1 4 my $self = shift;
874 6 50       15 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
875              
876 6         4 my %seen;
877 6         9 my @nset = sort { $a <=> $b } grep { !$seen{$_}++ } @$pset;
  34         43  
  35         50  
878 6 50       12 croak 'pitch set must contain at least two elements' if @nset < 2;
879              
880 6         8 my %icc;
881 6         10 for my $i ( 1 .. $#nset ) {
882 19         20 for my $j ( 0 .. $i - 1 ) {
883             $icc{
884             $self->pitch2intervalclass(
885             ( $nset[$i] - $nset[$j] ) % $self->{_DEG_IN_SCALE}
886             )
887 49         70 }++;
888             }
889             }
890              
891 6         6 my @icv;
892 6         9 for my $ics ( 1 .. int( $self->{_DEG_IN_SCALE} / 2 ) ) {
893 36   100     64 push @icv, $icc{$ics} || 0;
894             }
895              
896 6 50       27 return wantarray ? ( \@icv, \%icc ) : \@icv;
897             }
898              
899             sub intervals2pcs {
900 2     2 1 3 my $self = shift;
901 2         3 my $start_pitch = shift;
902 2 50       6 my $iset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
903 2 50       5 croak 'interval set must contain something' if !@$iset;
904              
905 2   50     3 $start_pitch //= 0;
906 2         3 $start_pitch = int $start_pitch;
907              
908 2         3 my @pset = $start_pitch;
909 2         3 for my $i (@$iset) {
910 8         12 push @pset, ( $pset[-1] + $i ) % $self->{_DEG_IN_SCALE};
911             }
912              
913 2         9 return \@pset;
914             }
915              
916             sub invariance_matrix {
917 1     1 1 2 my $self = shift;
918 1 50       4 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
919 1 50       3 croak 'pitch set must contain something' if !@$pset;
920              
921 1         2 my @ivm;
922 1         3 for my $i ( 0 .. $#$pset ) {
923 4         4 for my $j ( 0 .. $#$pset ) {
924 16         19 $ivm[$i][$j] = ( $pset->[$i] + $pset->[$j] ) % $self->{_DEG_IN_SCALE};
925             }
926             }
927              
928 1         7 return \@ivm;
929             }
930              
931             sub invert {
932 243     243 1 180 my $self = shift;
933 243         168 my $axis = shift;
934 243 50       321 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
935 243 50       328 croak 'pitch set must contain something' if !@$pset;
936              
937 243   50     325 $axis //= 0;
938 243         197 $axis = int $axis;
939              
940 243         321 my @inverse = @$pset;
941 243         213 for my $p (@inverse) {
942 1394         1079 $p = ( $axis - $p ) % $self->{_DEG_IN_SCALE};
943             }
944              
945 243         404 return \@inverse;
946             }
947              
948             # Utility routine to get the last few elements of a list (but never more
949             # than the whole list, etc).
950             sub lastn {
951 3     3 1 8 my ( $self, $pset, $n ) = @_;
952 3 50 33     15 croak 'cannot get elements of nothing'
953             if !defined $pset
954             or ref $pset ne 'ARRAY';
955              
956 3 50       5 return unless @$pset;
957              
958 3   66     8 $n //= $self->{_lastn};
959 3 50       6 croak 'n of lastn must be number' unless looks_like_number $n;
960              
961 3         3 my $len = @$pset;
962 3 100       4 $len = $n if $len > $n;
963 3         4 $len *= -1;
964 3         9 return @{$pset}[ $len .. -1 ];
  3         15  
965             }
966              
967             sub mininterval {
968 7     7 1 8 my ( $self, $from, $to ) = @_;
969 7         7 my $dir = 1;
970              
971 7 50       22 croak 'from pitch must be a number' unless looks_like_number $from;
972 7 50       11 croak 'to pitch must be a number' unless looks_like_number $to;
973              
974 7         8 $from %= $self->{_DEG_IN_SCALE};
975 7         7 $to %= $self->{_DEG_IN_SCALE};
976              
977 7 100       11 if ( $from > $to ) {
978 3         4 ( $from, $to ) = ( $to, $from );
979 3         3 $dir = -1;
980             }
981 7         7 my $interval = $to - $from;
982 7 100       14 if ( $interval > $self->{_DEG_IN_SCALE} / 2 ) {
983 4         4 $dir *= -1;
984 4         5 $from += $self->{_DEG_IN_SCALE};
985 4         6 $interval = $from - $to;
986             }
987              
988 7         26 return $interval * $dir;
989             }
990              
991             sub multiply {
992 1     1 1 2 my $self = shift;
993 1         2 my $factor = shift;
994 1 50       5 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
995 1 50       3 croak 'pitch set must contain something' if !@$pset;
996              
997 1   50     2 $factor //= 1;
998 1         1 $factor = int $factor;
999              
1000 1         2 return [ map { my $p = $_ * $factor % $self->{_DEG_IN_SCALE}; $p } @$pset ];
  4         6  
  4         8  
1001             }
1002              
1003             # Utility methods for get/check/reset of each element in turn of a given
1004             # array reference, with wrap-around. Handy if pulling sequential
1005             # elements off a list, but have much code between the successive calls.
1006             {
1007             my %seen;
1008              
1009             # get the iterator value for a ref
1010             sub geti {
1011 1     1 1 1 my ( $self, $ref ) = @_;
1012 1   50     10 return $seen{ refaddr $ref} || 0;
1013             }
1014              
1015             # nexti(\@array) - returns subsequent elements of array on each
1016             # successive call
1017             sub nexti {
1018 2     2 1 2 my ( $self, $ref ) = @_;
1019 2   100     10 $seen{ refaddr $ref} ||= 0;
1020 2         9 $ref->[ ++$seen{ refaddr $ref} % @$ref ];
1021             }
1022              
1023             # reseti(\@array) - resets counter
1024             sub reseti {
1025 0     0 1 0 my ( $self, $ref ) = @_;
1026 0         0 $seen{ refaddr $ref} = 0;
1027             }
1028              
1029             # set the iterator for a ref
1030             sub seti {
1031 1     1 1 1 my ( $self, $ref, $i ) = @_;
1032 1 50       5 croak 'iterator must be number'
1033             unless looks_like_number($i);
1034 1         4 $seen{ refaddr $ref} = $i;
1035             }
1036              
1037             # returns current element, but does not advance pointer
1038             sub whati {
1039 1     1 1 2 my ( $self, $ref ) = @_;
1040 1   50     7 $seen{ refaddr $ref} ||= 0;
1041 1         6 $ref->[ $seen{ refaddr $ref} % @$ref ];
1042             }
1043             }
1044              
1045             sub new {
1046 5     5 1 32 my ( $class, %param ) = @_;
1047 5         6 my $self = {};
1048              
1049 5   66     27 $self->{_DEG_IN_SCALE} = int( $param{DEG_IN_SCALE} // $DEG_IN_SCALE );
1050 5 50       14 if ( $self->{_DEG_IN_SCALE} < 2 ) {
1051 0         0 croak 'degrees in scale must be greater than one';
1052             }
1053              
1054 5 100       12 if ( exists $param{lastn} ) {
1055             croak 'lastn must be number'
1056 1 50       3 unless looks_like_number $param{lastn};
1057 1         2 $self->{_lastn} = $param{lastn};
1058             } else {
1059 4         8 $self->{_lastn} = 2;
1060             }
1061              
1062             # XXX packing not implemented beyond "right" method (via www.mta.ca docs)
1063 5   50     20 $self->{_packing} = $param{PACKING} // 'right';
1064              
1065 5         8 bless $self, $class;
1066 5         10 return $self;
1067             }
1068              
1069             sub normal_form {
1070 459     459 1 335 my $self = shift;
1071 459 100       557 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1072              
1073 459 50       603 croak 'pitch set must contain something' if !@$pset;
1074              
1075 459         301 my %origmap;
1076 459         479 for my $p (@$pset) {
1077 2679         1563 push @{ $origmap{ $p % $self->{_DEG_IN_SCALE} } }, $p;
  2679         3927  
1078             }
1079 459 50       730 if ( keys %origmap == 1 ) {
1080 0 0       0 return wantarray ? ( [ keys %origmap ], \%origmap ) : [ keys %origmap ];
1081             }
1082 459         1091 my @nset = sort { $a <=> $b } keys %origmap;
  4519         3754  
1083              
1084 459         822 my $equivs = $self->circular_permute( \@nset );
1085 459         692 my @order = 1 .. $#nset;
1086 459 50       678 if ( $self->{_packing} eq 'right' ) {
    0          
1087 459         407 @order = reverse @order;
1088             } elsif ( $self->{_packing} eq 'left' ) {
1089             # XXX not sure about this, www.mta.ca instructions not totally
1090             # clear on the Forte method, and the 7-Z18 (0234589) form
1091             # listed there reduces to (0123589). So, blow up until can
1092             # figure that out.
1093             # unshift @order, pop @order;
1094             # Also, the inclusion of http://en.wikipedia.org/wiki/Forte_number
1095             # plus a prime_form call on those pitch sets shows no changes caused
1096             # by the default 'right' packing method, so sticking with it until
1097             # learn otherwise. (In hindsight, the right packing method is that
1098             # of rahn1980, so sticking with that...)
1099 0         0 die 'left packing method not yet implemented (sorry)';
1100             } else {
1101 0         0 croak 'unknown packing method (try the "right" one)';
1102             }
1103              
1104 459         295 my @normal;
1105 459         428 for my $i (@order) {
1106 749         621 my $min_span = $self->{_DEG_IN_SCALE};
1107 749         425 my @min_span_idx;
1108              
1109 749         771 for my $eidx ( 0 .. $#$equivs ) {
1110             my $span =
1111 3427         2983 ( $equivs->[$eidx][$i] - $equivs->[$eidx][0] ) % $self->{_DEG_IN_SCALE};
1112 3427 100       4555 if ( $span < $min_span ) {
    100          
1113 1049         626 $min_span = $span;
1114 1049         1120 @min_span_idx = $eidx;
1115             } elsif ( $span == $min_span ) {
1116 516         424 push @min_span_idx, $eidx;
1117             }
1118             }
1119              
1120 749 100       802 if ( @min_span_idx == 1 ) {
1121 434         263 @normal = @{ $equivs->[ $min_span_idx[0] ] };
  434         727  
1122 434         496 last;
1123             } else {
1124 315         206 @$equivs = @{$equivs}[@min_span_idx];
  315         708  
1125             }
1126             }
1127              
1128 459 100       630 if ( !@normal ) {
1129             # nothing unique, pick lowest starting pitch, which is first index
1130             # by virtue of the numeric sort performed above.
1131 25         19 @normal = @{ $equivs->[0] };
  25         45  
1132             }
1133              
1134 459         979 $_ += 0 for @normal; # KLUGE avoid Test::Differences seeing '4' vs. 4
1135              
1136 459 100       1975 return wantarray ? ( \@normal, \%origmap ) : \@normal;
1137             }
1138              
1139             # Utility, converts a pitch set into a scale_degrees-bit number:
1140             # 7 3 0
1141             # [0,3,7] -> 000010001001 -> 137
1142             sub pcs2bits {
1143 2     2 1 3 my $self = shift;
1144 2 50       6 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1145              
1146 2 50       5 croak 'pitch set must contain something' if !@$pset;
1147              
1148 2         1 my $bs = 0;
1149 2         9 for my $p ( map $_ % $self->{_DEG_IN_SCALE}, @$pset ) {
1150 6         7 $bs |= 1 << $p;
1151             }
1152 2         7 return $bs;
1153             }
1154              
1155             sub pcs2forte {
1156 5     5 1 6 my $self = shift;
1157 5 50       12 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1158              
1159 5 50       14 croak 'pitch set must contain something' if !@$pset;
1160              
1161 5         5 return $PCS2FORTE{ join ',', @{ $self->prime_form($pset) } };
  5         8  
1162             }
1163              
1164             sub pcs2intervals {
1165 1     1 1 2 my $self = shift;
1166 1 50       4 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1167              
1168 1 50       4 croak 'pitch set must contain at least two elements' if @$pset < 2;
1169              
1170 1         1 my @intervals;
1171 1         2 for my $i ( 1 .. $#{$pset} ) {
  1         3  
1172 2         5 push @intervals, $pset->[$i] - $pset->[ $i - 1 ];
1173             }
1174              
1175 1         5 return \@intervals;
1176             }
1177              
1178             sub pcs2str {
1179 3     3 1 5 my $self = shift;
1180 3 50       6 croak 'must supply a pitch set' if !defined $_[0];
1181              
1182 3         3 my $str;
1183 3 100       13 if ( ref $_[0] eq 'ARRAY' ) {
    100          
1184 1         2 $str = '[' . join( ',', @{ $_[0] } ) . ']';
  1         3  
1185             } elsif ( $_[0] =~ m/,/ ) {
1186 1         3 $str = '[' . $_[0] . ']';
1187             } else {
1188 1         4 $str = '[' . join( ',', @_ ) . ']';
1189             }
1190 3         9 return $str;
1191             }
1192              
1193             sub pitch2intervalclass {
1194 56     56 1 47 my ( $self, $pitch ) = @_;
1195              
1196             # ensure member of the tone system, otherwise strange results
1197 56         35 $pitch %= $self->{_DEG_IN_SCALE};
1198              
1199             return $pitch > int( $self->{_DEG_IN_SCALE} / 2 )
1200 56 100       132 ? $self->{_DEG_IN_SCALE} - $pitch
1201             : $pitch;
1202             }
1203              
1204             # XXX tracking of original pitches would be nice, though complicated, as
1205             # ->invert would need to be modified or a non-modulating version used
1206             sub prime_form {
1207 227     227 1 181225 my $self = shift;
1208 227 50       458 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1209              
1210 227 50       394 croak 'pitch set must contain something' if !@$pset;
1211              
1212 227         340 my @forms = scalar $self->normal_form($pset);
1213 227         395 push @forms, scalar $self->normal_form( $self->invert( 0, $forms[0] ) );
1214              
1215 227         251 for my $set (@forms) {
1216 454 100       940 $set = $self->transpose( $self->{_DEG_IN_SCALE} - $set->[0], $set )
1217             if $set->[0] != 0;
1218             }
1219              
1220 227         175 my @prime;
1221 227 100       174 if ( "@{$forms[0]}" eq "@{$forms[1]}" ) {
  227         457  
  227         443  
1222 84         58 @prime = @{ $forms[0] };
  84         150  
1223             } else {
1224             # look for most compact to the left
1225 143         137 my @sums = ( 0, 0 );
1226             PITCH:
1227 143         185 for my $i ( 0 .. $#$pset ) {
1228 349         257 for my $j ( 0 .. 1 ) {
1229 698         566 $sums[$j] += $forms[$j][$i];
1230             }
1231 349 100       523 if ( $sums[0] < $sums[1] ) {
    100          
1232 136         84 @prime = @{ $forms[0] };
  136         259  
1233 136         176 last PITCH;
1234             } elsif ( $sums[0] > $sums[1] ) {
1235 7         4 @prime = @{ $forms[1] };
  7         12  
1236 7         10 last PITCH;
1237             }
1238             }
1239             }
1240              
1241 227         589 return \@prime;
1242             }
1243              
1244             # Utility, "mirrors" a pitch to be within supplied min/max values as
1245             # appropriate for how many times the pitch "reflects" back within those
1246             # limits, which will depend on which limit is broken and by how much.
1247             sub reflect_pitch {
1248 21     21 1 40 my ( $self, $v, $min, $max ) = @_;
1249 21 50       33 croak 'pitch must be a number' if !looks_like_number $v;
1250 21 50 33     129 croak 'limits must be numbers and min less than max'
      33        
1251             if !looks_like_number $min
1252             or !looks_like_number $max
1253             or $min >= $max;
1254 21 100 100     48 return $v if $v <= $max and $v >= $min;
1255              
1256 17         8 my ( @origins, $overshoot, $direction );
1257 17 100       18 if ( $v > $max ) {
1258 5         5 @origins = ( $max, $min );
1259 5         3 $overshoot = abs( $v - $max );
1260 5         5 $direction = -1;
1261             } else {
1262 12         13 @origins = ( $min, $max );
1263 12         7 $overshoot = abs( $min - $v );
1264 12         7 $direction = 1;
1265             }
1266 17         12 my $range = abs( $max - $min );
1267 17         14 my $register = int( $overshoot / $range );
1268 17 100       23 if ( $register % 2 == 1 ) {
1269 9         5 @origins = reverse @origins;
1270 9         9 $direction *= -1;
1271             }
1272 17         9 my $remainder = $overshoot % $range;
1273              
1274 17         27 return $origins[0] + $remainder * $direction;
1275             }
1276              
1277             sub retrograde {
1278 1     1 1 2 my $self = shift;
1279 1 50       4 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1280              
1281 1 50       52 croak 'pitch set must contain something' if !@$pset;
1282              
1283 1         6 return [ reverse @$pset ];
1284             }
1285              
1286             sub rotate {
1287 6     6 1 7 my $self = shift;
1288 6         7 my $r = shift;
1289 6 50       10 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1290              
1291 6 50 33     35 croak 'rotate value must be integer'
1292             if !defined $r
1293             or $r !~ /^-?\d+$/;
1294 6 50       8 croak 'pitch set must contain something' if !@$pset;
1295              
1296 6         6 my @rot;
1297 6 100       7 if ( $r == 0 ) {
1298 1         2 @rot = @$pset;
1299             } else {
1300 5         11 for my $i ( 0 .. $#$pset ) {
1301 23         28 $rot[$i] = $pset->[ ( $i - $r ) % @$pset ];
1302             }
1303             }
1304              
1305 6         26 return \@rot;
1306             }
1307              
1308             # Utility method to rotate a list to a named element (for example "gis"
1309             # in a list of note names, see my etude no.2 for results of heavy use of
1310             # such rotations).
1311             sub rotateto {
1312 2     2 1 4 my $self = shift;
1313 2         1 my $what = shift;
1314 2         2 my $dir = shift;
1315 2 50       6 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1316              
1317 2 50       5 croak 'nothing to search on' unless defined $what;
1318 2 50       4 croak 'nothing to rotate on' if !@$pset;
1319              
1320 2         5 my @idx = 0 .. $#$pset;
1321              
1322 2   50     5 $dir //= 1;
1323 2 100       4 @idx = reverse @idx if $dir < 0;
1324              
1325 2         3 for my $i (@idx) {
1326 5 100       10 next unless $pset->[$i] eq $what;
1327 2         4 return $self->rotate( -$i, $pset );
1328             }
1329 0         0 croak "no such element $what";
1330             }
1331              
1332             # XXX probably should disallow changing this on the fly, esp. if allow
1333             # method chaining, as it could throw off results in wacky ways.
1334             sub scale_degrees {
1335 6     6 1 1687 my ( $self, $dis ) = @_;
1336 6 100       15 if ( defined $dis ) {
1337 2 50 33     23 croak 'scale degrees value must be positive integer greater than 1'
      33        
1338             if !defined $dis
1339             or $dis !~ /^\d+$/
1340             or $dis < 2;
1341 2         4 $self->{_DEG_IN_SCALE} = $dis;
1342             }
1343 6         16 return $self->{_DEG_IN_SCALE};
1344             }
1345              
1346             sub set_complex {
1347 1     1 1 2 my $self = shift;
1348 1 50       3 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1349              
1350 1 50       3 croak 'pitch set must contain something' if !@$pset;
1351              
1352 1         8 my $iset = $self->invert( 0, $pset );
1353 1         3 my $dis = $self->scale_degrees;
1354              
1355 1         2 my @plex = $pset;
1356 1         3 for my $i ( 1 .. $#$pset ) {
1357 11         9 for my $j ( 0 .. $#$pset ) {
1358 132 100       104 if ( $j == 0 ) {
1359 11         11 $plex[$i][0] = $iset->[$i];
1360             } else {
1361 121         127 $plex[$i][$j] = ( $pset->[$j] + $iset->[$i] ) % $dis;
1362             }
1363             }
1364             }
1365              
1366 1         16 return \@plex;
1367             }
1368              
1369             sub subsets {
1370 2     2 1 3 my $self = shift;
1371 2         3 my $len = shift;
1372 2 50       7 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1373              
1374 2         2 my %seen;
1375             my @nset =
1376 2 50       5 map { my $p = $_ % $self->{_DEG_IN_SCALE}; !$seen{$p}++ ? $p : () } @$pset;
  10         9  
  10         23  
1377 2 50       11 croak 'pitch set must contain two or more unique pitches' if @nset < 2;
1378              
1379 2 50       6 if ( defined $len ) {
1380 2 50 33     12 croak 'length must be less than size of pitch set (but not zero)'
1381             if $len >= @nset
1382             or $len == 0;
1383 2 50       5 if ( $len < 0 ) {
1384 0         0 $len = @nset + $len;
1385 0 0       0 croak 'negative length exceeds magnitude of pitch set' if $len < 1;
1386             }
1387             } else {
1388 0         0 $len = @nset - 1;
1389             }
1390              
1391 2         9 return [ combinations( \@nset, $len ) ];
1392             }
1393              
1394             sub tcis {
1395 1     1 1 3 my $self = shift;
1396 1 50       4 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1397              
1398 1 50       2 croak 'pitch set must contain something' if !@$pset;
1399              
1400 1         2 my %seen;
1401 1         4 @seen{@$pset} = ();
1402              
1403 1         1 my @tcis;
1404 1         4 for my $i ( 0 .. $self->{_DEG_IN_SCALE} - 1 ) {
1405 12         10 $tcis[$i] = 0;
1406 12         7 for my $p ( @{ $self->transpose_invert( $i, 0, $pset ) } ) {
  12         14  
1407             $tcis[$i]++
1408 48 100       65 if exists $seen{$p};
1409             }
1410             }
1411 1         6 return \@tcis;
1412             }
1413              
1414             sub tcs {
1415 1     1 1 1 my $self = shift;
1416 1 50       4 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1417              
1418 1 50       3 croak 'pitch set must contain something' if !@$pset;
1419              
1420 1         1 my %seen;
1421 1         4 @seen{@$pset} = ();
1422              
1423 1         2 my @tcs = scalar @$pset;
1424 1         4 for my $i ( 1 .. $self->{_DEG_IN_SCALE} - 1 ) {
1425 11         11 $tcs[$i] = 0;
1426 11         7 for my $p ( @{ $self->transpose( $i, $pset ) } ) {
  11         9  
1427             $tcs[$i]++
1428 44 100       59 if exists $seen{$p};
1429             }
1430             }
1431 1         5 return \@tcs;
1432             }
1433              
1434             sub transpose {
1435 243     243 1 198 my $self = shift;
1436 243         157 my $t = shift;
1437 243 50       357 my @tset = ref $_[0] eq 'ARRAY' ? @{ $_[0] } : @_;
  243         341  
1438              
1439 243 50       336 croak 'transpose value not set' if !defined $t;
1440 243 50       282 croak 'pitch set must contain something' if !@tset;
1441              
1442 243         175 $t = int $t;
1443 243         196 for my $p (@tset) {
1444 1386         1064 $p = ( $p + $t ) % $self->{_DEG_IN_SCALE};
1445             }
1446 243         385 return \@tset;
1447             }
1448              
1449             sub transpose_invert {
1450 14     14 1 10 my $self = shift;
1451 14         10 my $t = shift;
1452 14         7 my $axis = shift;
1453 14 50       20 my $pset = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
1454              
1455 14 50       16 croak 'transpose value not set' if !defined $t;
1456 14 50       19 croak 'pitch set must contain something' if !@$pset;
1457              
1458 14   50     17 $axis //= 0;
1459 14         14 my $tset = $self->invert( $axis, $pset );
1460              
1461 14         9 $t = int $t;
1462 14         12 for my $p (@$tset) {
1463 55         41 $p = ( $p + $t ) % $self->{_DEG_IN_SCALE};
1464             }
1465 14         18 return $tset;
1466             }
1467              
1468             sub variances {
1469 1     1 1 2 my ( $self, $pset1, $pset2 ) = @_;
1470              
1471 1 50       4 croak 'pitch set must be array ref' unless ref $pset1 eq 'ARRAY';
1472 1 50       3 croak 'pitch set must contain something' if !@$pset1;
1473 1 50       3 croak 'pitch set must be array ref' unless ref $pset2 eq 'ARRAY';
1474 1 50       3 croak 'pitch set must contain something' if !@$pset2;
1475              
1476 1         1 my ( @union, @intersection, @difference, %count );
1477 1         3 for my $p ( @$pset1, @$pset2 ) {
1478 8         12 $count{$p}++;
1479             }
1480 1         5 for my $p ( sort { $a <=> $b } keys %count ) {
  11         10  
1481 6         5 push @union, $p;
1482 6 100       3 push @{ $count{$p} > 1 ? \@intersection : \@difference }, $p;
  6         11  
1483             }
1484 1 50       6 return wantarray ? ( \@intersection, \@difference, \@union ) : \@intersection;
1485             }
1486              
1487             sub zrelation {
1488 2     2 1 4 my ( $self, $pset1, $pset2 ) = @_;
1489              
1490 2 50       7 croak 'pitch set must be array ref' unless ref $pset1 eq 'ARRAY';
1491 2 50       5 croak 'pitch set must contain something' if !@$pset1;
1492 2 50       4 croak 'pitch set must be array ref' unless ref $pset2 eq 'ARRAY';
1493 2 50       3 croak 'pitch set must contain something' if !@$pset2;
1494              
1495 2         3 my @ic_vecs;
1496 2         2 for my $ps ( $pset1, $pset2 ) {
1497 4         9 push @ic_vecs, scalar $self->interval_class_content($ps);
1498             }
1499 2 100       2 return ( "@{$ic_vecs[0]}" eq "@{$ic_vecs[1]}" ) ? 1 : 0;
  2         5  
  2         13  
1500             }
1501              
1502             1;
1503             __END__