#!/usr/bin/perl

use strict; use warnings;

use Carp;

use POSIX ":sys_wait_h";
use Time::HiRes qw(sleep);
use IO::File;

my $in_opus = 0;

# environment
my $ved_dir = $ENV{ved_dir};
my $help_dir = "$ved_dir/help";
my $filename_template = "%06d";
$ENV{filename_template} = $filename_template;
$ENV{initial_file_count} = 1;
$ENV{use_dsp} = 1;
my $volume_normal = $ENV{volume};
my $volume = $volume_normal;
my $mic_volume_factor = $ENV{mic_volume_factor};
my $speed_normal = $ENV{speed};
my $speed = $speed_normal;

my $volume_delta = $ENV{volume_adjust_delta};
my $speed_factor = $ENV{speed_adjust_factor};

# context variables
our $need_kick = 0;
our $keymap;

my ($k, $c);

my @unread;

my $break_count = 0;
my $break_time = 0;
my $dont_beep_for_start_recording;
my $stopped;
my $stop_here;
my $made_a_break = 0;

my $have_cut = 0;
my $cut_level = 0;

my $stretch;

# path : the components are book, chapter, paragraph
my $max_path_depth = 3;
my @path;
my $index = 0; # this is the sentence number (or element number)

# read keys
my $cget_pid = open IN, "cget |";
# so we can 'kick' cget with USR1

# signals
$SIG{CHLD} = sub {
	while (waitpid(-1,WNOHANG) > 0) { }
	if ($need_kick) { kick(); }
};
my $interrupted = 0;
$SIG{INT} = sub {
	$interrupted = 1;
};
# die "signal"
$SIG{__DIE__} = sub {
	cleanup();
	error();
};

# argument
my $opus = $ARGV[0];
defined $opus or die "syntax: ved opus\n";

# open the "opus"
mkdir $opus; # doesn't matter if the directory already exists
chdir $opus or die "can't open $opus: $!\n";

$in_opus = 1;

mkdir ".clip";
mkdir ".temp";
mkdir ".pool";
open DEL_LOG, ">>.deleted"; close DEL_LOG;
if (count('.clip')) {
	$cut_level = depth('.clip');
	$have_cut = 1;
}

my %break_note = (
	1 => [4, 0.1],
	2 => [7, 0.2],
	3 => [12, 0.3],
);

# section_breaks, for sentence, paragraph, chapter, book
my @section_break = (0, 1, 2, 3);

# key map, command mode
my %command_keymap = (
	4 => 'prev',
	5 => 'this',
	6 => 'next',
	1 => 'enter_start',
	2 => 'enter',
	3 => 'enter_end',
	7 => 'start',
	8 => 'leave',
	9 => 'end',
	0 => 'insert',
	'.' => 'append',
	'/' => 'change',
	'*' => 'delete',
	'-' => 'cut',
	'+' => 'play',
	enter => 'join',
	escape => 'quit',
	'eof' => 'quit',
	'z' => 'speed_slower',
	'x' => 'speed_normal',
	'c' => 'speed_faster',
	'a' => 'volume_softer',
	's' => 'volume_normal',
	'd' => 'volume_louder',
	'`' => 'help',
);

# key map, insert mode
my %insert_keymap = (
	'+' => 'stop',
	'5' => 'stop',
	enter => 'break',
	'/' => 'change',
	other => 'other',
);

# key map, play mode
my %play_keymap = (
	kick => 'done',
	'+' => 'stop_here',
	'5' => 'stop',
	'z' => 'speed_slower',
	'x' => 'speed_normal',
	'c' => 'speed_faster',
	'a' => 'volume_softer',
	's' => 'volume_normal',
	'd' => 'volume_louder',
	other => 'other',
);

# banner :)
print "voice editor\n\n";

my $count = count();
if (my $path = slurp(".path")) {
	# go to where we left off - ugliness!!!
	unlink ".path";
	chomp $path;
	my @p = split m|/|, $path;
	while (@p) {
		my $i = shift @p;
		$i <= $count && $i > 0 or last;
		$index = $i;
		if (!@p) { last }
		enter(1);
	}
} else {
	# go to 1/1/1/0
	enter_start_all();
}

if ($count && $index == 0) { $index = 1; }

set_volume($volume);
set_speed($speed);
echo_off();

start();

command();

cleanup();
end();

exit(0);

sub count {
	my ($dir) = @_;
	$dir = "." if !defined $dir;
#	return `ls $dir | tail -n 1` || 0;
	# we use opendir to avoid generating a SIGCHLD
	opendir COUNT_DH, $dir || confess "can't opendir $dir";
	my @files = grep { !/^\./ } readdir COUNT_DH;
	closedir COUNT_DH;
	return 0+@files;
}

sub depth {
	my ($dir) = @_;
	$dir = "." if !defined $dir;
	opendir COUNT_DH, $dir || confess "can't opendir $dir";
	my @files = grep { !/^\./ } readdir COUNT_DH;
	for (@files) {
		my $path = "$dir/$_";
		if (-d $path) {
			closedir COUNT_DH;
			return 1 + depth($path);
		}
	}
	closedir COUNT_DH;
	return 0;
}

sub cleanup {
	echo_off();
	intchild($cget_pid); # bogosity to kill cget
	close IN;
	if ($in_opus) {
		my @p;
		while (@path) {
			if ($count > 0) {
				unshift(@p, $index);
			}
			leave(1);
		}
		unshift @p, $index;
		my $path = join "/", @p;
		belch(".path", "$path\n");
	}
}

sub command {
	local $keymap = \%command_keymap;
	local $need_kick = 0;
	while(1) {
		r();
		if ($interrupted || $c eq "quit") {
			last;
		} elsif ($c eq "append") {
			if ($have_cut) {
				if (level() == $cut_level) {
					++$index;
					paste();
				} else {
					bong();
				}
			} else {
				if (@path == $max_path_depth) {
					append();
				} else {
					append_section_and_enter_paragraph();
					insert();
				}
			}
		} elsif ($c eq "insert") {
			if ($have_cut) {
				if (level() == $cut_level) {
					paste();
				} else {
					bong();
				}
			} else {
				if (@path == $max_path_depth) {
					insert();
				} else {
					insert_section_and_enter_paragraph();
					insert();
				}
			}
		} elsif ($c eq "delete") {
			if ($index == 0 || level() != 0) {
				bong();
			} else {
				del();
				play1_maybe();
			}
		} elsif ($c eq "change") {
			if ($index == 0 || level() != 0) {
				bong();
			} else {
				my $at_end = $index == $count;
				del();
				if ($at_end) { append(); } else { insert(); }
			}
		} elsif ($c eq "this") {
			if ($index == 0) {
				bong();
			} else {
				play1();
			}
		} elsif ($c eq "prev") {
			if ($index <= 1) {
				my $break = try_to_go_to_end_of_last_element(1);
			} else {
				--$index;
				play1();
			}
		} elsif ($c eq "next") {
			if ($index >= $count) {
				my $break = try_to_go_to_start_of_next_element(1);
			} else {
				++$index;
				play1();
			}
		} elsif ($c eq "play") {
			if ($count == 0) {
				bong();
			} else {
				play();
			}
		} elsif ($c eq "start") {
			my $ok = 1;
			if ($index <= 1) {
				$ok = try_to_go_to_end_of_last_element();
				if ($ok) {
					break_sound($ok);
				} else {
					bong();
				}
			}
			if ($ok) {
				$index = 1;
				play1_maybe();
			}
		} elsif ($c eq "end") {
			my $ok = 1;
			if ($index == $count) {
				$ok = try_to_go_to_start_of_next_element();
				if ($ok) {
					break_sound($ok);
				} else {
					bong();
				}
			}
			if ($ok) {
				$index = $count;
				play1_maybe();
			}
		} elsif ($c eq "cut") {
			if ($index == 0) {
				bong();
			} else {
				cut();
			}
		} elsif ($c eq "leave") {
			if (@path == 0) {
				bong();
			} else {
				leave(1);
				ring_level_or_play_sentence();
			}
		} elsif ($c eq "enter") {
			if (@path == $max_path_depth) {
				bong();
			} else {
				enter(1);
				ring_level_or_play_sentence();
			}
		} elsif ($c eq "enter_start") {
			if (@path == $max_path_depth) {
				bong();
			} else {
				enter_start(1);
				ring_level_or_play_sentence();
			}
		} elsif ($c eq "enter_end") {
			if (@path == $max_path_depth) {
				bong();
			} else {
				enter_end(1);
				ring_level_or_play_sentence();
			}
		} elsif ($c eq "join") {
			do_join();
		} elsif ($c =~ /^speed_(.*)/) {
			speed_command($1);
			play1_maybe();
		} elsif ($c =~ /^volume_(.*)/) {
			volume_command($1) and beep();
		} elsif ($c eq "help") {
			help();
		}

		# check if a sub was interrupted
		if ($interrupted) {
			last;
		}
	}
}

sub play1_maybe {
	if ($index != 0) {
		play1();
	}
}

sub play1 {
	local $keymap = \%play_keymap;
	$stopped = 0;
	ring_level();
	play1_doit();
	if (!$stopped) { ring_level(); }
}

sub play {
	local $keymap = \%play_keymap;
	$stopped = 0;
	ring_level();
	play_doit();
	if (!$stopped) { ring_level(); }
}

sub play_doit {
	while (1) {
		play_to_end_of_section();
		if ($stopped) { last; }
		my $break = try_to_go_to_start_of_next_element();
		if ($break) {
			sleep_break($break);
		} else {
			last;
		}
	}
}

sub play_to_end_of_section {
	while (1) {
		play1_doit();
		if ($stopped) { last; }
		if ($index < $count) { ++$index; }
		else { last }
		sleep_break(level());
	}
}

sub play1_doit {
	# this is recursive now, very frightening!
	# possibly not a good idea, a bit confusing?
	my $filename = filename($index);
	if (-d $filename) {
		enter_start();
		play_to_end_of_section();
		if (!($stopped && $stop_here)) {
			leave();
		}
		return;
	}
	local $need_kick = 1;
	AGAIN:
	my $play_pid = bg("playback", filename($index));
	while (1) {
		r();
		if ($c eq "done") {
			last;
		}
		if ($interrupted || $c eq "stop" || $c eq "other" || $c eq "stop_here") {
			intchild($play_pid);
			if ($c eq "other") {
				# pass this keypress on to command mode
				unr($k);
			}
			$stopped = 1;
			$stop_here = $c eq "stop_here";
			last;
		}
		if ($c =~ /^speed_(.*)/) {
			intchild($play_pid);
			speed_command($1);
			goto AGAIN;
		}
		if ($c =~ /^volume_(.*)/) {
			intchild($play_pid);
			volume_command($1);
			goto AGAIN;
		}
		# add other functions...
	}
}

sub insert {
	insert_or_append(0);
}

sub append {
	insert_or_append(1);
}

sub insert_or_append {
	my ($is_append) = @_;
	my $pool = path_to_pool_dir();
	my $date_time = `date +'%Y%m%d %H%M%S%N'`; chomp $date_time;
	my ($date, $time) = split / /, $date_time;
	$time = substr $time, 0, 8;
	if ($is_append) { ++$index; }
	elsif ($index == 0) { ++$index; }
	local $keymap = \%insert_keymap;
	local $need_kick = 0; # 'input' can't die, theoretically
	my $i;
	my $dir = "$pool$date/$time";
	mkdir "$pool$date";
	if (-d "$dir") { die "insert directory already exists - clock bung?"; }
	mkdir $dir;
	if ($dont_beep_for_start_recording) { $dont_beep_for_start_recording = 0; }
	else { beep(); }
	echo_on();
	my $input_pid = bg_chdir($dir, "input");
	while (1) {
		r();
		if ($interrupted || $c eq "stop" || $c eq "other" || $c eq "break" || $c eq "change") {
			intchild($input_pid);
			if ($c eq "other" || $c eq "change") {
				unr($k);
			}
			last;
		}
	}
	echo_off();

	my ($number_inserted, $insertion_point) = insert_from_dir($dir, "symlink");
	if ($number_inserted == 0 && $is_append) { --$index; }
	if ($index > $count) { $index = $count; }  # unnecessary?
	#rmdir $dir or confess "cannot rmdir ".`pwd`."/$dir!";
	if ($c eq "break") {
		my $time = time();
		if ($number_inserted || $time > $break_time + 4) {
			$break_count=1;
		} else {
			++$break_count;
		}
		if ($break_count > 3) {
			bong();
			$break_count = 0;
			$dont_beep_for_start_recording = 1; # how dodgy
			goto &append;
		} else {
			$break_time = $time;
			break_before($insertion_point, $break_count);
			$made_a_break = 1;
			break_sound($break_count);
			$dont_beep_for_start_recording = 1; # how dodgy
			goto &insert;
			# insert (NOT append) at the start of the new paragraph; what a mess!
		}
	}
	if ($c ne "change") {
		if ($number_inserted || $made_a_break) {
			boop();
		} else {
			bong();
		}
	}
	$made_a_break = 0;
	$break_count = 0;
}

sub insert_from_dir {
	my ($dir, $type) = @_;
	my $i;
	my $number_to_insert = count($dir);
	my $insertion_point = $index + $number_to_insert;
	if ($number_to_insert) {
		shunt($index, $number_to_insert);
		for ($i = 1; $i <= $number_to_insert; ++$i) {
			my $from = $dir."/".filename($i);
			my $to = filename($index + $i - 1);
			if ($type eq "symlink") {
				sl($from, $to);
			} else {
				mv($from, $to);
			}
		}
		$index += $number_to_insert - 1;
	}
	return ($number_to_insert, $insertion_point);
}

sub break_before {
	my ($index_of_first_to_move, $break_count) = @_;
	my $i;
	my @rel_path_to_old_paragraph;
	# move sentences after $index_of_first_to_move to temp dir
	my $temp_dir = path_to_temp_dir();
	count($temp_dir) == 0 or confess "temp dir should be empty!!";
	my $dest_i = 1;
	for ($i = $index_of_first_to_move; $i<=$count; ++$i) {
		mv(filename($i), $temp_dir.filename($dest_i));
		++$dest_i;
	}
	$count = count();
	# go up to level at which we create the new element,
	# deleting empty elements on the way...
	my $index_of_old_element;
	my $deleted_old_element = 0;
	for ($i=0; $i<$break_count; ++$i) {
		chdir '..';
		$index_of_old_element = pop @path;
		unshift @rel_path_to_old_paragraph, $index_of_old_element;
		if ($count == 0) {
			remove_dir(filename($index_of_old_element));
			if ($i == $break_count - 1) {
				$deleted_old_element = 1;
			}
		}
		$count = count();
	}
	# work out path of new element
	my $index_of_new_element;
	if ($deleted_old_element) {
		$index_of_new_element = $index_of_old_element;
	} else {
		$index_of_new_element = $index_of_old_element + 1;
		# move elements after this forward one index
		shunt($index_of_old_element+1, 1);
	}
	# create new element
	my $dir = filename($index_of_new_element);
	mkdir($dir); chdir($dir);
	push @path, $index_of_new_element;
	# create child elements (down to first paragraph) if
	# the new element is above paragraph level
	for ($i=0; $i<$break_count-1; ++$i) {
		push @path, 1;
		my $dir = filename(1);
		mkdir($dir); chdir($dir);
	}
	$index = 1; $count = 0;
	# move paragraphs after break point to new paragraph
	insert_from_dir($temp_dir, "move");
	$index = 1;
}

sub mv {
	my ($from, $to) = @_;
	-e $to and
		confess "can't rename $from to $to: already exists";
	rename $from, $to or confess "can't rename: $!";
}

sub sl {
	my ($from, $to) = @_;
	-e $to and
		confess "can't symlink $from to $to: already exists";
	symlink $from, $to or confess "can't symlink: $!";
}

sub filename {
	my ($i) = @_;
	return sprintf($filename_template, $i);
}

sub del {
	# XXX should be more careful with this?
	my $fn = filename($index);
	my $link = readlink($fn);
	$link =~ s,.*?\.pool/,,;
	my $path_to_deleted = path_to_deleted_log();
	open DEL_LOG, ">>$path_to_deleted";
	print DEL_LOG "@path $index: $link\n";
	close DEL_LOG;
	unlink $fn or confess "where is the $index file!?";
	shunt($index+1, -1);
	del_sound();
}

sub bg {
	my $childpid = fork();
	if (! defined $childpid) {
		die "can't fork";
	}
	if ($childpid == 0) {
		setpgrp();
		exec @_;
		warn "exec failed";
		error();
		exit 1;
	}
	return $childpid;
}

sub bg_chdir {
	my $childpid = fork();
	if (! defined $childpid) {
		die "can't fork";
	}
	if ($childpid == 0) {
		setpgrp();
		chdir shift;
		exec @_;
		warn "exec failed";
		error();
		exit 1;
	}
	return $childpid;
}

sub intchild {
	my ($wait_for_pid) = @_;
	local $SIG{CHLD};
	kill -2, $wait_for_pid; # send INT to pgrp
	kill 2, $wait_for_pid; # send INT to process
#	if ($wait_for_pid == $$) { return; }

	while (1) {
		my $pid = waitpid($wait_for_pid, WNOHANG);
		if ($pid == -1 || $pid == $wait_for_pid) {
			last;
		}
		sleep 0.1;
		kill -2, $wait_for_pid;
		kill 2, $wait_for_pid;
	}
}

sub kick {
	kill 10, $cget_pid; # send USR1 to cget
}

sub bell {
	my ($pitch, $duration, $volume) = @_;
	$volume = $ENV{bell_volume_factor} if @_ == 2;
	my $freq = 440 * 2 ** ($pitch / 12);
	system("bell", $freq, $volume, $duration);
}

sub start {
	bell(0, 0.1);
	bell(12, 0.1);
}

sub end {
	bell(12, 0.1);
	bell(0, 0.1);
}

sub error {
	bell(12, 0.1);
	bell(12, 0.1);
	bell(12, 0.1);
	bell(12, 0.1);
}

sub beep {
	bell(0, 0.1);
}

sub boop {
	bell(-12, 0.1);
}

sub bong {
	bell(-12 - 5, 0.1);
	print "***\n";
}

sub what {
	bell(-12, 0.05);
}

sub del_sound {
	bell(-12 - 1, 0.4);
}

sub cut_sound {
	bell(-12 - 1, 0.2);
}

sub paste_sound {
	bell(-12, 0.2);
}

sub echo_on {
	my $mic_volume = int($volume * $mic_volume_factor);
	if ($mic_volume > 100) { $mic_volume = 100; }
	system("setmixer mic $mic_volume");
}

sub echo_off {
	system("setmixer mic 0");
}

sub r {
	GET: {
		$k = @unread ? pop @unread : <IN>;
		if (!defined $k) { $k = 'eof'; }
		chomp $k;
		$c = $keymap->{$k};
		if (!defined $c) {
			$c = $keymap->{other};
		}
		if (!defined $c) {
			print "?\n";
			what();
			redo GET;
		}
		if (defined $k) {
			print "$c\n";
		}
	}
}

sub unr {
	push @unread, @_;
}

sub cut {
	my $clip_dir = path_to_clip_dir();
	my $clip_index = count($clip_dir) + 1;
	if (!$have_cut) {
		$have_cut = 1;
		$cut_level = level();
	} else {
		if ($cut_level != level()) {
			bong(); return;
		}
	}
	mv(filename($index), $clip_dir.filename($clip_index));
	shunt($index+1, -1);
	cut_sound(); # change
	play1_maybe();
}

sub path_to_base_dir {
	my ($dir) = @_;
	my $dir_path = ("../" x @path).".$dir/";
	-d $dir_path or confess "where is the $dir directory???";
	return $dir_path;
}

sub path_to_clip_dir {
	return path_to_base_dir("clip");
}

sub path_to_temp_dir {
	return path_to_base_dir("temp");
}

sub path_to_pool_dir {
	return path_to_base_dir("pool");
}

sub path_to_deleted_log {
	return ("../" x @path).".deleted";
}

# renumber elements with index >= $start by $offset;
# use when deleting or inserting something
sub shunt {
	my ($start, $offset) = @_;
	if ($offset < 0) {
		for (my $i = $start; $i<=$count; ++$i) {
			mv(filename($i), filename($i+$offset));
		}
	} elsif ($offset > 0) {
		for (my $i = $count; $i>=$start; --$i) {
			mv(filename($i), filename($i+$offset));
		}
	}
	$count += $offset;
	if ($index > $count) { $index = $count; }
}

sub enter {
	my ($delete_index) = @_;
	if ($index == 0) {
		$index = 1;
	}
	mkdir filename($index);
	chdir filename($index) or confess("can't enter $index");
	push @path, $index;
	$count = count();
	if (my $i = slurp(".index")) {
		chomp $i;
		if ($delete_index) { unlink ".index"; }
		$index = $i;
		return 1;
	} else {
		$index = $count ? 1 : 0;
	}
	return 0;
}

sub enter_end {
	enter(@_);
	$index = $count;
}

sub enter_start {
	enter(@_);
	$index = $count ? 1 : 0;
}

sub leave {
	my ($store_index) = @_;
	if ($store_index && $index != 0) { belch(".index", "$index\n"); }
	chdir '..';
	$index = pop @path;
	$count = count();
	# delete the element if it's empty
	if (count(filename($index)) == 0) {
		remove_dir(filename($index));
		shunt($index+1, -1);
	}
}

sub ring_level {
	break_sound(level());
}

sub ring_level_or_play_sentence {
	if (@path < $max_path_depth) {
		ring_level();
	} else {
		play1_maybe();
	}
}

sub break_sound {
	my ($c) = @_;
	if ($c > 0) {
		my ($pitch, $duration) = @{$break_note{$c}};
		bell($pitch, $duration);
	}
	#for (my $i = 0; $i<$c; ++$i) { bell(4, 0.1); }
}

sub try_to_go_to_start_of_next_element {
	my ($from_ui) = @_;
	my @temp_path = @path;
	my $count_break = 0;
	my $can_go = 0;
	my $i;
	my $count;
	while (@temp_path) {
		++$count_break;
		$i = pop @temp_path;
		$count = count("../" x $count_break);
		if ($i < $count) {
			$can_go = 1;
			last;
		}
	}
	if ($can_go) {
		for (1..$count_break) { leave(1); }
		$index = count() == $count ? $i + 1 : $i;
		# ^ this bogosity is to cater for
		# leaving an empty thing which gets deleted automatically!
		enter_start_all(1);
		if ($from_ui) {
			break_sound($count_break);
			play1_maybe();
		}
		return $count_break;
	}
	if ($from_ui) { bong(); }
	return 0;
}

sub try_to_go_to_end_of_last_element {
	my ($from_ui) = @_;
	my @temp_path = @path;
	my $count_break = 0;
	my $can_go = 0;
	my $i;
	while (@temp_path) {
		++$count_break;
		$i = pop @temp_path;
		if ($i > 1) {
			$can_go = 1;
			last;
		}
	}
	if ($can_go) {
		for (1..$count_break) { leave(1); }
		$index = $i - 1;
		enter_end_all(1);
		if ($from_ui) {
			break_sound($count_break);
			play1_maybe();
		}
		return $count_break;
	}
	if ($from_ui) { bong(); }
	return 0;
}

sub do_join {
	my $join_to;
	if (@path == 0) { bong(); return; }
	# this is a bit too dwimmy ...
	# what happens in a paragraph with only 1 sentence?
	# it tries to join to the next one.
	if ($index == $count) { $join_to = 1; }
	elsif ($index <= 1) { $join_to = -1; }
	else { bong(); return; }
	leave();
	if ($join_to == -1 && $index <= 1) { bong(); enter_start(); return; }
	if ($join_to == 1 && $index == $count) { bong(); enter_end(); return; }

	my $temp_dir = path_to_temp_dir();
	remove_dir($temp_dir) or confess "temp dir should be empty!!";
	mv(filename($index), $temp_dir);
	shunt($index+1, -1);
	if ($join_to == -1) {
		--$index;
		enter_end(1);
		++$index;
	} else {
		enter_start(1);
	}
	$temp_dir = path_to_temp_dir();
		# it changed cause we entered something; bogosity
	my ($inserted) = insert_from_dir($temp_dir, "move");
	if ($join_to == -1 && $inserted > 1) {
		$index -= $inserted - 1;
	}
	break_sound(1);
}

sub enter_start_all {
	while (@path < $max_path_depth) { enter_start(@_); }
}

sub enter_end_all {
	while (@path < $max_path_depth) { enter_end(@_); }
}

sub append_section_and_enter_paragraph {
	++$index;
	insert_section_and_enter_paragraph();
}

sub insert_section_and_enter_paragraph {
	if ($index == 0) { ++$index; }
	shunt($index, 1);
	enter_start_all();
}

sub level {
	return $max_path_depth - @path;
}

sub sleep_break {
	my ($break) = @_;
	my $sleep = $section_break[$break] * $stretch;
	if ($sleep) { sleep $sleep; }
}

sub paste {
	if ($index == 0) { ++$index; }
	insert_from_dir(path_to_clip_dir(), "move");
	$have_cut = 0;
	paste_sound();
	play1();
}

sub slurp {
	my ($f) = @_;
	if (! ref $f) {
		$f = IO::File->new($f, "r") or
			return undef;
	}
	return join '', <$f>;
}

sub belch {
	my $f = $_[0];
	if (! ref $f) {
		$f = IO::File->new($f, "w") or
			die "can't open file `$f' to write: $!";
	}
	print $f $_[1] or
		die "can't write to `$f': $!";
}

sub remove_dir {
	my $dir = $_[0];
	unlink "$dir/.index";
	rmdir $dir;
}

sub volume_command {
	local $need_kick = 0;
	my ($x) = @_;
	if ($x eq "normal") {
		set_volume($volume_normal);
	} elsif ($x eq "softer") {
		if ($volume < $volume_delta) {
			bong(); # not that you can hear it!
			return 0;
		} else {
			set_volume($volume - $volume_delta);
		}
	} else { # $x eq "louder"
		if ($volume + $volume_delta > 100) {
			bong(); # you'll be able to hear this one!
			return 0;
		} else {
			set_volume($volume + $volume_delta);
		}
	}
	return 1;
}

sub set_volume {
	($volume) = @_;
	system("setmixer pcm $volume");
}

sub speed_command {
	local $need_kick = 0;
	my ($x) = @_;
	if ($x eq "normal") {
		set_speed($speed_normal);
	} elsif ($x eq "slower") {
		set_speed($speed / $speed_factor);
	} else { # $x eq "faster"
		set_speed($speed * $speed_factor);
	}
	return 1;
}

sub set_speed {
	($speed) = @_;
	$stretch = $ENV{playback_stretch} = 1/$speed;
}

sub help {
	local $keymap = \%command_keymap;
	local $need_kick = 0;
	my $play_pid = bg("speexdec", "$help_dir/help");
	while(1) {
		r();
		if ($interrupted || $c eq "help") {
			intchild($play_pid) if $play_pid;
			last;
		} elsif ($c eq "done") {
			$play_pid = 0;
		} else {
			intchild($play_pid) if $play_pid;
			$play_pid = bg("speexdec", "$help_dir/$c");
		}
	}
	boop();
}
