#! /usr/bin/perl
#########################################################################
#        This Perl script is Copyright (c) 2003, Peter J Billam         #
#               c/o P J B Computing, www.pjb.com.au                     #
#                                                                       #
#     This script is free software; you can redistribute it and/or      #
#            modify it under the same terms as Perl itself.             #
#########################################################################

# SCSI: cdda2wav, cdrecord, cdparanoia, idprio, mencoder, mplayer
#  $ENV{CDDA_DEVICE} cdrecord -scanbus
# ATAPI: cdcontrol -f /dev/cd0c info, burncd, mkisofs
#  $ENV{CDROM}  /usr/sbin/pciconf -lv, atacontrol list
# MIDI: timidity lame
#   
#  timidity -Ow -o sample.wav sample.mid
#  normalize -m *.wav
#  scp *.wav theflame:/data/cd/
#  ssh theflame
#  su -
#  cd /data/cd/
#  cdrecord dev=0,0 -v -dao -pad -speed=12 -copy *.wav

#  .mid to .wav to .mp3 ...
#  timidity -Ow -o sample.wav sample.mid
#  normalize -m *.wav
#  lame -h sample.wav sample.mp3

#  .wav files can be played with
#  sndfile-play whatever.wav
#  .mpg files can be played with
#  sndfile-play whatever.mpg  or
#  mpg123 whatever.mpg

while ($ARGV[$[] =~ /^-([a-z])/) {
   if ($1 eq 'e')      { &whatever; shift;
   } elsif ($1 eq 'c') { &whatever; shift;
   } else { print <<EOT; exit 0;
usage:
   whatever file      # does what
EOT
	}
}
use Cwd qw(chdir);
use Term::Clui;
use Term::Clui::FileSelect;
my @PATH = split (":",$ENV{PATH});
my $timidity     = &which('timidity');
my $lame         = &which('lame');
my $cdrecord     = &which('cdrecord');
my $cdda2wav     = &which('cdda2wav');
my $sndfile_play = &which('sndfile-play');
my $mpg123       = &which('mpg123');
my $aplaymidi    = &which('aplaymidi');
my $arecordmidi  = &which('arecordmidi');
my $normalize    = &which('normalize');
my $mkisofs      = &which('mkisofs');
my $man          = &which('man');
my $su           = &which('su');

while (1) {
	my $task = &choose('Do what ?', &tasks());
	exit unless $task;
	if ($task eq 'Extract and Burn') {
		warn "You'll need to be superuser ...\n";
		system "/usr/bin/su root -c $0"; exit 0;
	} elsif ($task eq 'burn WAV->AudioCD')  { &burn_wav();
	} elsif ($task eq 'burn MP3->MP3CD')    { &burn_mp3();
	} elsif ($task eq 'rip AudioCD->WAV')   { &rip_wav();
	} elsif ($task eq 'rip MP3CD->MP3')     { &rip_mp3();
	} elsif ($task eq 'encode WAV->MP3')    { &wav2mp3();
	} elsif ($task eq 'decode MP3->WAV')    { &mp32wav();
	} elsif ($task eq 'convert MIDI->WAV')  { &mid2wav();
	} elsif ($task eq 'convert MIDI->MP3')  { &mid2mp3();
	} elsif ($task eq 'play MIDI,WAV,MP3')  { &play();
	} elsif ($task eq 'record Keyboard->MIDI') { &kbd2mid();
	} elsif ($task eq 'change Directory')   { &changedir();
	} elsif ($task eq 'edit Makefile')      { &edit('Makefile');
	} elsif ($task eq 'run Make')           { system 'make';
	} elsif ($task eq 'list Soundfont')     { &list_soundfont();
	} elsif ($task eq 'consult Manual')     { &man();
	} elsif ($task eq 'configure Timidity') { &configure_timidity();
	}
}
exit 0;

#----------------------- functionality ------------------------
sub burn_wav {
	if (! $cdrecord) { &sorry("you need to install cdrecord."); return; }
	&set_cdda_device() || return;
	my @files = &select_file(-FPat=>'{*.wav,*.WAV}',-Path=>$ENV{PWD},-Chdir=>0);
	return unless @files;
	my $files = join "' '", @files;
	system "$cdrecord dev=0,0 -v -dao -pad -speed=12 -copy '$files'";
}
sub burn_mp3 {
	if (! $mkisofs)  { &sorry("you need to install mkisofs.");  return; }
	if (! $cdrecord) { &sorry("you need to install cdrecord."); return; }
}
sub rip_wav {
	if (! $cdda2wav) { &sorry("you need to install cdda2wav."); return; }
	&set_cdda_device() || return;
	my $task = &choose('Extract', 'All tracks', 'Just one track');
	return unless $task;
	if ($task eq 'All tracks') {
		system "$cdda2wav cddb=0 -H -B -Owav"; return;
	}
	# cdda2wav produces its output on stderr ARRGghhh :-(
	if (! open (P, "$cdda2wav -Q -H -g -v toc -J 2>&1 |")) {
		die "can't run $cdda2wav: $!\n";
	}
	my @toc = <P>;
	close P;
	my @tracks, @header;

	foreach (@toc) {
		next if /^\s*#/;
		next if /not detected/;
		next if /not supported/;
		next if /Album title: '' from ''/;
		chop;
		s/^\s+//;
		if (/^T\d/) { s/ title '' from ''//; push @tracks, $_;
		} else { push @header, $_;
		}
	}
	print join("\n", @header), "\n";

	$track = &choose('Extract which track ?', @tracks);
	$track =~ s/^\s*T0?//;
	$track =~ s/:?\s+.*$//;

	if ($track) { system "$cdda2wav -H -Q -x -Owav -t $track+$track"; }
}
sub rip_mp3 {
}
sub wav2mp3 {
	if (! $lame) { &sorry("you need to install lame."); return; }
	my @files = &select_file(-FPat=>'*.wav', -Path=>$ENV{PWD}, -Chdir=>0);
	foreach my $i (@files) {
		my $o = $i; $o =~ s/wav$/mp3/;
		if (-f $o && !&confirm("OK to overwrite $o ?")) { next; }
		system "$lame -h $i $o";
	}
}
sub mp32wav {
	if (! $lame)      { &sorry("you need to install lame.");      return; }
	if (! $normalize) { &sorry("you need to install normalize."); return; }
	my @files = &select_file(-FPat=>'*.mp3', -Path=>$ENV{PWD}, -Chdir=>0);
	foreach my $i (@files) {
		my $o = $i; $o =~ s/mp3$/wav/;
		if (-f $o && !&confirm("OK to overwrite $o ?")) { next; }
		system "$lame --mp3input --decode $i $o";
		system "$normalize --peak $o";
	}
}
sub mid2wav {
	if (! $timidity)  { &sorry("you need to install timidity.");  return; }
	if (! $normalize) { &sorry("you need to install normalize."); return; }
	my @files = &select_file(-FPat=>'*.mid', -Path=>$ENV{PWD}, -Chdir=>0);
	my $config = &timiditycfg();
	if (! $config) { &sorry("can't find any timidity.cfg file"); return; }
	my @wavs = ();
	foreach my $i (@files) {
		my $o = $i; $o =~ s/mid$/wav/; push @wavs, $o;
		if (-f $o && !&confirm("OK to overwrite $o ?")) { next; }
		system "$timidity -Ow -c $config -o $o $i";
	}
	system "$normalize --batch --peak '".join("' '",@wavs), "'";
}
sub mid2mp3 {
	if (! $timidity)  { &sorry("you need to install timidity.");  return; }
	if (! $normalize) { &sorry("you need to install normalize."); return; }
	if (! $lame)      { &sorry("you need to install lame.");      return; }
	my @files = &select_file(-FPat=>'*.mid', -Path=>$ENV{PWD}, -Chdir=>0);
	my @wavs = ();
	foreach my $i (@files) {
		my $o = $i; $o =~ s/mid$/wav/; push @wavs, $o;
		if (-f $o && !&confirm("OK to overwrite $o ?")) { next; }
		system "$timidity -Ow -o $o $i";
	}
	system "$normalize --batch --peak '".join("' '",@wavs), "'";
	foreach my $o (@wavs) {
		my $oo = $o; $oo =~ s/wav$/mp3/;
		if (-f $oo && !&confirm("OK to overwrite $oo ?")) { next; }
		system "$lame -h $o $oo";
		unlink $o;
	}
}
sub play {
	my $file = &select_file(-Readable=>1, -Path=>$ENV{PWD},
		-FPat=>'{*.wav,*.mp3,*.mid}');
	return unless $file;
	if ($mpg123 && $file =~ /\.mp3$/) {
		system "$mpg123 $file";
		return;
	} elsif ($file =~ /\.wav$|\.mp3$/) {
		if (! $sndfile_play) {
			if ($file =~ /\.mp3$/) {
				&sorry("you need to install mpg123 or sndfile-play."); return;
			} else {
				&sorry("you need to install sndfile-play."); return;
			}
		}
		system "$sndfile_play $file";
		return;
	}
	if (! $aplaymidi) { &sorry("you need to install aplaymidi."); return; }
	# also needed by metronome below, should factorize this code out ...
	if (!open(P,"$aplaymidi -l |")) { die "can't run $aplaymidi -l: $!\n"; }
	my (%outport2device, %device2outport);
	while (<P>) {
		if (/^\s*(\d+:\d)\s+(.*)$/) {
			my $port = $1;
			my $device = $2; substr ($device,0,32) = ''; $device =~ s/^\s*//;
			$outport2device{$port} = $device;
			$device2outport{$device} = $port;
		}
	}
	close P;
	my @outdevices = sort keys %device2outport;
	my $outdevices; my $outport;
	if (!@outdevices) {
		&sorry("aplaymidi can't see any midi output devices."); return;
	} elsif (1 == @outdevices) {
		&inform("using midi device $outdevices[$[]");
		$outport = $device2outport{$outdevices[$[]};
	} else {
		$outport = $device2outport{&choose('To which device ?',@outdevices)};
		return unless $outport;
	}
	system "$aplaymidi -p $outport \"$file\"";
}
sub kbd2mid {
	if (! $arecordmidi) { &sorry("you need to install arecordmidi."); return; }
	if (!open(P,"$arecordmidi -l |")) { die "can't run $arecordmidi -l: $!\n"; }
	while (<P>) {
		if (/^\s*(\d+:\d)\s+(.*)$/) {
			my $port = $1;
			my $device = $2; substr ($device,0,32) = ''; $device =~ s/^\s*//;
			$inport2device{$port} = $device;
			$device2inport{$device} = $port;
		}
	}
	close P;
	my @indevices = sort keys %device2inport;
	my $indevices; my $inport;
	if (!@indevices) {
		&sorry("arecordmidi can't see any midi input devices."); return;
	} elsif (1 == @indevices) {
		&inform("using midi device $indevices[$[]");
		$inport = $device2inport{$indevices[$[]};
	} else {
		$inport = $device2inport{&choose('To which device ?',@indevices)};
		return unless $inport;
	}
	my $bpm = &choose('crochets (quarter-notes) per minute ?', &tempi());
	$bpm = $bpm || 120;
	my $timesig = &choose('time signature ?', '3/8','6/8','9/8','12/8',
		'2/4','3/4','4/4','5/4','6/4','7/4','2/2','3/2');
	$timesig = $timesig || '4/4';
	$timesig =~ s/\//:/;
	my $file = &ask("To what midifile ?");
	return unless $file;
	if ($file !~ /\.mid$/) { $file .= '.mid'; }
	my $metronome;
	if ($outport) { $metronome=&choose('With a metronome ?','Yes','No'); }
	my $ok = &ask("<Return> to start recording, <Ctrl-C> to stop ...");
	if ($metronome) {
		system "arecordmidi -p$inport -b$bpm -i$timesig -m$outport $file";
	} else {
		system "arecordmidi -p$inport -b$bpm -i$timesig $file";
	}
}
sub changedir {
	# &select_file(-Path=>$ENV{PWD}, -FPat=>'OV*AgwQpJY', -SelDir=>1);
	my $newdir = &select_file(-Path=>$ENV{PWD}, -Directory=>1);
	return unless $newdir;
	if (! -d $newdir) { &sorry("$newdir is not a directory"); return; }
	if (! chdir $newdir) { &sorry("can't chdir to $newdir: \!"); return; }
	# assertively rename *.WAV->*.wav, *.MID->*.mid, *.MP3->*.mp3
	if (! opendir (D, '.')) { &sorry("can't opendir $newdir: \!"); return; }
	my @allfiles = grep { !/^\./ } readdir(D);
	closedir D;
	my $oldname;
	foreach $oldname (grep { /\.WAV$/} @allfiles) {
		my $newname = $oldname; $newname =~ s/WAV$/wav/;
		rename $oldname, $newname;
	}
	foreach $oldname (grep { /\.MP3$/} @allfiles) {
		my $newname = $oldname; $newname =~ s/MP3$/mp3/;
		rename $oldname, $newname;
	}
	foreach $oldname (grep { /\.MID$/} @allfiles) {
		my $newname = $oldname; $newname =~ s/MID$/mid/;
		rename $oldname, $newname;
	}

}

sub list_soundfont {
	eval 'require File::Format::RIFF';
	if ($@) { &sorry("you need to install File::Format::RIFF."); return; }

	my $config = &timiditycfg();
	my $dir = $ENV{PWD};
	if (open (F, $config)) {
		while (<F>) { if (/^dir\s+(.+)$/) { $dir = $1; last; } } close F;
	} else {
		&inform("can't find any timidity.cfg file ...");
	}
	my $file = &select_file(
		-Title=>'Which Soundfont file ?', -FPat=>'{*.sf2,*.SF2}', -Path=>$dir,
	);
	return unless $file;
	open(IN, $file) or die "Could not open $file: $!\n";

	my $riff1 = File::Format::RIFF->read(\*IN);
	close(IN);
	# $riff1->dump; $pdta->dump;
	my $pdta = $riff1->at(2);
	my $phdr = $pdta->at(0);
	my $data = $phdr->data;
	my %t;
	while ($data) {
		chop;
		my $chunk = substr $data,0,38,'';
		my $name = substr $chunk,0,20,'';
		my ($preset,$bank) = unpack 'SS', $chunk;
		$name =~ tr/ 0-9a-zA-Z_//cd;
		if ($name =~ /^EOP/) { next; }
		my $k = 1000*$bank + $preset;
		$t{$k} = sprintf "%5d %5d %s", $preset,$bank,$name;
	}
	my @t = "$file\nPreset Bank  PresetName";
	foreach (sort {$a<=>$b} keys %t) { push @t, $t{$_}; }
	&view("Contents of $file", join("\n", @t)."\n");
}
sub configure_timidity {
	if (! $timidity)  { &sorry("you need to install timidity.");  return; }
	my $f = &timiditycfg;
	if (! $f) {
		&inform("can't find any timidity.cfg ...");
	} elsif (-w $f) {
		&edit($f);
	} else {
		&inform("you don't have write permission to $f ...");
		if (!-w $ENV{PWD}) {
			&inform("and you don't have write permission here in $ENV{PWD}");
			return;
		}
		return unless &confirm("Create a local timidity.cfg in $ENV{PWD} ?");
		if (! open (O, ">$ENV{PWD}/timidity.cfg")) {
			&sorry("can't write to $ENV{PWD}/timidity.cfg: $!"); return;
		}
		if (open (I, $f)) {
			while (<I>) { print O $_; } close I;
		} else { 
			print O <<EOT;
# Sample timidity.cfg - see "man timidity.cfg"
dir /directory/where/you/keep/your/soundfonts

# specify default Soundfont:
soundfont Chaos4m.sf2

# but take bank0 patch0 from SteinwayGrandPiano & patch74 from Ultimate
bank 0
0  %font SteinwayGrandPiano1.2.sf2 0  0
74 %font Ultimate.sf2              0 74

EOT
		}
		close O;
		&edit("$ENV{PWD}/timidity.cfg");
	}
}
sub man {
	my @topics = @_ || (
	'aconnect', 'aplaymidi', 'arecordmidi', 'atacontrol',
	'audio_stuff',
	'burncd', 'cdcontrol', 'cdda2wav',  'cdrecord',
	'File::Format::RIFF',
	'lame', 'mencoder', 'mkisofs', 'mplayer', 'normalize',
	'pciconf', 'sndfile-play',
	'Term::Clui', 'Term::Clui::FileSelect', 
	'timidity', 'timidity.cfg',
	);
	my $topic = &choose('Which topic ?', @topics); return unless $topic;
	if ($topic eq 'audio_stuff') { system "perldoc $0";
	} elsif ($topic =~ /::/) { system "perldoc $topic";
	} else { system "$man $topic";
	}
}

#----------------------- infrastructure ------------------------
sub which { my $f; foreach $d (@PATH) {$f="$d/$_[$[]";  return $f if -x $f; }}

sub tempi {
	qw(40 42 44 46 48 50 52 54 56 58 60 63 69 72 76 80 84 8 92 96 100 104
	108 112 116 120 126 132 138 144 152 160 168 176 184 192 200 208);
}

sub ports {
	if (!open(P,'aplaymidi -l |')) { die "can't run aplaymidi -l: $!\n"; }
	while (<P>) {
		if (/^\s*(\d+:\d)\s+(.*)$/) {
			my $port = $1;
			my $device = $2; substr ($device,0,32) = ''; $device =~ s/^\s*//;
			$outport2device{$port} = $device;
			$device2outport{$device} = $port;
		}
	}
	close P;
	if (!open(P,'arecordmidi -l |')) { die "can't run arecordmidi -l: $!\n"; }
	while (<P>) {
		if (/^\s*(\d+:\d)\s+(.*)$/) {
			my $port = $1;
			my $device = $2; substr ($device,0,32) = ''; $device =~ s/^\s*//;
			$inport2device{$port} = $device;
			$device2inport{$device} = $port;
		}
	}
	close P;
}
sub timiditycfg {
	return unless $timidity;
	my $f;
	foreach $f (
		"$ENV{PWD}/timidity.cfg",
		'/usr/local/share/timidity/timidity.cfg',
		'/etc/timidity.cfg',
		) {
		if (-f $f) { return $f; }
	}
	if (! open(P, "strings $timidity |")) { return ''; }
	while (<P>) {
		if (/^Please check (\S+.cfg)/) { close P; return $1; }
	}
	close P; return '';
}
sub set_cdda_device {
	if (! $ENV{CDDA_DEVICE}) {
		if (-e "/dev/cdrom") {
			$ENV{CDDA_DEVICE} = '/dev/cdrom:@';
			&inform("using $ENV{CDDA_DEVICE} ...");
			return 1;
		}
		&inform("You should set the CDDA_DEVICE environment variable!");
		if (! open (P, "$cdrecord -scanbus |")) {   # will only run as root:-(
			warn "can't run cdrecord -scanbus: $!\n";
		} else {
			my @devices;
			while (<P>) {
				chop;
				s/\t/  /g; s/  +/  /g;
				if (/^\s+\d.*[^*]$/) { push @devices, $_; }
			}
			close P;
			my $device = &choose("Which Device ?", @devices);
			$device =~ s/^\s+//;
			$device =~ s/\s.*$//;
			$ENV{CDDA_DEVICE} = $device;
		}
	}
	if (! $ENV{CDDA_DEVICE}) {
		$ENV{CDDA_DEVICE} = &ask('CDDA_DEVICE ?');
	}
}

sub tasks {
	my @tasks = (
		'rip AudioCD->WAV', 'burn WAV->AudioCD',
		'rip MP3CD->MP3', 'burn MP3->MP3CD',
		'encode WAV->MP3', 'decode MP3->WAV',
		'play MIDI,WAV,MP3',
		'record Keyboard->MIDI',
		'edit Muscript', 'convert Muscript->MIDI',
		'convert MIDI->WAV', 'convert MIDI->MP3',
		'change Directory', 'configure Timidity',
	);
	if (-e './Makefile') {
		push @tasks, ('run Make', 'edit Makefile');
	} else {
		push @tasks, ('create Makefile');
	}
	push @tasks, (
		'list Soundfont', 'consult Manual',
	);
	return sort @tasks;
}


__END__

echo ===============================================================
echo Getting rid of spaces in .mp3 files ...
for i in *.[Mm][Pp]3; do mv "$i" `echo $i | tr ' ' '_'`; done
echo Changing .MP3 to .mp3 ...
for i in *.MP3; do mv "$i" `basename $i .MP3`.mp3; done
echo Changing _-_ to _ ...
for i in *_-_*.mp3; do mv "$i" `echo $i | sed s/_-_/_/`; done

echo
echo ===============================================================
echo Decoding .mp3 files to *.wav ...
for i in *.mp3; do lame --decode $i `basename $i .mp3`.wav; done

echo
echo ===============================================================
echo normalising .wav files ...
normalize -m *.wav

echo
echo ===============================================================
echo Total size of .wav files ...
du -kch *.wav | grep total

echo
echo ===============================================================
echo Checking for non-44100-Hz encoded .wav files ...
file *.wav | grep -v 44100

=pod

=head1 NAME

audio_stuff - wrapper for aplaymidi, cdda2wav, cdrecord, lame, timidity etc.


=head1 SYNOPSIS

$ audio_stuff

=head1 DESCRIPTION

This script, which comes along with the I<Term::Clui> Perl-module
in its I<examples> directory,
integrates
various open-source programs for handling
Muscript, Midi, WAV, MP3 and CDDA files
into one ArrowKey-and-Return user-interface,

=head1 FEATURES

=over 3
		'rip AudioCD->WAV', 'burn WAV->AudioCD',
		'rip MP3CD->MP3', 'burn MP3->MP3CD',
		'encode WAV->MP3', 'decode MP3->WAV',
		'play WAV,MP3,MID', 'record Keyboard->MIDI',
		'edit Muscript', 'convert Muscript->MIDI',
		'convert MIDI->WAV', 'convert MIDI->MP3',
		'change Directory', 'configure Timidity',

=item I<rip AudioCD-E<gt>WAV> and I<burn WAV-E<gt>AudioCD>

These features use I<cdda2wav> and I<cdrecord> to get files
off AudioCDs into I<.wav> format, or vice-versa.

=item I<rip MP3CD-E<gt>MP3> and I<burn MP3-E<gt>MP3CD>

These features use I<cp> and I<cdrecord> to get files
off MP3-CDs onto local hard-disk, or vice-versa.

=item I<encode WAV-E<gt>MP3> and I<decode MP3-E<gt>WAV>

These features use I<lame> to get files
from I<.wav> format into I<.mp3> format or vice-versa.

=item I<play WAV,MP3,MID>

Depending on which file you select,
this feature either uses
I<mpg123> to play a I<.mp3> file, or
I<sndfile-play> to play a I<.wav> or I<.mp3> file
to the headphones, or I<aplaymidi> to send a I<.mid> file to a
Synthesiser.

=back

=head1 AUTHOR

Peter J Billam  www.pjb.com.au/comp/contact.html

=head1 CREDITS

Based on Term::Clui, aplaymidi, arecordmidi, cdrecord, cdda2wav,
lame, normalize, mpg123, sndfile_play and timidity.

=head1 SEE ALSO

http://www.pjb.com.au/ ,
http://search.cpan.org/~pjb , 
Term::Clui,
aplaymidi(1),
arecordmidi(1),
cdrecord(1),
cdda2wav(1),
lame(1),
normalize(1),
sndfile_play(1),
timidity(1)

=cut

