#!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. zathura is a handy PDF viewer, if available:
#
#   function open {
#     zathura "$@" 2> /dev/null &|
#   }
#
# Run perldoc(1) on this file for additional documentation.
#
# A ZSH completion script is 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/;
use Try::Tiny;

my $BASENAME = basename($0);

my @LILYPOND = qw/lilypond/;
# Debian and OpenBSD being two laggards in this regard...
my $LILYPOND_VERSION = '2.14.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 $do_open     = 0;
my $instrument  = "acoustic grand";
my $partial     = q{};
my $relative    = q{c'};
my $repeats     = 1;
my $save_layout = 0;
my $skip_midi   = 0;
my $sleep_kluge = 0;
my $staff       = 'Staff';
my $tempo       = 130;

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

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'   => \my $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=i'      => \$repeats,
  'rhythmic-staff|R' => sub { $staff = 'RhythmicStaff' },
  'silent|s'         => \$skip_midi,
  'sleep|S=s'        => \$sleep_kluge,
  'tempo|t=s'        => \$tempo,
  'verbose'          => \my $verbose,
) or print_help();

undef $relative if $is_absolute;

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

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

if ( !@ARGV or ( @ARGV == 1 and $ARGV[0] eq '-' ) ) {
  @ARGV = <STDIN>;
}

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

my $rel_str = defined $relative ? '\relative ' . $relative : '';

my $ly_template = <<"END_TMPL";
\\version "$LILYPOND_VERSION"
\\include "articulate.ly"
$event_listener
\\header {
  title = "ly-fu generated output"
}
themusic = $rel_str {
  @ARGV
}
\\score {
  \\new $staff << $rel_str {
% only in lilypond 2.16
%   \\accidentalStyle "neo-modern"
    \\set Staff.instrumentName = #"$instrument"
    \\tempo 4=$tempo
    $partial
    \\repeat volta $repeats { \\themusic }
  } >>
  \\layout { }
}

\\score {
  \\new Staff << $rel_str {
    \\set Staff.midiInstrument = #"$instrument"
    $pre_art
    \\tempo 4=$tempo
    $partial
    \\repeat unfold $repeats { \\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;
try {
  my ( $wtr, $rdr, $err );
  $lypid =
    open3( $wtr, $rdr, $err, @LILYPOND, "--output=$src_fname", $src_fname );
  while (<$rdr>) {
    print if $verbose;
  }
}
catch {
  die "@LILYPOND failed: $_";
};
waitpid $lypid, 0;

if ($do_open) {
  die "error: score not created: $score_fname\n" unless -f $score_fname;
  system( @SCORE_READER, $score_fname );
}

if ( !$skip_midi ) {
  die "error: MIDI not created: $midi_fname\n" unless -f $midi_fname;
  try {
    my ( $wtr, $rdr, $err );
    my $midipid = open3( $wtr, $rdr, $err, @MIDI_PLAYER, $midi_fname );
    while (<$rdr>) {
      print if $verbose;
    }
    waitpid $midipid, 0;
  }
  catch {
    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 -

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. C<zathura> is a handy PDF viewer, if available:

  function open {
    zathura "$@" 2>/dev/null &|
  }

=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.)

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

=head1 OPTIONS

This program 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> is
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

A ZSH completion script is available in the C<zsh-compdef/> directory of 
the L<App::MusicTools> distribution. Install this to a C<$fpath> 
directory.

=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.

L<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, besides the PDF failing to open, or the music
failing to play. Hence the I<--verbose> flag.

=head1 SEE ALSO

L<http://www.lilypond.org/>

=head1 AUTHOR

Jeremy Mates

=head1 COPYRIGHT

Copyright (C) 2012-2013 by Jeremy Mates

This program 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
