#!perl
#
# Compile and run LilyPond snippets specified on command line:
#
# ly-fu c des ees c des bes c aes
#
# NOTE this utility assumes Mac OS X and timidity by default; set the
# MIDI_EDITOR and SCORE_VIEWER environment variables, or adjust this
# code as necessary.
#
# Run perldoc(1) on this script for additional documentation.
#
# ZSH completion script available in the zsh-compdef directory of the
# App::MusicTools distribution.

use strict;
use warnings;

use File::Basename qw/basename/;
use File::Spec ();
use File::Temp qw/tempfile/;
use Getopt::Long qw/GetOptions/;
use IO::Handle;
use IPC::Open3 qw/open3/;

my $BASENAME = basename($0);

my @LILYPOND         = qw/lilypond/;
my $LILYPOND_VERSION = '2.16.0';

my @MIDI_PLAYER  = qw/timidity/;
my $MIDI_SUFFIX  = '.midi';
my @SCORE_READER = qw/open -a Preview.app/;
my $SCORE_SUFFIX = '.pdf';

my @CLEANUP_SUFFIX = qw/ps pdf/;

my $tempo       = 130;
my $instrument  = "acoustic grand";
my $do_open     = 0;
my $relative    = q{c'};
my $partial     = q{};
my $save_layout = 0;
my $skip_midi   = 0;
my ( $is_absolute, $repeats );
my $prestaff    = q{};
my $sleep_kluge = 0;

########################################################################
#
# MAIN

print_help() unless @ARGV;

if ( exists $ENV{'MIDI_EDITOR'} ) {
  @MIDI_PLAYER = map { s{(?<!\\)\\}{}g; $_ } split /(?<!\\)\s+/,
    $ENV{'MIDI_EDITOR'};
}
if ( exists $ENV{'SCORE_VIEWER'} ) {
  @SCORE_READER = map { s{(?<!\\)\\}{}g; $_ } split /(?<!\\)\s+/,
    $ENV{'SCORE_VIEWER'};
}

GetOptions(
  'absolute|abs|a' => \$is_absolute,
  'articulate'     => \my $do_articulate,
  'events'         => \my $do_events,
  'help|h|?'       => \&print_help,
  'instrument|i=s' => \$instrument,
  'layout|l'       => \$save_layout,
  'open'           => \$do_open,
  'partial|p=s'    => \$partial,
  'relative|b=s'   => \$relative,
  'repeats|r=s'    => \$repeats,
  'silent|s'       => \$skip_midi,
  'sleep|S=s'      => \$sleep_kluge,
  'tempo|t=s'      => \$tempo,
  'verbose'        => \my $verbose,
);

$save_layout = 1 if $do_open;
push @LILYPOND, '--pdf' if $save_layout;

my ( $src_fh, $src_fname ) = tempfile(
  "$BASENAME.XXXXXXX",
  DIR    => File::Spec->tmpdir,
  UNLINK => 0
);
my $midi_fname  = $src_fname . $MIDI_SUFFIX;
my $score_fname = $src_fname . $SCORE_SUFFIX;

if ( @ARGV == 1 and $ARGV[0] eq '-' ) {
  chomp( @ARGV = <STDIN> );
  s/\s+%\s.+$// for @ARGV;    # strip lilypond comments
}

if ( defined $repeats ) {
  @ARGV = (@ARGV) x $repeats;
}

my $pre_art  = '';
my $post_art = '';
if ($do_articulate) {
  $pre_art  = '\articulate {';
  $post_art = '}';
}
my $event_listener = $do_events ? '\include "event-listener.ly"' : "";

$prestaff = "\\relative $relative" unless $is_absolute;

my $ly_template = <<"END_TMPL";
\\version "$LILYPOND_VERSION"
\\include "articulate.ly"
$event_listener
\\header {
  title = "ly-fu generated output"
}
themusic = {
  \\tempo 4=$tempo
  $partial
  @ARGV
}
\\score {
  \\new Staff << $prestaff {
    \\accidentalStyle "neo-modern"
    \\set Staff.instrumentName = #"$instrument"
    \\themusic
  } >>
  \\layout { }
}

\\score {
  \\new Staff << $prestaff {
    \\set Staff.midiInstrument = #"$instrument"
    $pre_art
    \\themusic
    $post_art
  } >>
  \\midi { }
}
END_TMPL

$src_fh->autoflush(1);
print $src_fh $ly_template;

my $exit_status = 0;

# NOTE Lilypond will add suffix to output, hopefully the same suffix
# used by this script (glob for it if it can be dynamic?).
#
# Just system() would be nice, but lilypond spams the terminal, so hide
# the noise by default. Might want to collect stderr for inspection if
# things do go awry, but that's more work.
print $src_fname, "\n";
my $lypid;
eval {
  my ( $wtr, $rdr, $err );
  $lypid =
    open3( $wtr, $rdr, $err, @LILYPOND, "--output=$src_fname", $src_fname );
  while (<$rdr>) {
    print if $verbose;
  }
};
if ($@) {
  die "@LILYPOND failed: $@";
}
waitpid $lypid, 0;

system( @SCORE_READER, $score_fname ) if $do_open;

if ( !$skip_midi ) {
  eval {
    my ( $wtr, $rdr, $err );
    my $midipid = open3( $wtr, $rdr, $err, @MIDI_PLAYER, $midi_fname );
    while (<$rdr>) {
      print if $verbose;
    }
    waitpid $midipid, 0;
  };
  if ($@) {
    die "@MIDI_PLAYER failed: $@";
  }
}

if ( !$save_layout ) {
  # Finale.app can blow up if file unlinked from underneath it? So need
  # this delay, or save the layout option set.
  sleep int $sleep_kluge if !$save_layout and $sleep_kluge > 0;

  unlink $midi_fname;
  unlink $src_fname;
  unlink "$src_fname.$_" for @CLEANUP_SUFFIX;
} else {
  # Might want to share the files, so avoid whoops-perm-denied-chmod-
  # a+r phase of that process.
  chmod 0644, $midi_fname, $src_fname,
    map { "$src_fname.$_" } @CLEANUP_SUFFIX;
}

exit $exit_status;

########################################################################
#
# SUBROUTINES

sub print_help {
  warn <<"END_HELP";
Usage: $0 [options] "lilypond input" "more input" ...

The lilypond input will likely need to be quoted as lilypond input may
clash with various shell metacharacters.

  --absolute      Notation assumed to be absolute.
  --instrument    Set MIDI instrument (see lilypond docs).
  --partial|p     Lilypond fragment played once at beginning.
  --relative|b    Specify what note input is relative to.
  --repeats|r     How many times to repeat the lilypond input.
  --tempo|t       Set a tempo for the music.

  --layout|l      Save the MIDI and other various files.
  --open          Show the score in some sort of viewer.
  --silent|s      Skip generating MIDI.
  --sleep|S       Sleep before unlink of various tmp files.
  --verbose       Show noise from lilypond, MIDI player.

END_HELP
  exit 64;
}

__END__

=head1 NAME

ly-fu - play or display lilypond snippets

=head1 SYNOPSIS

  $ export MIDI_EDITOR=timidity
  $ export SCORE_VIEWER=xpdf
  $ ly-fu --instrument=banjo c des ees c des bes c aes
  $ ly-fu  -i=trumpet --open "c8 g'4 c,8 g'4 c,8 g'2"
  $ echo c e g | ly-fu -

=head1 DESCRIPTION

Plays and possibly displays lilypond snippets entered at the command
line. The C<MIDI_EDITOR> environment variable should be set to a program
that can play MIDI files, and the C<SCORE_VIEWER> optionally set to a
PDF viewer. (Or edit the source code as necessary.)

http://www.lilypond.org/ and in particular the Learning and Notation
manuals should be consulted to understand the lilypond syntax.

=head1 OPTIONS

This script currently supports the following command line switches:

=over 4

=item B<--absolute>

Assume lilypond absolute notation.

=item B<--instrument>=I<instrument>

Set MIDI instrument (see lilypond docs and ZSH compdef script).

=item B<--layout>

Save the MIDI and other various files (they are unlinked by default).

=item B<--open>

Show the score via the C<SCORE_VIEWER> program.

=item B<--partial>=I<lilypond fragment>

A lilypond fragment played once at the beginning.

=item B<--relative>=I<note>

Specify what note the input is relative to.

=item B<--repeats>=I<count>

How many times to repeat the (non-B<partial>) input.

=item B<--silent>

Do not play the MIDI.

=item B<--sleep>=I<seconds>

Kluge sleep before unlinking temporary files (if C<SCORE_VIEWER> slow,
or so forth).

=item B<--tempo>=I<tempo>

What the tempo is (in quarter notes, e.g. C<120> or the like).

=item B<--verbose>

Show output from lilypond and the MIDI player.

=back

=head1 FILES

ZSH completion script available in the zsh-compdef directory of the
L<App::MusicTools> distribution.

=head1 BUGS

=head2 Reporting Bugs

If the bug is in the latest version, send a report to the author.
Patches that fix problems or add new features are welcome.

http://github.com/thrig/App-MusicTools

=head2 Known Issues

STDERR hidden by default, so if you feed lilypond bad input, you will
not see or hear anything.

=head1 SEE ALSO

http://www.lilypond.org/

=head1 AUTHOR

Jeremy Mates

=head1 COPYRIGHT

Copyright (C) 2012 by Jeremy Mates

This script is free software; you can redistribute it and/or modify it
under the same terms as Perl itself, either Perl version 5.16 or, at
your option, any later version of Perl 5 you may have available.

=cut
