package MIDI::Music;

use strict;
use Carp;
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK $AUTOLOAD);
use Fcntl;

require Exporter;
require DynaLoader;
require AutoLoader;

@ISA = qw(Exporter DynaLoader);
@EXPORT = qw();
$VERSION = '0.01';

sub AUTOLOAD {

    my $constname;
    ($constname = $AUTOLOAD) =~ s/.*:://;
    croak "& not defined" if $constname eq 'constant';
    my $val = constant($constname, @_ ? $_[0] : 0);
    if ($! != 0) {
	if ($! =~ /Invalid/) {
	    $AutoLoader::AUTOLOAD = $AUTOLOAD;
	    goto &AutoLoader::AUTOLOAD;
	}
	else {
		croak "Your vendor has not defined MIDI::Music macro $constname";
	}
    }
    no strict 'refs';
    *$AUTOLOAD = sub { $val };
    goto &$AUTOLOAD;
}

bootstrap MIDI::Music $VERSION;

sub DESTROY {
    my $mm = shift;
    $mm->close() if ($mm->{'_initialized'});
}

################################################################################
################################# Constructor ##################################
sub new {

    my $class = shift;
    my %param = @_;

    ######################################
    # Device number (0 == first available)
    my $device = $param{'device'} || 0;    # Device number

    ##############################
    # Recording-related parameters
    my $readbuf  = $param{'readbuf'}  || 4096; # events per read * 8
    #my $actsense = $param{'actsense'} || 0;    # enable active sensing
    my $realtime = $param{'realtime'} || 0;    # realtime messages on
    #my $timing   = $param{'timing'}   || 0;    # timer on (requires rt(?))

    #############################
    # Playback-related parameters
    #my $extbuf      = $param{'extbuf'}  || 0;    # ?? not documented
    my $gmdrum      = $param{'gmdrum'}  || [];   # Drums to cache
    my $gminstr     = $param{'gminstr'} || [];   # Patches to cache

    ###################################
    # Initial timing parameters
    my $timebase = $param{'timebase'} || 96;  # Ticks per quarter-note
    my $tempo    = $param{'tempo'}    || 120; # BPM
    my $timesig  = $param{'timesig'}  || [0x04, 0x02, 0x18, 0x08]; # 4/4

    my $ppqn_per_clock = $timebase / 24; # Clocks (pulses) per quarter-note

    my $self = {'_device'      => $device,
                '_errstr'      => '',
                '_initialized' => 0,

                '_event_times' => [], # For the storage of
                '_midistruct'  => {}, #   MIDI file data

                '_readbuf'   => $readbuf,
                #'_actsense'  => $actsense,
                '_realtime'  => $realtime,
                #'_timing'    => $timing,
                '_rec_dtime' => 0, # if timing is enabled, this is
                                   #   calculated and supplied as
                                   #   1th element of text events

                #'_extbuf'         => $extbuf,
                '_gmdrum'         => $gmdrum,
                '_gminstr'        => $gminstr,

                '_tempo'          => $tempo,
                '_timebase'       => $timebase,
                '_ppqn_per_clock' => $ppqn_per_clock,

                '_timesig' => $timesig,
                };

    return bless $self, $class;
}

################################################################################
########################### Misc. ##############################################
sub errstr {
    my $mm = shift;
    return $mm->{'_errstr'};
}


################################################################################
####################### MIDI File Methods ######################################
sub _clear {

    my $mm = shift;
    $mm->{'_midistruct'}  = {};
    $mm->{'_event_times'} = [];
}

sub _loadmidifile {

    my $mm       = shift;
    my $midifile = shift;

    if (eval 'require MIDI') {

        my $opus = MIDI::Opus->new({'from_file' => $midifile});

        $mm->{'_timebase'} = $opus->ticks();
        my $format         = $opus->format();
        my $pos            = 0;

        foreach my $track (@{$opus->tracks_r}) {

            my $events_r = $track->events_r;
            $pos         = 0 unless ($format == 2);

            foreach my $event (@{$events_r}) {

                $pos += $event->[1]; # Add current dtime to last position

                push(@{$mm->{'_midistruct'}->{$pos}}, $event);
            }
        }
        $mm->{'_event_times'} = [ sort { $a <=> $b } keys %{$mm->{'_midistruct'}} ];

    } else {
        $mm->{'_errstr'} = ref($mm) . "::_loadmidifile(): require MIDI: $!";
        return 0;
    }
    return 1;
}

sub playmidifile {

    my $mm       = shift;
    my $midifile = shift || '';

    if ($midifile) {

        unless ($mm->_loadmidifile($midifile)) {
            $mm->{'_errstr'} = ref($mm) . '::playmidifile(): ' . $mm->{'_errstr'};
            return 0;
        }

    } else {
        $mm->{'_errstr'} = ref($mm) . '::playmidifile(): no file supplied';
        return 0;
    }

    unless ($mm->{'_initialized'}) {

        unless ($mm->init('mode' => O_WRONLY)) {
            $mm->{'_errstr'} = ref($mm) . '::playmidifile(): ' . $mm->{'_errstr'};
            return 0;
        }
    }

    $mm->_playloaded() || do {
        $mm->{'_errstr'} = ref($mm) . '::playmidifile(): ' . $mm->{'_errstr'};
        return 0;
    };
    $mm->close();
    $mm->_clear();

    return 1;
}

sub _playloaded {

    my $mm    = shift;
    my $dtime = 0;
    my $last  = 0;

    my $events = [];
    for (0 .. $#{$mm->{'_event_times'}}) {

        my $pos   = $mm->{'_event_times'}->[$_];
        my $dtime = $pos - $last;

        foreach my $event (@{ $mm->{'_midistruct'}->{$pos} }) {

            $event->[1] = $dtime;
            push @$events, $event;

            $dtime = 0;
        }
        $last = $pos;
    }
    $mm->playevents($events) || do {
        $mm->{'_errstr'} = ref($mm) . '::_playloaded(): ' . $mm->{'_errstr'};
        return 0;
    };
    $mm->dumpbuf();
    return 1;
}

################################################################################
############################## Recording #######################################
sub _readevents_OSS {

    my $mm      = shift;
    my $data    = $mm->_readblock();
    my $events  = [];

    $events = [ map { [ unpack('C8', substr($data, ($_ * 8), 8)) ]
                    } (0 .. ((length($data) / 8) - 1))
              ] if ($data);

    return $events;
}

sub readevents {

    my $mm      = shift;
    my $events  = $mm->_readevents_OSS();
    my $decoded = [];

    if (@{$events}) {

        for (0 .. $#{$events}) {

            my $event      = $events->[$_];
            my $ev_decoded = [];

            if ($event->[0] == &EV_CHN_VOICE) {

                if ($event->[2] == &MIDI_NOTEON) {

                    $ev_decoded->[0] = 'note_on';
                    $ev_decoded->[1] = $mm->{'_rec_dtime'};
                    $ev_decoded->[2] = $event->[3]; # channel
                    $ev_decoded->[3] = $event->[4]; # note
                    $ev_decoded->[4] = $event->[5]; # velocity

                    $mm->{'_rec_dtime'} = 0;

                } elsif ($event->[2] == &MIDI_NOTEOFF) {

                    $ev_decoded->[0] = 'note_off';
                    $ev_decoded->[1] = $mm->{'_rec_dtime'};
                    $ev_decoded->[2] = $event->[3]; # channel
                    $ev_decoded->[3] = $event->[4]; # note
                    $ev_decoded->[4] = $event->[5]; # velocity

                    $mm->{'_rec_dtime'} = 0;

                } elsif ($event->[2] == &MIDI_KEY_PRESSURE) {

                    $ev_decoded->[0] = 'key_after_touch';
                    $ev_decoded->[1] = $mm->{'_rec_dtime'};
                    $ev_decoded->[2] = $event->[3]; # channel
                    $ev_decoded->[3] = $event->[4]; # note
                    $ev_decoded->[4] = $event->[5]; # velocity

                    $mm->{'_rec_dtime'} = 0;
                }

            } elsif ($event->[0] == &EV_CHN_COMMON) {

                if ($event->[2] == &MIDI_CHN_PRESSURE) {

                    $ev_decoded->[0] = 'channel_after_touch';
                    $ev_decoded->[1] = $mm->{'_rec_dtime'};
                    $ev_decoded->[2] = $event->[3]; # channel
                    $ev_decoded->[3] = $event->[4]; # velocity

                    $mm->{'_rec_dtime'} = 0;

                } elsif ($event->[2] == &MIDI_PGM_CHANGE) {

                    $ev_decoded->[0] = 'patch_change';
                    $ev_decoded->[1] = $mm->{'_rec_dtime'};
                    $ev_decoded->[2] = $event->[3]; # channel
                    $ev_decoded->[3] = $event->[4]; # program

                    $mm->{'_rec_dtime'} = 0;

                } elsif ($event->[2] == &MIDI_CTL_CHANGE) { # control change

                    $ev_decoded->[0] = 'control_change';
                    $ev_decoded->[1] = $mm->{'_rec_dtime'};
                    $ev_decoded->[2] = $event->[3]; # channel
                    $ev_decoded->[3] = $event->[4]; # controller
                    $ev_decoded->[4] = $event->[5]; # value

                    $mm->{'_rec_dtime'} = 0;

                } elsif ($event->[2] == &MIDI_PITCH_BEND) {

                    $ev_decoded->[0] = 'pitch_wheel_change';
                    $ev_decoded->[1] = $mm->{'_rec_dtime'};
                    $ev_decoded->[2] = $event->[3];                # channel
                    $ev_decoded->[3] = ($event->[7] * 256) - 8192; # value

                }

#### fix this #####

            } elsif ($event->[0] == &EV_SYSEX) {

                $ev_decoded->[0] = 'sysex_f0'; # ???
                $ev_decoded->[1] = $mm->{'_rec_dtime'};
                $ev_decoded->[2] = $event->[2]; # data - six bytes at a time! (stupid)

	        for (;;) {
	            my $events_oss = $mm->_readevent_OSS();
	            
	        }

                $mm->{'_rec_dtime'} = 0;

            } elsif ($event->[0] == &EV_TIMING) {

                if      ($event->[1] == &TMR_START)    {
                } elsif ($event->[1] == &TMR_STOP)     {
                } elsif ($event->[1] == &TMR_CONTINUE) {
                } elsif ($event->[1] == &TMR_WAIT_ABS) {
                } elsif ($event->[1] == &TMR_WAIT_REL) {
                } elsif ($event->[1] == &TMR_ECHO)     {
                } elsif ($event->[1] == &TMR_TEMPO)    {

#### possible need for fix here ###
#
                    $ev_decoded->[0] = 'set_tempo';
                    $ev_decoded->[1] = $mm->{'_rec_dtime'};
                    $ev_decoded->[2] = $event->[4]; ## not sure if this is correct..

                    $mm->{'_rec_dtime'} = 0;

                } elsif ($event->[1] == &TMR_SPP) { # not sure how this differs
                                                    #   from 0xf2...

                    $ev_decoded->[0] = 'song_position';
                    $ev_decoded->[1] = $mm->{'_rec_dtime'};

                    $mm->{'_rec_dtime'} = 0;

                } elsif ($event->[1] == &TMR_TIMESIG) {

                    $ev_decoded->[0] = 'time_signature';
                    $ev_decoded->[1] = $mm->{'_rec_dtime'};

                    my $timesig = $event->[4];

                    $ev_decoded->[2] = ($timesig >> 0x18) & 0xff;
                    $ev_decoded->[3] = ($timesig >> 0x10) & 0xff;
                    $ev_decoded->[4] = ($timesig >> 0x08) & 0xff;
                    $ev_decoded->[5] =  $timesig          & 0xff;

                    $mm->{'_rec_dtime'} = 0;
                }

            } elsif ($event->[0] == &EV_SEQ_LOCAL) {
            } elsif ($event->[0] == &EV_SYSTEM)    {

                if ($event->[2] == 0xf0) { # sysex
                } elsif ($event->[2] == 0xf1) { # MTC Qframe

                } elsif ($event->[2] == 0xf2) {

                    $ev_decoded->[0] = 'song_position';
                    $ev_decoded->[1] = $mm->{'_rec_dtime'};

                    $mm->{'_rec_dtime'} = 0;

                } elsif ($event->[2] == 0xf3) { # song select

                    $ev_decoded->[0] = 'song_select';
                    $ev_decoded->[1] = $mm->{'_rec_dtime'};
                    $ev_decoded->[2] = $event->[3]; # song number

                    $mm->{'_rec_dtime'} = 0;

                } elsif ($event->[2] == 0xf4) {
                } elsif ($event->[2] == 0xf5) {
                } elsif ($event->[2] == 0xf6) { # tune request

                    $ev_decoded->[0] = 'tune_request';
                    $ev_decoded->[1] = $mm->{'_rec_dtime'};

                    $mm->{'_rec_dtime'} = 0;

                } elsif ($event->[2] == 0xf7) { # end-of-sysex
                } elsif ($event->[2] == 0xf8) { # timing clock

	           # print "Timing clock\n";

                    $mm->{'_rec_dtime'} += $mm->{'_ppqn_per_clock'};

                } elsif ($event->[2] == 0xf9) {
                } elsif ($event->[2] == 0xfa) { # start
                } elsif ($event->[2] == 0xfb) { # continue
                } elsif ($event->[2] == 0xfc) { # stop
                } elsif ($event->[2] == 0xfd) {
                } elsif ($event->[2] == 0xfe) { # active sensing

	           # print "Active sensing\n";

                } elsif ($event->[2] == 0xff) { # reset
                }
            }
            push(@{$decoded}, $ev_decoded) if (@{$ev_decoded});
        }
    }
    return $decoded;
}

1;

__END__

=head1 NAME

MIDI::Music - Perl interface to /dev/music.

=head1 SYNOPSIS

    use MIDI::Music;

    my $mm = new MIDI::Music;

    # Play a MIDI file through the
    # first available device
    $mm->playmidifile('foo.mid') || die $mm->errstr;

or:

    use MIDI::Music;
    use Fcntl;
    my $mm = new MIDI::Music;

    # Initialize device for writing
    $mm->init('mode'     => O_WRONLY,
              'timebase' => 96,
              'tempo'    => 60,
              'timesig'  => [2, 2, 24, 8],
              ) || die $mm->errstr;

    # Play a C-major chord
    $mm->playevents([['patch_change', 0, 0, 49],
                     ['note_on', 0, 0, 60, 64],
                     ['note_on', 0, 0, 64, 64],
                     ['note_on', 0, 0, 67, 64],
                     ['note_on', 0, 0, 72, 64],
                     ['note_off', 144, 0, 60, 64],
                     ['note_off', 0, 0, 64, 64],
                     ['note_off', 0, 0, 67, 64],
                     ['note_off', 0, 0, 72, 64],
                    ]) || die $mm->errstr;
    $mm->dumpbuf;
    $mm->close;

or:

    use MIDI::Music;
    use MIDI;
    use Fcntl;

    my $opus  = MIDI::Opus->new();
    my $track = MIDI::Track->new();

    my $mm = new MIDI::Music('tempo'    => 120, # These parameters
                             'realtime' => 1,   # can be passed to
                             );                 # the constructor

    # Record some MIDI data from
    # an external device..
    $mm->init('mode' => O_RDONLY) || die $mm->errstr;

    for (;;) {

        << break condition here... >>

        my $event_struct = $mm->readevents;

        push(@{ $track->events_r }, @$event_struct)
            if (defined $event_struct);
    }

    $mm->close;

    $opus->tracks($track);
    $opus->write_to_file('bar.mid');

=head1 DESCRIPTION

MIDI::Music is a high-level interface to /dev/music, and is designed to
function on any *NIX system supported by Open Sound System v.3.8 or higher.

Playback through internal and external MIDI devices is supported, as is
the "recording" of events from an external device. Additional goals in
designing MIDI::Music were:

=over 4

=item 1

to provide an API with as few methods necessary to satisfy 99% of MIDI
programmers' purposes.

=item 2

to provide easy integration with Sean M. Burke's MIDI-Perl suite by means 
of a common event specification.

=back

There are, at present, essentially three things you can do with MIDI::Music:

=over 4

=item 1 

Play a MIDI file.

=item 2 

Play a series of events defined in an event structure, which is a LoL
as described in the L<MIDI::Event> documentation.

=item 3

Read a series events from an external device. These events are returned as
the same type of event structure as in [2].

=back 

It is important to remember that MIDI::Music is not a "realtime" synthesizer
interface in the strictest, unbuffered sense of the term. :) Rather, a series 
of events are written to an internal buffer (in playback-related methods, 
anyway) which is flushed periodically. The "playevents" function may have, 
for example, long since returned, while the user continues to hear notes 
being played.

FWIW: The L<readevents()|"item_readevents"> method is fast to be sure, but the time involved in 
the interpretation of data from the external synthesizer should be taken
into account. This time will of course depend on how many messages are being 
processed at any given read, the speed of the machine doing the processing, 
etc.

=head1 INITIALIZATION PARAMETERS

These parameters are common to L<the constructor|"item_new"> and the L<init()|"item_init"> method.
A parameter supplied to the init() method will override the same parameter
supplied to new(). 

=head2 General

=over 4

=item 'device' => $device

where $device is the number identifying which synth device to use. The default 
value is 0 (first available device).

=back

=head2 Recording

=over 4

=item 'readbuf' => $buffer_size

where $buffer_size is the length in bytes of each read operation (if you 
are using MIDI::Music for recording), each event being read (at the underlying
level) as an 8-byte string. The default "read buffer" size is 4096 (512 events 
per read, maximum).

=cut
=item 'actsense' => $bool

Enable active sensing, if the device is to be opened for reading. Default is 1 
(true).
=pod

=item 'realtime' => $bool

Enable incoming realtime messages, if the device is to be opened for reading. 
Setting this to 1 allows the delta-times of recorded events to be calculated 
from "timing-clock" messages sent from the external device (otherwise, the
delta-time of any recorded event will be 0). Default is 0 (false).

=cut
=item 'timing' => $bool

Enable incoming timing messages, if the device is to be opened for reading. The
'realtime' option must be set to true for this to be useful. Default is 1 (true).
=pod

=back

=head2 Patch caching

Specifying these parameters may speed playback on hardware synths (if, for example, a 
large number of pattern-change messages are being processed over a short period of time (?)), 
though they have not been tested.

=over 4

=item 'gminstr' => \@instruments

where \@instruments is a reference to an array containing the program numbers
of instruments to cache on initialization. For example, passing 'gminstr'
the value:

    [ 0, 19, 56 ]

will result in the piano, the church organ, and the trumpet voices
(respectively) being cached.

=item 'gmdrum' => \@drum_kits

where \@drum_kits is a reference to an array containing the numbers identifying
drum kits to cache on initialization.

=back

=head2 Timing

=over 4

=item 'tempo' => $bpm

Initial tempo, where $bpm is number of beats per minute. Default is 120.

=item 'timebase' => $timebase

Initial number of ticks per quarter-note. Default is 96.

=item 'timesig' => \@timesig

Initial time signature, where \@timesig is a reference to an array containing
the time signature values:

    [ nn, dd, cc, bb ]

As described in the L<MIDI::Filespec> documentation.

=back

=head1 CONSTRUCTOR

=over 4

=item new(%init_parameters)

Return a new MIDI::Music. See section L<"INITIALIZATION PARAMETERS"> for a
description of options.

=back

=head1 METHODS

=over 4

=item playmidifile($path)

Play MIDI file supplied in $path. You must have Sean M. Burke's L<MIDI-Perl>
modules installed in order to use this method.

If L<init()|"item_init"> has not already been called, initialization will be handled
automatically.  The L<dumpbuf()|"item_dumpbuf"> method is also called automatically to
clear the play-buffer at the end of the "song," so there is no need to
bother with it.

Returns true on success, false on error (the last recorded error string 
being available through the L<errstr()|"item_errstr"> method).

Note: certain L<initialization parameters|"INITIALIZATION PARAMETERS"> supplied to the constructor,
such as timebase, may be overridden should they be specified differently in
the MIDI file header.

=item init(%init_param)

Opens and initializes the device according to parameters supplied. Returns
true on success, false on failure (in which case the error can be obtained by
calling the L<errstr()|"item_errstr"> method).

In addition to the options described in the L<"INITIALIZATION PARAMETERS">
section, init() accepts the 'mode' parameter, which value is an open-flag
(O_RDWR, O_WRONLY, O_RDONLY, etc.). Default is O_RDWR.

Open-flag constants are provided by L<Fcntl.pm>.

=item close()

Close the device. This is called automatically along with the destructor, but
you may need it in order to (for example) initialize a different device (only
one device/instance at a time is supported at present).

=item playevents(\@event_structure)

Play the events contained in the supplied reference to an event structure.
You almost always should call L<dumpbuf()|"item_dumpbuf"> after this in order to flush 
any events remain in the play-buffer.

Returns true on success, false on error (the last recorded error string 
being available through the L<errstr()|"item_errstr"> method).

The device must be L<initialized|"item_init"> for writing before a call to playevents().

=item readevents()

Read some events and return them as a reference to an event structure. There
will be a maximum of ($readbuf / 8) events contained in the structure, where
$readbuf is value of the L<initialization parameters|"INITIALIZATION PARAMETERS"> of the same name (default 
is 4096). 

(In the author's experience, there are rarely more than two or three non-timing 
events per read, and usually there is only one.)

If the 'realtime' initialization parameter was set to 1, delta-times will 
be computed to reflect the number of timing clocks that have occured from the
time the device was initialized (the running version of OSS must have realtime 
support for this to work). Otherwise, all delta-times will be zero.

The readevents() method returns undef if nothing but realtime messages are 
read.

The device must be initialized for reading before a call to readevents().

=item dumpbuf()

Dump any events remaining in the play-buffer.

=item errstr()

Return last recorded error message.

=back

=head1 NOTES

=over 4

=item *

To my knowledge, the "realtime" initialization option, allowing
timing-clocks to determine the delta-times of recorded events, will work only 
with the commercial release of OSS. This is not an issue unless you are 
interested in doing realtime recording (e.g. recording events while playing 
your keyboard/sequencer as opposed to "recording" in some sort of stepwise 
fashion). The commercial version of OSS may be obtained from the 4Front 
Technologies website:

    http://www.opensound.com/

It is worth the (resonable) price, and of course has the cross-platform 
advantage over ALSA.

=item *

MIDI::Music was developed and tested on an i686 running SuSE 6.4, with OSS
version 3.9.6 installed. A mid-1990s Yamaha QY22 sequencer was used in
testing the recording-related functions.

=back

=head1 TO DO

=over 4

=item *

Add methods for obtaining synthesizer information (number, types of available 
devices).

=item *

At present, MIDI::Music supports the interface with only one open device at a 
time. Future versions will allow for simultaneous instances of initialized 
devices, if possible.

=item *

At present, system-exclusive events (produced by bulk dumps, etc.) are not 
included in the event structures returned by L<readevents()|"item_readevents">. This should be 
fixed in the next release.

=item *

Streamline the L<playmidifile()|"item_playmidifile"> code for greater memory-efficiency, if possible.

=back

=head1 AUTHOR

Seth David Johnson, seth@pdamusic.com

=head1 SEE ALSO

The Open Sound System homepage (4Front Technologies):

    http://www.opensound.com/

The OSS Programming Guide (PDF), describing in some detail the /dev/music
API on which MIDI::Music is based:

    http://www.opensound.com/pguide/oss.pdf

Sean M. Burke's L<MIDI-Perl|MIDI> extensions provide methods for dealing 
with MIDI files; you will need to have them installed if you wish to use the
L<playmidifile()|"item_playmidifile"> method. The documentation for L<MIDI::Events> provides a 
description of the "event structures" common to MIDI::Music and MIDI-Perl.

Alex McLean's experimental L<MIDI::Realtime> is an earlier attempt to provide
a synthesizer interface to Perl. MIDI::Realtime takes an entirely different 
approach both in terms of interface and in terms of implementation, and may be 
better suited than MIDI::Music to specific purposes.

The aforementioned extensions can be obtained from the CPAN:

    http://www.cpan.org/

perl(1).

=cut