Circular Music Interval Diagram

Tags:

After studying John Coltrane's wholetone/chromatic circular diagram, I was determined to make a program to show musical intervals around a circle. And if I could, to make a concentric "inner" ring with note offsets, like Coltrane did. So the weekend arrived and I got busy...

tl;dr: circle-intervals ~ Images at the bottom

Let's dive in!

First up is the standard perl preamble:

#!/usr/bin/env perl
use strict;
use warnings;

Next is importing the functionality we will use from a couple other modules:

use List::SomeUtils qw(first_index);
use Math::Trig ();
use Music::Scales qw(get_scale_notes);
use SVG qw(title);

We will use a few trig constants and the main scale name:

use constant PI     => 2 * atan2(1,0);
use constant HALF   => PI / 2;
use constant DOUBLE => 2 * PI;
use constant SCALE  => 'chromatic';

Here we grab any command-line user arguments and fall back to a default for each:

my $interval   = shift || 1;    # interval to calculate: default chromatic 1-11
my $show_marks = shift || 12;   # how many circular note marks to display 2-60
my $show_inner = shift // 0;    # show the inner ring?
my $flat       = shift // 0;    # show note names with flats: default sharps
my $numeric    = shift // 0;    # display notes as pitch numbers
my $outer_note = shift || 'C';  # starting outer ring note
my $inner_note = shift || 'C#'; # starting inner ring note
my $diameter   = shift || 512;  # the diameter of the circle
my $fill       = shift || 'white';
my $outer_line = shift || 'green';
my $inner_line = shift || 'gray';
my $text_line  = shift || 'black';

We set a number of parameters that define the behavior of the program:

my $total_marks = 60;           # maximum number of interval markers 2-60
my $font_size   = 20;           # size of the caption font
my $border_size = 10;           # chart margin
my $sub_radius  = 11;           # radius for sub-circle markings
my $radius      = $diameter / 2;
my $frame_size  = $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{$interval}, Notes: $show_marks";
my $title   = 'Circular Music Intervals';
my $desc    = "Show $show_marks marks around a note circle for the $named{$interval} interval";

And we are going to use SVG for this task:

my $svg = SVG->new(
    width  => $frame_size,
    height => $frame_size,
);
$svg->title()->cdata($title);
$svg->desc(id => 'document-desc')->cdata($desc);

my $outer_style = $svg->group(
    id    => 'outer-style-group',
    style => {
        stroke => $outer_line,
        fill   => $fill,
    },
);

And now for the meat of the program!

There are two separate, concentric interval rings - an outer and an inner. We render the outer ring first, but before we can, a scale of notes must be aquired. With that in hand, we construct labels for our intervals (i.e. note names).

my @outer_scale = get_scale_notes($outer_note, SCALE, undef, $flat ? 'b' : '#');

my @outer_labels = get_labels(\@outer_scale, $interval, $show_marks);

Now for our main circle:

$outer_style->circle(
    cx => $frame_size / 2,
    cy => $frame_size / 2,
    r  => $radius,
    id => 'style-group-outer-circle',
);

If we are not showing the inner ring, render a caption in the middle of the circle, describing what we are going to be looking at:

$outer_style->text(
    id    => 'style-group-outer-caption',
    x     => $frame_size / 2 - $sub_radius * 10,
    y     => $frame_size / 2,
    style => {
        stroke      => $text_line,
        'font-size' => $font_size,
    },
    -cdata => $caption,
) if !$show_inner;

Now for the loop that actually draws the interval positions on the outer ring... But first we have to set up a couple things - a counter and an array of the positions to render:

my $i = 0; # counter

my @marks = map { $_ * $total_marks / $show_marks } 1 .. $show_marks;
my $fract = ($marks[1] - $marks[0]) / 2;

The $fract variable is the difference between two marks, and is used to render the innder ring.

Now for the loop! A coordinate is computed based on the current position, a small circle is drawn for each interval position, and the name of the note (or the pitch number) is added:

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,
        cy => $p->[1] + $sub_radius,
        r  => $sub_radius,
    );

    my $item = $outer_labels[ $i % @outer_labels ];
    my $text = $numeric
        ? first_index { $_ eq $item } @outer_scale
        : $item;
    $outer_style->text(
        id => $i . '-style-group-outer-sub-text',
        x  => $p->[0] + $sub_radius - ($sub_radius / 2),
        y  => $p->[1] + $sub_radius + ($sub_radius / 2),
    )->cdata($text);
}

Okay. With the outer ring drawn, we move on to the inner ring. This is mostly identical in how it is rendered. The only difference is that, by default, it uses a chromatic scale one halfstep above the starting note on the outer ring. The starting notes can be given on the command-line, by the way.

if ($show_inner) {
    my $inner_style = $svg->group(
        id    => 'inner-style-group',
        style => {
            stroke => $inner_line,
            fill   => $fill,
        },
    );

Next we do pretty much the same thing as with the outer ring to the inner ring: Generate arrays for note intervals, labels and positions, and then add them to the growing SVG diagram. But first we add the inner circle and caption.

    my @inner_scale = get_scale_notes($inner_note, SCALE, undef, $flat ? 'b' : '#');

    my @inner_labels = get_labels(\@inner_scale, $interval, $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      => $text_line,
            'font-size' => $font_size,
        },
        -cdata => $caption,
    );

With the inner circle in place, we procede to add the notes to the ring:

    $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,
            cy => $p->[1] + $sub_radius * 4,
            r  => $sub_radius,
        );

        my $item = $inner_labels[ $i % @inner_labels ];
        my $text = $numeric
            ? first_index { $_ eq $item } @inner_scale
            : $item;
        $inner_style->text(
            id => $i . '-style-group-inner-sub-text',
            x  => $p->[0] + $sub_radius * 3 + 3,
            y  => $p->[1] + $sub_radius * 3 + 3 + $sub_radius,
        )->cdata($text);
    }
}

Finally, we output the SVG we have created:

print $svg->xmlify;

There are two subroutines that are used. One is to gather the note labels to render. The other is to return the exact coordinate to go on a ring.

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;

    # Replace the time value with the polar coordinate
    my $coord = [
        $radius + $radius * cos($analog),
        $radius + $radius * sin($analog)
    ];

    return $coord;
}

So what do these diagrams look like? Here are some examples produced with the preceeding commands:

$ perl circle-intervals 1 24 1 > circle-intervals-01.svg

halfstep

$ perl circle-intervals 7 12 1 0 0 C F > circle-intervals-02.svg

perfect 5th

$ perl circle-intervals 4 12 0 0 1 > circle-intervals-03.svg

numeric

Here is Coltrane's wholetone/chromatic diagram:

$ perl circle-intervals 2 30 1 1 > circle-intervals-04.svg

wholetone