# -*- Perl -*-
#
# Scala scale (musical tuning and temperament) support for Perl, based
# on specification at: https://www.huygens-fokker.org/scala/
#
# Ratio to cent and cent to ratio equations lifted from "Musimathics,
# volume 1", pp. 45-46. MIDI conversion probably from wikipedia.

package Music::Scala;

use 5.010000;
use strict;
use warnings;

use Carp qw/croak/;
use File::Basename qw/basename/;
use Moo;
use namespace::clean;
use Scalar::Util qw/looks_like_number reftype/;

our $VERSION = '1.08';

##############################################################################
#
# ATTRIBUTES
#
# NOTE that much of the Moo setup (getters/setters, how "notes" handled,
# etc) is to preserve compatibility with how the code worked pre-Moo.
# Additional hilarity stemmed from (the mistake of?) offering multiple
# methods to get/set the same data in different guises (notes (as cents
# or ratios), (notes as) cents, (notes as) ratios).

has binmode => (
    is        => 'rw',
    predicate => 1,               # has_binmode
    reader    => 'get_binmode',
    writer    => 'set_binmode',
);

has concertfreq => (
    is      => 'rw',
    default => sub { 440 },
    isa     => sub {
        die 'frequency must be a positive number (Hz)'
          if !defined $_[0]
          or !looks_like_number $_[0]
          or $_[0] <= 0;
    },
    reader => 'get_concertfreq',
    writer => 'set_concertfreq',
);

has concertpitch => (
    is      => 'rw',
    default => sub { 69 },
    isa     => sub {
        die 'pitch must be a positive number'
          if !defined $_[0]
          or !looks_like_number $_[0]
          or $_[0] <= 0;
    },
    reader => 'get_concertpitch',
    writer => 'set_concertpitch',
);

has description => (
    is      => 'rw',
    default => sub { '' },
    isa     => sub {
        die 'description must be string value'
          if !defined $_[0]
          or defined reftype $_[0];
    },
    reader => 'get_description',
    writer => 'set_description',
);

# Sanity on scala scale file reads; other prudent limits with untrusted
# input would be to check the file size, and perhaps to bail if the note
# count is some absurd value.
has MAX_LINES => (
    is      => 'rw',
    default => sub { 3000 },
);

has notes => (
    is        => 'rw',
    clearer   => 1,
    predicate => 1,      # has_notes
);

##############################################################################
#
# METHODS

sub BUILD {
    my ($self, $param) = @_;

    if (exists $param->{file} and exists $param->{fh}) {
        die "new accepts only one of the 'file' or 'fh' arguments\n";
    }

    if (exists $param->{file}) {
        $self->read_scala(file => $param->{file});
    } elsif (exists $param->{fh}) {
        $self->read_scala(fh => $param->{fh});
    }
}

# Absolute interval list to relative (1 2 3 -> 1 1 1)
sub abs2rel {
    my $self = shift;
    return if !@_;
    my @result = $_[0];
    if (@_ > 1) {
        for my $i (1 .. $#_) {
            push @result, $_[$i] - $_[ $i - 1 ];
        }
    }
    return @result;
}

sub cents2ratio {
    my ($self, $cents, $precision) = @_;
    croak 'cents must be a number' if !looks_like_number $cents;
    if (defined $precision) {
        croak 'precision must be a positive integer'
          if !looks_like_number $precision or $precision < 0;
        $precision = int $precision;
    } else {
        $precision = 2;
    }

    return sprintf "%.*f", $precision, 10**($cents / 3986.31371386484);
}

# MIDI calculation, for easy comparison to scala results
sub freq2pitch {
    my ($self, $freq) = @_;
    croak 'frequency must be a positive number'
      if !looks_like_number $freq
      or $freq <= 0;

    # no precision, as assume pitch numbers are integers
    return sprintf '%.0f',
      $self->get_concertpitch +
      12 * (log($freq / $self->get_concertfreq) / 0.693147180559945);
}

sub get_cents {
    my ($self) = @_;
    croak 'no scala loaded' if !$self->has_notes;
    return $self->notes2cents(@{ $self->notes });
}

sub get_notes {
    my ($self) = @_;
    croak 'no scala loaded' if !$self->has_notes;
    return @{ $self->notes };
}

sub get_ratios {
    my ($self) = @_;
    croak 'no scala loaded' if !$self->has_notes;
    return $self->notes2ratios(@{ $self->notes });
}

sub interval2freq {
    my $self = shift;
    croak 'no scala loaded' if !$self->has_notes;

    my @ratios = $self->notes2ratios(@{ $self->notes });

    my @freqs;
    for my $i (ref $_[0] eq 'ARRAY' ? @{ $_[0] } : @_) {
        if ($i == 0) {    # special case for unison (ratio 1/1)
            push @freqs, $self->get_concertfreq;
        } else {
            my $is_dsc = $i < 0 ? 1 : 0;

            # for non-"octave" portion, if any
            my $offset = $i % @ratios;

            # "Octave" portion, if any - how many times the interval
            # passes through the complete scale
            my $octave_freq  = 0;
            my $octave_count = abs int $i / @ratios;

            # if non-octave on a negative interval, go one octave past
            # the target, then use the regular ascending logic to
            # backtrack to the proper frequency
            $octave_count++ if $is_dsc and $offset != 0;

            if ($octave_count > 0) {
                my $octaves_ratio = $ratios[-1]**$octave_count;
                $octaves_ratio = 1 / $octaves_ratio if $is_dsc;
                $octave_freq   = $self->get_concertfreq * $octaves_ratio;
            }

            my $remainder_freq = 0;
            if ($offset != 0) {
                $remainder_freq =
                  ($octave_freq || $self->get_concertfreq) * $ratios[ $offset - 1 ];

                # zero as remainder is based from $octave_freq, if
                # relevant, so already includes such
                $octave_freq = 0;
            }

            push @freqs, $octave_freq + $remainder_freq;
        }
    }

    return @freqs;
}

sub is_octavish {
    my $self = shift;
    croak 'no scala loaded' if !$self->has_notes;

    my @ratios = $self->notes2ratios(@{ $self->notes });

    # not octave bounded (double the frequency, e.g. 440 to 880)
    return 0 if $ratios[-1] != 2;

    my $min;
    for my $r (@ratios) {
        # don't know how to handle negative ratios
        return 0 if $r < 0;

        # multiple scales within the same definition file (probably for
        # instruments that have two different scales in the same
        # frequency domain) - but don't know how to handle these
        return 0 if defined $min and $r <= $min;

        $min = $r;
    }

    return 1;
}

sub notes2cents {
    my $self = shift;

    my @cents;
    for my $n (ref $_[0] eq 'ARRAY' ? @{ $_[0] } : @_) {
        if ($n =~ m{([0-9]+)/([1-9][0-9]*)}) {
            push @cents, 1200 * ((log($1 / $2) / 2.30258509299405) / 0.301029995663981);
        } else {
            push @cents, $n;
        }
    }

    return @cents;
}

sub notes2ratios {
    my $self = shift;

    my @ratios;
    for my $n (ref $_[0] eq 'ARRAY' ? @{ $_[0] } : @_) {
        if ($n =~ m{([0-9]+)/([1-9][0-9]*)}) {
            push @ratios, $1 / $2;    # ratio, as marked with /
        } else {
            push @ratios, 10**($n / 3986.31371386484);
        }
    }

    return @ratios;
}

# MIDI for comparison, the other way
sub pitch2freq {
    my ($self, $pitch) = @_;
    croak "pitch must be MIDI number"
      if !looks_like_number $pitch
      or $pitch < 0;

    return $self->get_concertfreq * (2**(($pitch - $self->get_concertpitch) / 12));
}

sub ratio2cents {
    my ($self, $ratio, $precision) = @_;
    croak 'ratio must be a number' if !looks_like_number $ratio;
    if (defined $precision) {
        croak 'precision must be a positive integer'
          if !looks_like_number $precision or $precision < 0;
        $precision = int $precision;
    } else {
        $precision = 2;
    }

    return sprintf "%.*f", $precision,
      1200 * ((log($ratio) / 2.30258509299405) / 0.301029995663981);
}

sub read_scala {
    my $self = shift;
    my %param;
    if (@_ == 1) {
        $param{file} = $_[0];
    } else {
        %param = @_;
    }

    my $fh;
    if (exists $param{file}) {
        open($fh, '<', $param{file}) or croak 'open failed: ' . $!;
    } elsif (exists $param{fh}) {
        $fh = $param{fh};
    } else {
        croak 'must specify file or fh parameter to read_scala';
    }
    if (exists $param{binmode}) {
        binmode $fh, $param{binmode} or croak 'binmode failed: ' . $!;
    } elsif ($self->has_binmode) {
        binmode $fh, $self->get_binmode or croak 'binmode failed: ' . $!;
    }

    my (@scala, $line_count);
    while (!eof($fh)) {
        my $line = readline $fh;
        croak 'readline failed: ' . $! unless defined $line;
        croak 'input exceeds MAX_LINES' if ++$line_count >= $self->MAX_LINES;
        next if $line =~ m/^[!]/;    # skip comments

        chomp $line;
        push @scala, $line;

        last if @scala == 2;
    }
    # but as might hit the MAX_LINES or eof() instead check again...
    if (@scala != 2) {
        croak 'missing description or note count lines';
    }

    $self->set_description(shift @scala);
    my $NOTECOUNT;
    if ($scala[-1] =~ m/^\s*([0-9]+)/) {
        $NOTECOUNT = $1;
    } else {
        croak 'could not parse note count';
    }

    my @notes;
    my $cur_note = 1;
    while (!eof($fh)) {
        my $line = readline $fh;
        croak 'readline failed: ' . $! unless defined $line;
        croak 'input exceeds MAX_LINES' if ++$line_count >= $self->MAX_LINES;
        next if $line =~ m/^[!]/;    # skip comments

        # All the scales.zip *.scl files as of 2013-02-19 have digits on
        # both sides of the dot (so there are no ".42" cent values, but
        # the "these are all valid pitch lines" does include a "408." as
        # allowed). Some scale files have negative cents, though that is
        # illegal for ratios. All the ratios are plain numbers (no
        # period), or if they have a slash, it is followed by another
        # number (so no "42/" cases). Checked via various greps on the
        # file contents.
        if ($line =~ m/^\s* ( -?[0-9]+\. [0-9]* ) /x) {
            push @notes, $1;    # cents
        } elsif ($line =~ m{^\s* -[0-9] }x) {
            # specification says these "should give a read error"
            croak 'invalid negative ratio in note list';
        } elsif ($line =~ m{^\s* ( [1-9][0-9]* (?:/[0-9]+)? ) }x) {
            my $ratio = $1;
            $ratio .= '/1' if $ratio !~ m{/};    # implicit qualify of ratios
            push @notes, $ratio;
        } else {
            # Nothing in the spec about non-matching lines, so blow up.
            # However, there are six files in scales.zip that have
            # trailing blank lines, though these blank lines occur only
            # after an appropriate number of note entries. So must exit
            # loop before reading those invalid? lines. (Did mail the
            # author about these, so probably has been rectified.)
            croak 'invalid note specification on line ' . $.;
        }

        last if $cur_note++ >= $NOTECOUNT;
    }
    if (@notes != $NOTECOUNT) {
        croak 'expected ' . $NOTECOUNT . ' notes but got ' . scalar(@notes) . " notes";
    }

    # edge case: remove any 1/1 (zero cents) at head of the list, as
    # this implementation treats that as implicit
    shift @notes if sprintf("%.0f", $self->notes2cents($notes[0])) == 0;

    $self->notes(\@notes);

    return $self;
}

# Relative interval list to absolute (1 1 1 -> 1 2 3)
sub rel2abs {
    my $self = shift;
    return if !@_;
    my @result = $_[0];
    if (@_ > 1) {
        for my $i (1 .. $#_) {
            push @result, $result[-1] + $_[$i];
        }
    }
    return @result;
}

# Given list of frequencies, assume first is root frequency, then
# convert the remainder of the frequencies to cents against that first
# frequency.
sub set_by_frequency {
    my $self  = shift;
    my $freqs = ref $_[0] eq 'ARRAY' ? $_[0] : \@_;
    croak 'need both root and other frequencies' if @$freqs < 2;
    croak 'root frequency must not be zero'      if $freqs->[0] == 0;

    my @notes;
    for my $i (1 .. $#{$freqs}) {
        push @notes,
          1200 *
          ((log($freqs->[$i] / $freqs->[0]) / 2.30258509299405) / 0.301029995663981);
    }

    # edge case: remove any 1/1 (zero cents) at head of the list, as
    # this implementation treats that as implicit
    shift @notes if sprintf("%.0f", $self->notes2cents($notes[0])) == 0;

    $self->notes(\@notes);

    return $self;
}

sub set_notes {
    my $self = shift;
    my @notes;
    for my $n (ref $_[0] eq 'ARRAY' ? @{ $_[0] } : @_) {
        if ($n =~ m{^ -?[0-9]+\. (?:[0-9]+)? $}x) {
            push @notes, $n;
        } elsif ($n =~ m{^ [1-9][0-9]* (?:/[0-9]+)? $}x) {
            my $ratio = $n;
            $ratio .= '/1' if $ratio !~ m{/};    # implicit qualify of ratios
            push @notes, $ratio;
        } else {
            croak 'notes must be integer ratios or real numbers';
        }
    }

    # edge case: remove any 1/1 (zero cents) at head of the list, as
    # this implementation treats that as implicit
    shift @notes if sprintf("%.0f", $self->notes2cents($notes[0])) == 0;

    $self->notes(\@notes);
    return $self;
}

sub write_scala {
    my $self = shift;
    croak 'no scala loaded' if !$self->has_notes;

    my %param;
    if (@_ == 1) {
        $param{file} = $_[0];
    } else {
        %param = @_;
    }

    my $fh;
    if (exists $param{file}) {
        open($fh, '>', $param{file}) or croak 'open failed: ' . $!;
    } elsif (exists $param{fh}) {
        $fh = $param{fh};
    } else {
        croak 'must specify file or fh parameter to write_scala';
    }
    if (exists $param{binmode}) {
        binmode $fh, $param{binmode} or croak 'binmode failed: ' . $!;
    } elsif ($self->has_binmode) {
        binmode $fh, $self->get_binmode or croak 'binmode failed: ' . $!;
    }

    my $filename = basename($param{file})
      if exists $param{file};
    my $note_count = @{ $self->notes } || 0;

    say $fh defined $filename
      ? "! $filename"
      : '!';
    say $fh '!';
    say $fh $self->get_description;
    say $fh ' ', $note_count;
    say $fh '!';    # conventional comment between note count and notes

    for my $note (@{ $self->notes }) {
        say $fh ' ', $note;
    }

    return $self;
}

1;
__END__

##############################################################################
#
# DOCS

=head1 NAME

Music::Scala - Scala scale support for Perl

=head1 SYNOPSIS

  use Music::Scala ();
  my $scala = Music::Scala->new;

  $scala->set_binmode(':encoding(iso-8859-1):crlf');
  $scala->read_scala('groenewald_bach.scl');

  $scala->get_description;   # "Jurgen Gronewald, si..."
  $scala->get_notes;         # (256/243, 189.25008, ...)
  $scala->get_cents;
  $scala->get_ratios;

  # which interval is what frequency given the loaded scala data
  # and the given concert frequency (A440 if not changed)
  $scala->set_concertfreq(422.5);
  $scala->interval2freq(0, 1); # (422.5, 445.1)

  $scala->set_description('Heavenly Chimes');
  # by ratio; these are strings that get parsed internally
  $scala->set_notes(qw{ 32/29 1/2 16/29 });
  $scala->write_scala('chimes.scl');

  # or by cents -- mark well the quoting on 1200; Perl will map
  # things like a bare 1200.000 to '1200' which then becomes the
  # ratio 1200/1 which is wrong.
  $scala->set_notes(250.9, 483.3, 715.6, 951.1, '1200.0');

  # utility MIDI equal temperament algorithms
  $scala->pitch2freq(69);
  $scala->freq2pitch(440);

This list is incomplete. See also the C<eg/> and C<t/> directories of
the distribution of this module for example code.

=head1 DESCRIPTION

Scala scale support for Perl: reading, writing, setting, and interval to
frequency conversion methods are provided. The L</"SEE ALSO"> section
links to the developer pages for the original specification, along with
an archive of scala files that define various tunings and temperaments.

=head2 SEVERAL WORDS REGARDING FLOATING POINT NUMBERS

Frequencies derived from scala scale calculations will likely need to be
rounded due to floating point number representation limitations:

  # octave, plus default concert pitch of 440, so expect 880
  my $scala = Music::Scala->new->set_notes('1200.000');

  $scala->interval2freq(1);   # 879.999999999999        (maybe)

  sprintf "%.*f", 0, $scala->interval2freq(1);   # 880  (for sure)

For more information, see:

L<http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html> "What
Every Computer Scientist Should Know About Floating-Point Arithmetic".
David Goldberg, Computing Surveys, March 1991.

=head1 CONSTRUCTOR

The B<new> method accepts any of the L</"ATTRIBUTES"> as well as
optionally one of the B<file> => I<filename> or B<fh> => I<filehandle>
arguments to read a scala scale file from.

  $scala = Music::Scala->new(
    binmode => ':encoding(iso-8859-1):crlf',
    file    => 'foo.scl',
  );

=head1 ATTRIBUTES

Moo attributes via C<has> statements. These may throw exceptions if bad
data is passed (negative frequencies or the like). The sporadic use of
B<get_*> and B<set_*> accessors is due to compatibility with older
versions of this module. These may also be specified to B<new>:

  # equivalent ways to do the same thing
  $scala = Music::Scala->new( concertfreq => 443 );
  $scala->set_concertfreq(443);

=over 4

=item B<binmode>

Holds the default C<binmode> layer used in the B<read_scala> and
B<write_scala> methods (unless a custom I<binmode> argument is passed to
those calls). The scala scale files from C<www.huygens-fokker.org> tend
to be in the ISO 8859-1 encoding, mostly for the description and other
such metadata fields. Note that Perl on Windows systems tends to turn on
C<:crlf>. For scala scale files, it probably should be specified,
regardless of the operating system. Therefore, a reasonable default to
set might be:

  $scala->set_binmode(':encoding(iso-8859-1):crlf');

Though this module does nothing by default for encoding.

=item B<concertfreq>

The concert frequency. C<440> (Hz) is the default. Use
B<get_concertfreq> or B<set_concertfreq> to obtain or change it.

=item B<concertpitch>

Holds the MIDI pitch number that the I<concertfreq> maps to.
C<69> by default (as that is the MIDI number of A440).

=item B<description>

Holds the description of the scala data. This will be the empty string
if no description was read or set prior.

=item B<MAX_LINES>

Gets or sets the maximum lines to allow in an input scala scale file,
C<3000> by default.

=item B<notes>

Gets or sets the notes from the scala scale data loaded, if any. Mostly
for internal use; the B<get_cents>, B<get_notes>, or B<get_ratios>
methods are likely better means to access this information, and the
B<read_scala>, B<set_by_frequency>, or B<set_notes> methods better ways
to set it.

=back

=head1 METHODS

Methods will throw exceptions under various conditions, mostly related
to bad input or scala scale data not being loaded.

=over 4

=item B<abs2rel> I<interval-list>

Takes a list of intervals assumed to be absolute (which is the format
the scala scale files are in) and returns the relative delta between
those intervals as a list.

=item B<cents2ratio> I<cents>, [ I<precision> ]

Converts a value in cents (e.g. C<1200>) to a ratio (e.g. C<2>). An
optional precision for C<sprintf> can be supplied; the default precision
is C<2>. There are C<1200> cents in an octave (a doubling of the
frequency).

=item B<freq2pitch> I<frequency>

Converts the passed frequency (Hz) to the corresponding MIDI pitch
number using the MIDI algorithm, as influenced by the B<concertfreq>
attribute. Unrelated to scala, but handy for comparison with results
from B<interval2freq>.

This method *is not* influenced by the scala scale data, and always uses
equal temperament. See also B<pitch2freq>.

=item B<get_binmode>

Returns the current value of the B<binmode> attribute.

=item B<get_cents>

Returns, as a list, the "notes" of the scala scale data, except
converted to cents ("notes" may either be ratios or values in
cents; this method ensures that they are all represented in cents).
Throws an exception if the notes have not been set by some previous
method call (one of the B<read_scala>, B<set_by_frequency>, or
B<set_notes> methods).

=item B<get_concertfreq>

Returns the current concert frequency (440 Hz by default).

=item B<get_concertpitch>

Returns the current concert pitch (69 by default, MIDI number).

=item B<get_description>

Returns the description of the scala scale data, if any.

=item B<get_notes>

Returns, as a list, the "notes" of the scala, but throws an exception if
this field has not been set by some previous method. The notes are
either real numbers (representing values in cents, or 1/1200 of an
octave (these may be rarely be negative)) or otherwise integer ratios
(e.g. C<3/2> or C<2> (these may not be negative)).

  $scala->read_scala(file => $some_file);
  my @notes = $scala->get_notes;
  if (@notes == 12) { ...

The implicit C<1/1> for unison is not contained in the list of
notes; the first element is for the 2nd degree of the scale (e.g.
the minor second of a 12-tone scale). Other implementations may
differ in this regard.

=item B<get_ratios>

Returns, as a list, the "notes" of the scala, except converted to ratios
("notes" may either be ratios or values in cents; this method ensures
that these values are returned as ratios). Throws an exception if the
notes have not been set by some previous method call.

=item B<interval2freq> I<intervals ...>

Converts a list of passed interval numbers (as a list or a single array
reference) to frequencies (in Hz) that are returned as a list. Interval
numbers are integers, C<0> for unison (the B<concertfreq>), C<1> for the
first interval (which would be a "minor 2nd" for a 12-note scale, but
something different for scales of different composition), and so on up
to the "octave." Negative intervals take the frequency in the other
direction, e.g. C<-1> for what in a 12-note system would be a minor 2nd
downwards. Intervals past the "octave" consist of however many "octaves"
are present in the scale, plus whatever remainder lies inside that
"octave," if any. "octave" uses scare quotes due to 13% of the scala
archive consisting of non-octave bounded scales; that is, scales that do
not repeat themselves when the frequency is doubled (see the
B<is_octavish> method for a test for that condition).

Conversions are based on the I<concertfreq> setting, which is 440Hz by
default. Use B<set_concertfreq> method to adjust this. An example that
derives the frequencies of C4 through B4 using the equal temperament
tuning file from the scala scale file archive:

  $scala->read_scala('equal.scl');
  $scala->set_concertfreq(261.63);
  my @freqs = map { sprintf '%.2f', $_ } 
    $scala->interval2freq(qw/0 1 2 3 4 5 6 7 8 9 10 11/);

Some scala files note what this value should be in the comments or
description, or it may vary based on the needs of the software or
instruments involved.

There is no error checking for nonsense conditions: an interval of a
15th makes no sense for a xylophone with only 10 keys in total. Audit
the contents of the scala scale file to learn what its limits are or
screen for appropriate scales depending on the application.

=item B<is_octavish>

Returns true if the scala scale has an ultimate ratio of 2:1, as well as
no negative or repeated ratios; false otherwise. Throws an exception if
no scala scale is loaded.

=item B<notes2cents> I<notes ...>

Given a list of notes, returns a list of corresponding cents. Used
internally by the B<get_cents> method.

=item B<notes2ratios> I<notes ...>

Given a list of notes, returns a list of corresponding ratios. Used
internally by the B<get_ratios> and B<interval2freq> methods.

=item B<pitch2freq> I<MIDI_pitch_number>

Converts the given MIDI pitch number to a frequency using the MIDI
conversion algorithm, as influenced by the I<concertfreq> setting.

This method *is not* influenced by the scala scale data, and always uses
equal temperament. See also B<freq2pitch>.

=item B<ratio2cents> I<ratio>, [ I<precision> ]

Converts a ratio (e.g. C<2>) to a value in cents (e.g. C<1200>). An
optional precision for C<sprintf> can be supplied; the default
precision is C<2>.

=item B<read_scala> I<filename>

Parses a scala file. Will throw some kind of exception if anything at
all is wrong with the input. Use the appropriate C<get_*> methods to
obtain the scala data thus parsed. Comments in the input file are
ignored, so anything subsequently written using B<write_scala> will lack
those. All ratios are made implicit by this method; that is, a C<2>
would be qualified as C<2/1>.

As an alternative, accepts also I<file> or I<fh> hash keys, along with
I<binmode> as in the B<new> method:

  $scala->read_scala('somefile');
  $scala->read_scala( file => 'file.scl', binmode => ':crlf' );
  $scala->read_scala( fh   => $input_fh );

=item B<rel2abs> I<interval-list>

Takes a list of relative intervals and returns a list of absolute
intervals. Scala scale files use absolute intervals.

=item B<set_binmode> I<layer>

Sets the current value of the B<bindmode> attribute.

=item B<set_by_frequency> I<root_frequency>, I<frequencies...>

Given a root frequency as the first argument, performs the equivalent of
B<set_notes> except that it creates the intervals on the fly based on
the I<root_frequency> supplied. Handy if you have a list of frequencies,
and need those converted to cents or ratios.

=item B<set_concertfreq> I<frequency>

Set the concert frequency.

=item B<set_concertpitch> I<midi-number>

Set the concert pitch.

=item B<set_description> I<string>

Set the description of the scala scale data.

=item B<set_notes> I<array_or_array_ref>

Sets the notes. Can either be an array, or an array reference,
ideally containing values in ratios or cents as per the Scala scale
file specification, as an exception will be thrown if these ideals
are not met.

NOTE cents with no value past the decimal must be quoted in code, as
otherwise Perl converts the value to C<1200> which the code then
turns into the integer ratio C<1200/1> instead of what should be
C<2/1>. B<read_scala> does not suffer this problem, as it is looking
for the literal dot, and that is a different code path than what
happens for ratios.

  $scala->set_notes(250.9, 483.3, 715.6, 951.1, '1200.0');

=item B<write_scala> I<filename>

Writes a scala file. Will throw some kind of exception if anything at
all is wrong, such as not having scala data loaded in the object. Like
B<read_scala> alternatively accepts I<file> or I<fh> hash keys, along
with a I<binmode> option to set the output encoding.

  $scala->write_scala('out.scl');
  $scala->write_scala( file => 'out.scl', binmode => ':crlf' );
  $scala->write_scala( fh => $output_fh );

Data will likely not be written until the I<fh> passed is closed. If
this seems surprising, see L<http://perl.plover.com/FAQs/Buffering.html>
to learn why it is not.

=back

=head1 EXAMPLES

Check the C<eg/> and C<t/> directories of the distribution of this
module for example code.

=head1 BUGS

Negative cents are likely not handled well, or at all. The specification
frowns on negative ratios, but does allow for negative cents, so
converting such negative cents to ratios (which do not support negative
values) might yield unexpected or wrong results. Only 0.36% of the scala
scale archive file scales contain negative cents.

=head1 SEE ALSO

L<http://www.huygens-fokker.org/scala/> by Manuel Op de Coul, and in
particular the scala archive L<http://www.huygens-fokker.org/docs/scales.zip>
which contains many different scales to play around with.

Scales, tunings, and temperament would be good music theory topics to
read up on, e.g. chapters in "Musicmathics, volume 1" by Gareth Loy,
among other in-depth treatments stemming from the no few centuries of
development behind music theory.

=head1 AUTHOR

thrig - Jeremy Mates (cpan:JMATES) C<< <jmates at cpan.org> >>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2013 by Jeremy Mates

This module is free software; you can redistribute it and/or modify it
under the Artistic License (2.0).

=cut
