-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathcircle-intervals
235 lines (198 loc) · 6.91 KB
/
circle-intervals
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
#!/usr/bin/env perl
use strict;
use warnings;
# Evenly space a number of named sub-circles around the perimeter a circle.
# Optionally show another inner circle of offset notes.
# Examples:
# Chromatic: perl circle-intervals --show_marks 24 --show_inner 1 > circle-intervals.svg
# Circle of 5ths: perl circle-intervals --interval 7 --show_inner 1 --inner_note F > circle-intervals.svg
# " perl circle-intervals --interval 5 --flat 1 > circle-intervals.svg
# Numeric: perl circle-intervals --interval 4 --show_inner 1 --numeric 1 > circle-intervals.svg
# " perl circle-intervals --interval 3 --show_marks 16 --show_inner 1 --numeric 1 > circle-intervals.svg
# Coltrane's wholetone: perl circle-intervals --interval 2 --show_marks 30 --show_inner 1 --flat 1 > circle-intervals.svg
use Getopt::Long qw(GetOptions);
use List::SomeUtils qw(first_index);
use Math::Trig qw(pi);
use Music::Scales qw(get_scale_notes);
use SVG qw(title);
use constant HALF => pi / 2;
use constant DOUBLE => 2 * pi;
use constant SCALE => 'chromatic';
my %opts = (
interval => 1, # default half-step (intervals: 1-11)
show_marks => 12, # how many circular note marks to display (2-60)
show_inner => 0, # show the inner ring?
flat => 0, # show note names with flats: default sharps
numeric => 0, # display notes as pitch numbers
outer_note => 'C', # starting outer ring note
inner_note => 'C#', # starting inner ring note
diameter => 512, # size of the outer circle
fill => 'white',
outer_line => 'green',
inner_line => 'gray',
text_line => 'black',
);
GetOptions( \%opts,
'interval=i',
'show_marks=i',
'show_inner',
'flat',
'numeric',
'outer_note=s',
'inner_note=s',
'diameter=i',
'fill=s',
'outer_line=s',
'inner_line=s',
'text_line=s',
) or die "Can't GetOptions()";
my $min_marks = 2; # minimum number of note markers
my $total_marks = 60; # maximum number of note markers
my $font_size = 20; # size of the caption font
# NB: Changing the border_size and sub_radius apparently messes up placement
my $border_size = 15; # chart margin
my $sub_radius = 11; # radius for sub-circle markings
my $radius = $opts{diameter} / 2;
my $frame_size = $opts{diameter} + 2 * $border_size;
my %named = (
1 => 'halfstep',
2 => 'wholestep',
3 => 'min 3rd',
4 => 'maj 3rd',
5 => 'perf 4th',
6 => 'tritone',
7 => 'perf 5th',
8 => 'sharp 5',
9 => 'sixth',
10 => 'flat 7',
11 => 'seventh',
);
my $caption = "Interval: $named{ $opts{interval} }, Notes: $opts{show_marks}";
my $title = 'Circular Music Intervals';
my $desc = "Show $opts{show_marks} marks around a note circle for the $named{ $opts{interval} } interval";
my $svg = SVG->new(
width => $frame_size,
height => $frame_size,
);
$svg->title()->cdata($title);
$svg->desc(id => 'document-desc')->cdata($desc);
# build the outer ring
my $outer_style = $svg->group(
id => 'outer-style-group',
style => {
stroke => $opts{outer_line},
fill => $opts{fill},
},
);
$outer_style->circle(
id => 'style-group-outer-circle',
cx => $frame_size / 2,
cy => $frame_size / 2,
r => $radius,
);
$outer_style->text(
id => 'style-group-outer-caption',
x => $frame_size / 2 - $sub_radius * 10,
y => $frame_size / 2,
style => {
stroke => $opts{text_line},
'font-size' => $font_size,
},
-cdata => $caption,
) if !$opts{show_inner};
my @outer_scale = get_scale_notes($opts{outer_note}, SCALE, undef, $opts{flat} ? 'b' : '#');
my @outer_labels = get_labels(\@outer_scale, $opts{interval}, $opts{show_marks});
# compute the ring positions
my @marks = map { $_ * $total_marks / $opts{show_marks} } 1 .. $opts{show_marks};
my $fract = ($marks[1] - $marks[0]) / 2;
my $i = 0;
# display the ring positions
for my $mark (@marks) {
$i++;
my $p = coordinate($mark, $total_marks, $radius);
$outer_style->circle(
id => $mark . '-style-group-outer-sub-circle',
cx => $p->[0] + $sub_radius + $border_size / 3,
cy => $p->[1] + $sub_radius + $border_size / 3,
r => $sub_radius,
);
my $item = $outer_labels[ $i % @outer_labels ];
my $text = $opts{numeric}
? first_index { $_ eq $item } @outer_scale
: $item;
$outer_style->text(
id => $mark . '-style-group-outer-sub-text',
x => $p->[0] + $sub_radius - $sub_radius / 2 + $border_size / 3,
y => $p->[1] + $sub_radius + $sub_radius / 2 + $border_size / 3,
)->cdata($text);
}
if ($opts{show_inner}) {
# generate a group element
my $inner_style = $svg->group(
id => 'inner-style-group',
style => {
stroke => $opts{inner_line},
fill => $opts{fill},
},
);
my @inner_scale = get_scale_notes($opts{inner_note}, SCALE, undef, $opts{flat} ? 'b' : '#');
my @inner_labels = get_labels(\@inner_scale, $opts{interval}, $opts{show_marks});
my $inner_radius = $radius - $sub_radius * 3;
$inner_style->circle(
id => 'style-group-inner-circle',
cx => $frame_size / 2,
cy => $frame_size / 2,
r => $inner_radius,
);
$inner_style->text(
id => 'style-group-inner-caption',
x => $frame_size / 2 - $sub_radius * 10,
y => $frame_size / 2,
style => {
stroke => $opts{text_line},
'font-size' => $font_size,
},
)->cdata($caption);
$i = 0;
for my $mark (@marks) {
$i++;
my $p = coordinate(
$mark + $fract,
$total_marks,
$inner_radius,
);
$inner_style->circle(
id => $mark . '-style-group-inner-sub-circle',
cx => $p->[0] + $sub_radius * 4 + $border_size / 3,
cy => $p->[1] + $sub_radius * 4 + $border_size / 3,
r => $sub_radius,
);
my $item = $inner_labels[ $i % @inner_labels ];
my $text = $opts{numeric}
? first_index { $_ eq $item } @inner_scale
: $item;
$inner_style->text(
id => $mark . '-style-group-inner-sub-text',
x => $p->[0] + $sub_radius * 3 + 3 + $border_size / 3,
y => $p->[1] + $sub_radius * 3 + 3 + $sub_radius + $border_size / 3,
)->cdata($text);
}
}
print $svg->xmlify;
sub get_labels {
my ($scale, $interval, $marks) = @_;
my @labels = map { $scale->[ ($_ * $interval) % @$scale ] }
0 .. $marks - 1;
return @labels;
}
sub coordinate {
my ($p, $total, $radius, $inner) = @_;
# compute the analog minute time equivalent
my $analog = $p / $total * DOUBLE - HALF;
# get the coordinate of the time value
my $coord = [
$radius + $radius * cos($analog),
$radius + $radius * sin($analog)
];
return $coord;
}