/usr/local/CPAN/Audio-GtkGramofile/Audio/GtkGramofile/Logic.pm


package Audio::GtkGramofile::Logic;

use strict;
use warnings;
use Carp;

use Glib 1.040, qw(TRUE FALSE);
use IO::File;
use Tie::Scalar;
use POSIX; #needed for floor()
use Audio::Gramofile;

use vars qw($VERSION);
$VERSION = do { my @r = (q$Revision: 1.5 $ =~ /\d+/g); shift @r; sprintf("0.%04d",@r) }; # must be all one line, for MakeMaker

use constant WINDOW_WIDTH => 450;
use constant PBAR_HEIGHT => 30;
use constant PBAR_OFFSET => 60;

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

sub set_gtkgramofile {
  my $self = shift;
  my $gtkgramofile = shift;

  $self->{gtkgramofile} = $gtkgramofile;
}

sub tracksplit_watch_callback {
  my ($fd, $condition, $data) = @_;

  my $fh = $data->{fh};
  my $filelist = $data->{params}->[2];
  my $index = $data->{params}->[3] - 1;
  my $file = $filelist->[$index];

  if ($condition >= 'in') {
    # there's data available for reading.  we have no
    # guarantee that all the data is there, just that
    # some is there.  however, we know that the child
    # will be writing full lines, so we'll assume that
    # we have lines and will just use <>.
    my $line = scalar <$fh>;
    if (defined $line) {
      # do something useful with the text.
      if ($line =~ $data->{done_regexp}) {
        my $fraction = $1;
        $fraction /= 100.0;
        $data->{progress}->set_fraction($fraction);
      } elsif ($line =~ $data->{end_regexp}) {
        my $tracks = $1;
        $data->{progress}->set_fraction(1.0);
        $data->{progress}->set_text($tracks . " tracks found in " . $file);
      }
    }
  }

  if ($condition >= 'hup' or $condition >= 'err') { # End Of File, Hang UP, or ERRor.
    $fh->close;
    $fh = undef;
  }

  if (defined $fh) { # the file handle is still open, so return TRUE to stay installed and be called again.
    return TRUE;
  } else {
    &{$data->{sub}}($data->{params});
    return FALSE;
  }
}

sub tracksplit {
  my $self = shift;
  my $filelist = shift;
  my $make_use_rms = shift;
  my $make_graphs = shift;
  my $blocklen = shift;
  my $global_silence_factor = shift;
  my $local_silence_threshold = shift;
  my $min_silence_blocks = shift;
  my $min_track_blocks = shift;
  my $extra_blocks_start = shift;
  my $extra_blocks_end = shift;

  $self->{gtkgramofile}->{gui}->{start_tracksplit_button}->set_sensitive(0);
  my $window = Gtk2::Window->new;
  $window->set_title('gramofile tracksplit');
  $window->signal_connect (delete_event => sub {$self->{gtkgramofile}->{gui}->{start_tracksplit_button}->set_sensitive(TRUE); $window->destroy;});
  $window->set_default_size (WINDOW_WIDTH, @$filelist * PBAR_HEIGHT + PBAR_OFFSET);
  my $vbox = Gtk2::VBox->new;
  $window->add ($vbox);
  $self->{gtkgramofile}->set_value('tracksplit_general','tracksplit_cancel_button', Gtk2::Button->new_from_stock('ar_cancel'));
  $self->{gtkgramofile}->get_value('tracksplit_general','tracksplit_cancel_button')->signal_connect (clicked => sub {$self->{gtkgramofile}->{gui}->{start_tracksplit_button}->set_sensitive(TRUE); $window->destroy;});
  $self->{gtkgramofile}->get_value('tracksplit_general','tracksplit_cancel_button')->set_sensitive(FALSE);
  $vbox->pack_end($self->{gtkgramofile}->get_value('tracksplit_general','tracksplit_cancel_button'), FALSE, FALSE, 0);
  $window->show_all;
  
  my $gramofile = Audio::Gramofile->new or croak "Can't make a new Gramofile object, $!";
  $gramofile->init_tracksplit("make_use_rms" => $make_use_rms) if ($make_use_rms);
  $gramofile->init_tracksplit("make_graphs" => $make_graphs) if ($make_graphs);
  $gramofile->init_tracksplit("blocklen" => $blocklen) if ($blocklen);
  $gramofile->init_tracksplit("global_silence_factor" => $global_silence_factor) if ($global_silence_factor);
  $gramofile->init_tracksplit("local_silence_threshold" => $local_silence_threshold) if ($local_silence_threshold);
  $gramofile->init_tracksplit("min_silence_blocks" => $min_silence_blocks) if ($min_silence_blocks);
  $gramofile->init_tracksplit("min_track_blocks" => $min_track_blocks) if ($min_track_blocks);
  $gramofile->init_tracksplit("extra_blocks_start" => $extra_blocks_start) if ($extra_blocks_start);
  $gramofile->init_tracksplit("extra_blocks_end" => $extra_blocks_end) if ($extra_blocks_end);

  my $index = 0;
  tracksplit_one([$self, $gramofile, $filelist, $index, $vbox]);
}

sub tracksplit_one {
  my $params = shift;
  my $self = $params->[0];
  my $gramofile = $params->[1];
  my $filelist = $params->[2];
  my $index = $params->[3];
  my $vbox = $params->[4];
 
  my $pid_file = $self->{gtkgramofile}->get_value('tracksplit_general','tracksplit_pid_file');

  if ($index == @$filelist or $self->{gtkgramofile}->get_value('tracksplit_general','tracksplit_stopped')) { # the worklist is empty
    my $label;
    if ($self->{gtkgramofile}->get_value('tracksplit_general','tracksplit_stopped')) {
      $label = Gtk2::Label->new("Track splitting aborted!");
    } else {
      my $text = "Track location complete! More information is in the '.tracks' file";
      $text .= ($index - 1) ? "s." : ".";
      $label = Gtk2::Label->new($text);
    }
    $vbox->pack_start($label, FALSE, FALSE, 0);
    $vbox->show_all;
    $self->{gtkgramofile}->get_value('tracksplit_general','tracksplit_cancel_button')->set_sensitive(TRUE);
    $self->{gtkgramofile}->{gui}->{start_tracksplit_button}->set_sensitive(TRUE);
    unlink $pid_file or croak "Can't unlink $pid_file";
    return;
  }

  my $pbar = Gtk2::ProgressBar->new;
  $pbar->set_pulse_step(0.05);
  $pbar->set_text('Locating tracks in ' . $filelist->[$index]);
  $vbox->pack_start($pbar, FALSE, FALSE, 0);
  $vbox->show_all;

  my $file = $filelist->[$index];
  my $fh = IO::File->new; # we use IO::File to get a unique file handle.
  my $pid = $fh->open ("-|"); # fork a copy of ourselves, and read the child's stdout.
  croak "can't fork: $!\n" unless defined $pid;
  if ($pid == 0) { # in child
    eval{
      $gramofile->set_input_file($file);
      $gramofile->split_to_tracks;
    };
    carp $@ if $@;
    exit; # important!  do not continue to run or Very Bad Things can and will happen!
  } else { # in parent
    my $pid_fh = IO::File->new(">$pid_file") or croak "Can't open $pid_file, $!";
    print $pid_fh $pid;
    $pid_fh->close;
    $index++;
    my $data = {fh => $fh, progress => $pbar, done_regexp => qr/^Done :\s+(\d+) %$/, 
                end_regexp => qr/^(\d+) tracks have been detected/, 
                sub => \&tracksplit_one, params => [$self, $gramofile, $filelist, $index, $vbox]};
    Glib::IO->add_watch ($fh->fileno, [qw/in hup err/], \&tracksplit_watch_callback, $data);
  }
}

my ($track_num, $total_num);
sub process_watch_callback {
  my ($fd, $condition, $data) = @_;

  my $fh = $data->{fh};
  my $filelist = $data->{params}->[2];
  my $index = $data->{params}->[4] - 1;
  my $file = $filelist->[$index];

  if ($condition >= 'in') { # there's data available for reading.
    my $line = scalar <$fh>;
    if (defined $line) {
      if ($line =~ $data->{track_regexp}) {
        ($track_num, $total_num) = ($1, $2);
        $data->{progress}->set_text("Processing track $track_num of $total_num in $file");
      } elsif ($line =~ $data->{done_regexp}) {
        my ($track_f, $total_f) = ($1, $2);
        $total_f /= 100.0;
        $data->{progress}->set_fraction($total_f);
        $data->{progress}->set_text("Processing track " . $track_num . " of " . $total_num . " in $file - ${track_f}%");
      }
    }
  }

  if ($condition >= 'hup' or $condition >= 'err') { # End Of File, Hang UP, or ERRor. 
    $data->{progress}->set_text("Processed " . $total_num . " tracks in $file");
    $data->{progress}->set_fraction(1.0);
    $fh->close;
    $fh = undef;
  }

  if (defined $fh) { # the file handle is still open, so return TRUE to stay installed and be called again.
    return TRUE;
  } else {
    &{$data->{sub}}($data->{params});
    return FALSE;
  }
}

sub process_signal {
  my $self = shift;
  my $input_file_list_ref = shift;
  my $output_file_list_ref = shift;
  my $filter_list_ref = shift;
  my $simple_median_num_samples = shift;
  my $double_median_first_num_samples = shift;
  my $double_median_second_num_samples = shift;
  my $simple_mean_num_samples = shift;
  my $rms_filter_num_samples = shift;
  my $cmf_median_tick_num_samples = shift;
  my $cmf_rms_length = shift;
  my $cmf_recursive_median_length = shift;
  my $cmf_decimation_factor = shift;
  my $cmf_tick_detection_threshold = shift;
  my $cmf2_rms_length = shift;
  my $cmf2_recursive_median_length = shift;
  my $cmf2_decimation_factor = shift;
  my $cmf2_tick_fine_threshold = shift;
  my $cmf2_tick_detection_threshold = shift;
  my $cmf3_rms_length = shift;
  my $cmf3_recursive_median_length = shift;
  my $cmf3_decimation_factor = shift;
  my $cmf3_tick_fine_threshold = shift;
  my $cmf3_tick_detection_threshold = shift;
  my $cmf3_fft_length = shift;
  my $simple_normalize_factor = shift;
  my $begin_and_end_times = shift;
  my $begin_time = shift;
  my $end_time = shift;
  my $whole_frames = shift;
  my $framesize = shift;

  my $window = Gtk2::Window->new;
  $window->set_title('gramofile signal processing');
  $self->{gtkgramofile}->{gui}->{start_process_button}->set_sensitive(0);
  $self->{gtkgramofile}->set_value('process_general','process_textview',Gtk2::TextView->new);
  $self->{gtkgramofile}->set_value('process_general','process_cancel_button',Gtk2::Button->new_from_stock('ar_cancel'));
  $self->{gtkgramofile}->get_value('process_general','process_cancel_button')->signal_connect (clicked => sub {$self->{gtkgramofile}->{gui}->{start_process_button}->set_sensitive(TRUE); $window->destroy;});
  $self->{gtkgramofile}->get_value('process_general','process_cancel_button')->set_sensitive(FALSE);
  $window->signal_connect (delete_event => sub {$self->{gtkgramofile}->{gui}->{start_process_button}->set_sensitive(TRUE); $window->destroy;});
  $window->set_default_size (WINDOW_WIDTH, @$input_file_list_ref * PBAR_HEIGHT + PBAR_OFFSET);
  my $vbox = Gtk2::VBox->new;
  $window->add($vbox);
  $self->{gtkgramofile}->set_value('process_general','process_cancel_button', Gtk2::Button->new_from_stock('ar_cancel'));
  $self->{gtkgramofile}->get_value('process_general','process_cancel_button')->signal_connect (clicked => sub {$self->{gtkgramofile}->{gui}->{start_process_button}->set_sensitive(1); $window->destroy;});
  $self->{gtkgramofile}->get_value('process_general','process_cancel_button')->set_sensitive(FALSE);
  $vbox->pack_end($self->{gtkgramofile}->get_value('process_general','process_cancel_button'), FALSE, FALSE, 0);
  $window->show_all;

  my $gramofile = Audio::Gramofile->new or croak "Can't make a new Gramofile object, $!";
  
  $gramofile->init_filter_tracks(@$filter_list_ref);

  $gramofile->init_simple_median_filter("num_samples" => $simple_median_num_samples) 
    if ($simple_median_num_samples);
  
  $gramofile->init_double_median_filter("first_num_samples" => $double_median_first_num_samples) 
    if ($double_median_first_num_samples);
  $gramofile->init_double_median_filter("second_num_samples" => $double_median_second_num_samples) 
    if ($double_median_second_num_samples);
  
  $gramofile->init_simple_mean_filter("num_samples" => $simple_mean_num_samples) 
    if ($simple_mean_num_samples);
  
  $gramofile->init_rms_filter("num_samples" => $rms_filter_num_samples) 
    if ($rms_filter_num_samples);
  
  $gramofile->init_cmf_filter("num_samples" => $cmf_median_tick_num_samples) 
    if ($cmf_median_tick_num_samples);
  $gramofile->init_cmf_filter("rms_length" => $cmf_rms_length) 
    if ($cmf_rms_length);
  $gramofile->init_cmf_filter("rec_med_len" => $cmf_recursive_median_length) 
    if ($cmf_recursive_median_length);
  $gramofile->init_cmf_filter("rec_med_dec" => $cmf_decimation_factor) 
    if ($cmf_decimation_factor);
  $gramofile->init_cmf_filter("tick_threshold" => $cmf_tick_detection_threshold) 
    if ($cmf_tick_detection_threshold);
  
  $gramofile->init_cmf2_filter("rms_length" => $cmf2_rms_length) 
    if ($cmf2_rms_length);
  $gramofile->init_cmf2_filter("rec_med_len" => $cmf2_recursive_median_length) 
    if ($cmf2_recursive_median_length);
  $gramofile->init_cmf2_filter("rec_med_dec" => $cmf2_decimation_factor) 
    if ($cmf2_decimation_factor);
  $gramofile->init_cmf2_filter("fine_threshold" => $cmf2_tick_fine_threshold) 
    if ($cmf2_tick_fine_threshold);
  $gramofile->init_cmf2_filter("tick_threshold" => $cmf2_tick_detection_threshold)
    if ($cmf2_tick_detection_threshold);
  
  $gramofile->init_cmf3_filter("rms_length" => $cmf3_rms_length) 
    if ($cmf3_rms_length);
  $gramofile->init_cmf3_filter("rec_med_len" => $cmf3_recursive_median_length) 
    if ($cmf3_recursive_median_length);
  $gramofile->init_cmf3_filter("rec_med_dec" => $cmf3_decimation_factor) 
    if ($cmf3_decimation_factor);
  $gramofile->init_cmf3_filter("fine_threshold" => $cmf3_tick_fine_threshold) 
    if ($cmf3_tick_fine_threshold);
  $gramofile->init_cmf3_filter("tick_threshold" => $cmf3_tick_detection_threshold)
    if ($cmf3_tick_detection_threshold);
  $gramofile->init_cmf3_filter("fft_length" => $cmf3_fft_length) 
    if ($cmf3_fft_length);
  
  $gramofile->init_simple_normalize_filter("normalize_factor" => $simple_normalize_factor) 
    if ($simple_normalize_factor);

  $gramofile->use_begin_end_time($begin_time, $end_time) if ($begin_and_end_times);

  $gramofile->adjust_frames($framesize) if ($whole_frames);

  my $index = 0;
  process_one([$self, $gramofile, $input_file_list_ref, $output_file_list_ref, $index, $vbox]);
}

sub process_one {
  my $params = shift;
  my $self = $params->[0];
  my $gramofile = $params->[1];
  my $infilelist = $params->[2];
  my $outfilelist = $params->[3];
  my $index = $params->[4];
  my $vbox = $params->[5];
 
  my $pid_file = $self->{gtkgramofile}->get_value('process_general','process_pid_file');
  if ($index == @$infilelist or $self->{gtkgramofile}->get_value('process_general','process_stopped')) {
    my $label;
    if ($self->{gtkgramofile}->get_value('process_general','process_stopped')) {
      $label = Gtk2::Label->new("Signal processing aborted!");
    } else {
      my $text = ($index - 1) ? "All .wav files " : ".wav file ";
      $text .= "split and processed!";
      $label = Gtk2::Label->new($text);
    }
    $vbox->pack_start($label, FALSE, FALSE, 0);
    $vbox->show_all;
    $self->{gtkgramofile}->get_value('process_general','process_cancel_button')->set_sensitive(TRUE);
    $self->{gtkgramofile}->{gui}->{start_process_button}->set_sensitive(0);
    unlink $pid_file or croak "Can't unlink $pid_file";
    return;
  }

  my $pbar = Gtk2::ProgressBar->new;
  $pbar->set_pulse_step(0.05);
  $pbar->set_text('Processing ' . $infilelist->[$index]);
  $vbox->pack_start($pbar, FALSE, FALSE, 0);
  $vbox->show_all;

  my $input_file = $infilelist->[$index];
  my $output_file = $outfilelist->[$index];
  my $fh = IO::File->new; # we use IO::File to get a unique file handle.
  my $pid = $fh->open ("-|"); # fork a copy of ourselves, and read the child's stdout.
  croak "can't fork: $!\n" unless defined $pid;
  if ($pid == 0) { # in child.
    eval {
      $gramofile->set_input_file($input_file);
      $gramofile->set_output_file($output_file);
      $gramofile->filter_tracks;
    };
    carp $@ if $@;
    exit; # important!  do not continue to run or Very Bad Things can and will happen!
  } else { # in parent.
    my $pid_fh = IO::File->new(">$pid_file") or croak "Can't open $pid_file, $!";
    print $pid_fh $pid;
    $pid_fh->close;
    $index++;
    my $data = {fh => $fh, progress => $pbar, track_regexp => qr/^Track:\s+(\d+)\s+of\s+(\d+)\.\s*$/, 
                done_regexp => qr/^Done:\s+(\d+)%\s+track\s+(\d+)%\s+total\s*$/, 
                sub => \&process_one, params => [$self, $gramofile, $infilelist, $outfilelist, $index, $vbox]};
    Glib::IO->add_watch ($fh->fileno, [qw/in hup err/], \&process_watch_callback, $data);
  }
}

1;