#!/usr/bin/perl -w

#
# $Id: Scales.pm,v 1.2 2002-09-10 21:46:50-05 bobn Exp bobn $
#
# provide musical scales
#

=head1

NAME

Scales.pm - provide musically-significant streams of numbers and/or text.

=cut

package Scales;

# use warnings;
use strict;

use Data::Dumper;
select STDERR; $|++;
select STDOUT; $|++;

use Storable qw(nstore store_fd nstore_fd freeze thaw dclone);

use vars qw( $VERSION %TUNINGS );

$VERSION = '0.20';
sub VERSION
{
	return $VERSION;
}

sub new
{
	my $class = shift;
	my $self = {};
	return(bless($self, $class));
}

my @notes;

@notes = split(' ', 'c c# d d# e f f# g g# a a# b' );

my (%notes, $i);
$i = 0;

for ( @notes )
{
	$notes{$_} = $i++;
}

=head1 

METHODS

=head2 

notes()

called as $m->notes('c') or $m->notes(0), return the 12-tone scale beginning with the specified note.
The examples given both output 12 tones starting with c.  Numeric input is interpreted 
as the number of half-steps from c on which to begin.

=cut

sub notes
{
	my @ret;
	shift if ref($_[0]); # save first parm if not called as method.
	my ($key, $majmin) = @_;
	$key = lc $key;
	$key = $notes{$key} if exists $notes{$key};
	die "Invalid value for key: '$key'" unless (  $key =~/^\d+$/ and $key >= 0 and $key <= 11 ) ;
	for ( 0..11 )
	{
		push @ret, $notes[ ($key + $_) % 12 ];
	}
	return @ret;
}

my %scales;


# relate name to number of 1/2 steps from tonic.
my %note_number =
(
	I		=> 0,
	ii		=> 1,
	II		=> 2,
	iii		=> 3,
	III		=> 4,
	IV		=> 5,
	'IV+'	=> 6,
	v		=> 6,
	V		=> 7,
	'V+'	=> 8,
	vi		=> 8,
	VI		=> 9,
	vii		=> 10,
	VII		=> 11,
);


%scales =
(
	all =>
	{
		'' => [ qw( 0 1 2 3 4 5 6 7 8 9 10 11 ) ],
	},
	dominant =>
	{
		seventhC => [ qw( 0 4 7 10 ) ],
		'7thC'   => [ qw( 0 4 7 10 ) ],
		ninethC => [ qw( 0 4 7 10 14) ],
		'9thC'  => [ qw( 0 4 7 10 14) ],
		thirteenthC => [ qw( 0 4 7 10 21) ],
		'13thC'     => [ qw( 0 4 7 10 21) ],
	},
	major =>
	{
		seventhC => [ qw( 0 4 7 11 ) ],
		'7thC'   => [ qw( 0 4 7 11 ) ],
		diatonic => [ qw( 0 2 4 5 7 9 11 ) ],
		'' 		 => [ qw( 0 2 4 5 7 9 11 ) ],
		pentatonic => [ qw( 0 2 4 7 9 ) ],
	},
	minor =>
	{
		diatonic => [ qw( 0 2 3 5 7 8 10 ) ],
		''       => [ qw( 0 2 3 5 7 8 10 ) ],
		harmonic => [ qw( 0 2 3 5 7 8 11 ) ],
		melodic =>  [ qw( 0 2 3 5 7 9 11 ) ],
		pentatonic => [ qw( 0 3 5 7 10 ) ],
		seventhC => [ qw( 0 3 7 10 ) ],
		'7thC'   => [ qw( 0 3 7 10 ) ],
	},
	wholetone => 
	{
		'' => [ qw( 0 2 4 6 8 10 ) ]
	},
	diminished =>
	{
		'' => [ qw( 0 2 3 5 6 8 9 11 ) ],
	},
	diminishedC =>
	{
		'' => [ qw( 0 3 6 9 ) ],
	},
#	mixolydian =>
#	{

#		''		=> [ qw( 0 2 4 5 7 9 10) ],
#	},
);

my $new_scales = dclone(\%scales);
# print Dumper($new_scales);

my @out_scales = ();;

sub do_list_scales
{
	_list_scales(\%scales, ());
	return @out_scales;
}

sub _list_scales
{
	my ( $hr, @out ) = @_;
	for my $key ( keys %$hr )
	{
		if ( ref($hr->{$key}) eq 'HASH' )
		{
			_list_scales( $hr->{$key}, @out, $key);
		}
		else
		{
			push @out_scales, join(',',  @out, $key);
		}
	}
}

=head2

define_scale

Allows user program to define a scale for later use.

Example:

    $m->define_scale('mixolydian', '', ' I II III IV V VI vii  ' );

or:

    $m->define_scale('mixolydian', '', ' 0 2 4 5 7 9 10  ' );


When called with numbers, they are tekn to be the number of 
half-stpes from the tonic.

note: this modifies class data, so all further uses of the module by this process 
will be affected.  
Is this a "feature"? Probably not in the mod_perl context.

Note that this could be used to define the notes of chords, rather than scales,
e.g.: 

	$m->define_scale('minor,9th', ' 0 3 7 10 14 ' );

but note that the program will render the 9th as 2nds, also.

=cut

sub define_scale
{
	shift if ref($_[0]); # save first parm if not called as method.
	my ($type, $subtype, @notes) = @_;
	if ( scalar(@notes) == 1) { @notes = split(' ', $notes[0]) };

	@notes = map { ( exists $note_number{$_} ? $note_number{$_} : $_ ) } @notes;

	$scales{$type}{$subtype} = [ @notes ];
}

# use of define_scale
define_scale('mixolydian', '', ' I II III IV V VI vii  ' );




# return an array of the notes in this scale.
sub scale
{
	shift if ref($_[0]); # save first parm if not called as method.
	my ( $key, $type, $subtype ) = @_;
	my (@vals, @ret);
	my @notes = notes($key);

	if ( exists $scales{$type} and exists $scales{$type}{$subtype} )
	{
		@vals = @{$scales{$type}{$subtype}};
	}
	else
	{
		die "no scale defined for typs: '$type' and subtype: '$subtype'\n";
	}

	for ( @vals )
	{
		push @ret, $notes[ $_  % 12 ];
	}
	return @ret;
}

sub show_all
{
	print Data::Dumper->Dump( 
		[ \%scales, \%note_number, ],
		[ qw(*scales *note_number) ],
		);
}

1;

__END__

#
# $Log: Scales.pm,v $
# Revision 1.2  2002-09-10 21:46:50-05  bobn
# whatever
#
# Revision 1.1  2002-09-05 21:27:55-05  bobn
# Initial revision
#
#

