#!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, or create suitably named programs that DTRT.
#
# 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 ();
use Getopt::Long qw/GetOptions/;
use IO::Handle;
use IPC::Run3 qw/run3/;

my $Prog_Name = basename $0;

my @Lilypond_Cmd = qw/lilypond/;
my @MIDI_Player  = qw/timidity/;
my @Score_Reader = qw/open/;

my $LILYPOND_VERSION = '2.18.2';

my $instrument = "acoustic grand";
my $partial    = q{};
my $relative   = q{c'};
my $repeats    = 1;
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|?'         => \&emit_help,
  'instrument|i=s'   => \$instrument,
  'language=s'       => \my $language,
  'layout|l'         => \my $Flag_Generate_Score,
  'open'             => \my $Flag_Show_Score,
  'partial|p=s'      => \$partial,
  'relative|b=s'     => \$relative,
  'repeats|r=i'      => \$repeats,
  'rhythmic-staff|R' => sub { $staff = 'RhythmicStaff' },
  'show-code'        => \my $Flag_Show_Code,
  'silent|s'         => \my $Flag_No_MIDI,
  'sleep|S=i'        => \my $Flag_Sleep_Kluge,
  'tempo|t=s'        => \$tempo,
  'verbose'          => \my $Flag_Verbose,
) or do { warn "could not parse options\n"; emit_help() };

undef $relative if $is_absolute;

$Flag_Generate_Score = 1 if $Flag_Show_Score;
push @Lilypond_Cmd, '--pdf' if $Flag_Generate_Score;

$Flag_Sleep_Kluge ||= 0;

$language //= 'nederlands';

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 ( $repeat_midi, $repeat_score, $score_bar );
if ( $repeats > 1 ) {
  $repeat_midi  = "\\repeat unfold $repeats ";
  $repeat_score = "\\repeat volta $repeats ";
  $score_bar    = '';
} else {
  $repeat_midi  = '';
  $repeat_score = '';
  $score_bar    = qq{\\bar "|."};
}

# Assume they want both the score and MIDI bits on standard out
if ($Flag_Show_Code) {
  $Flag_Generate_Score = 1;
  $Flag_No_MIDI        = 0;
}

my $lilypond_input = <<"END_TMPL_HEAD";
\\version "$LILYPOND_VERSION"
\\include "articulate.ly"
\\language "$language"
$event_listener
\\header {
  title = "ly-fu generated output"
}
themusic = $rel_str {
  @ARGV
}
END_TMPL_HEAD

if ($Flag_Generate_Score) {
  $lilypond_input .= <<"END_TMPL_LAYOUT";
\\score {
  \\new $staff << $rel_str {
    \\accidentalStyle "neo-modern"
    \\set Staff.instrumentName = #"$instrument"
    \\tempo 4=$tempo
    $partial
    $repeat_score { \\themusic }
    $score_bar
  } >>
  \\layout { }
}
END_TMPL_LAYOUT
}

unless ($Flag_No_MIDI) {
  $lilypond_input .= <<"END_TMPL_MIDI";
\\score {
  \\new Staff << $rel_str {
    \\set Staff.midiInstrument = #"$instrument"
    $pre_art
    \\tempo 4=$tempo
    $partial
    $repeat_midi { \\themusic }
    $post_art
  } >>
  \\midi { }
}
END_TMPL_MIDI
}

if ($Flag_Show_Code) {
  print $lilypond_input;
  exit 0;
}

my $work_dir = File::Spec->tmpdir;
chdir $work_dir or die "$Prog_Name: could not chdir() to '$work_dir': $!\n";

# NOTE a dedicated directory for output would be safer on a shared tmp dir
# system, as I am unsure of how well lilypond defends against /tmp attacks. I
# use a TMPDIR that is not the system one.
my $tmp_ly = File::Temp->new(
  DIR      => $work_dir,
  SUFFIX   => '.ly',
  TEMPLATE => "$Prog_Name.XXXXXXXXXX",
  UNLINK   => 0
);
$tmp_ly->print($lilypond_input);
$tmp_ly->flush;
$tmp_ly->sync;

my $ly_filename = $tmp_ly->filename;
( my $score_filename = $ly_filename ) =~ s/\.ly/\.pdf/;
( my $midi_filename  = $ly_filename ) =~ s/\.ly/\.midi/;

print $ly_filename, "\n" if $Flag_Generate_Score;

my ( $stdout, $stderr );

eval {
  my $stdin;
  push @Lilypond_Cmd, $ly_filename;
  run3 \@Lilypond_Cmd, \$stdin, \$stdout, \$stderr;
};
if ($@) {
  chomp $@;
  die "$Prog_Name: run3() of '@Lilypond_Cmd' failed: $@\n";
}
if ( $? >> 8 != 0 ) {
  warn "$Prog_Name: non-zero exit from '@Lilypond_Cmd'\n";
  # something awry, so show everything lilypond said for debugging
  warn $stderr;
  print $stdout;
  exit $?;
}
print $stdout if $Flag_Verbose;

if ($Flag_Show_Score) {
  die "error: score not created: $score_filename\n" unless -f $score_filename;

  local $SIG{CHLD} = 'IGNORE';
  if ( fork() ) {
    exec( @Score_Reader, $score_filename )
      or die "$Prog_Name: could not exec score reader '@Score_Reader': $!\n";
  }
}

unless ($Flag_No_MIDI) {
  die "error: MIDI not created: $midi_filename\n" unless -f $midi_filename;

  my $stdin;
  push @MIDI_Player, $midi_filename;
  run3 \@MIDI_Player, \$stdin, \$stdout, \$stderr;
  if ($@) {
    chomp $@;
    die "$Prog_Name: run3() of '@MIDI_Player' failed: $@\n";
  }
  if ( $? >> 8 != 0 ) {
    warn "$Prog_Name: non-zero exit from '@MIDI_Player'\n";
    warn $stderr;
    print $stdout;
    exit $?;
  }
  print $stdout if $Flag_Verbose;
}

if ( !$Flag_Generate_Score ) {
  # Due to some applications being slow to start and thus complaining or
  # blowing up should say the MIDI be unlinked from out underneath them.
  sleep $Flag_Sleep_Kluge if $Flag_Sleep_Kluge > 0;
  unlink $ly_filename, $midi_filename, $score_filename;

} else {
  # Might want to share the files, so avoid the whoops-perm-denied-chmod-
  # a+r phase of that process, given that tmp files otherwise have locked-
  # down perms.
  chmod 0644, $ly_filename, $midi_filename, $score_filename;
}

exit 0;

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

sub emit_help {
  warn <<"END_HELP";
Usage: $0 [options] "lilypond code ..."

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.
  --show-code     Print the lilypond data to stdout and exit.
  --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 -

Or, to instead save the generated lilypond somewhere:

  $ ly-fu --show-code e e e c1 > masterpiece.ly

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.

=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<--language>=I<language>

Specify the lilypond language to use, by default the C<lilypond> default of
C<nederlands>. If using note names from L<Music::PitchNum::German>, 

  ... | ly-fu --language=deutsch ...

may be of some use. "Note names in other languages" in the lilypond notation
reference contains the full list of supported languages.

=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<--rhythmic-staff>

Make the staff a lilypond C<RhythmicStaff> instead of the usual one.

=item B<--show-code>

Prints the generated lilypond data to standard out, then exits the program. The
score is not shown, nor the music played.

=item B<--silent>

Do not play the MIDI.

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

Kluge sleep before unlinking temporary files (might be handy if C<SCORE_VIEWER>
is slow to start).

=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

NOTE This program makes heavy use of temporary files. On a shared system, the
lilypond generated output files might introduce /tmp security problems
(arbitrary file clobber or unlink against the user running the code). These can
be avoided by employing a private temporary directory, for example by pointing
the C<TMPDIR> environment variable to such a directory.

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

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>

=head1 SEE ALSO

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

=head1 AUTHOR

Jeremy Mates

=head1 COPYRIGHT

Copyright (C) 2012-2015 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
