#!/usr/bin/perl5
# xmixer -- version 0.5
#
# Copyright (c) 1995-1997 Berkeley Software Design, Inc.  All rights reserved.
# The Berkeley Software Design Inc. software License Agreement specifies
# the terms and conditions for redistribution.
#
#	BSDI	xmixer.p5,v 1.3 1997/09/10 19:09:07 sanders Exp
#
# IDEAS:
#     Make $ENV{CDPLAYER} a list
#     Handle $SOUND_MIXER_READ_CAPS and multiple recording devices.
#     Rewrite Sound::Voxware module using XS interface (native C).
#
#     slider for index into current song should be "active"
#         (need to hack libcdrom to do this though).
#     "iconify" down to just the CD controls
#     

use Tk;

eval { main(@ARGV) };
die $@ if $@;
exit 0;

sub main {
    ############################################################
    ###   Command Line Arguments   #############################
    ############################################################

    my @args = @ARG;
    my $arg;

    my $mixer_dev;
    if ($args[0] =~ /^-/) {
	$arg = shift @args;
	if ($arg eq '-m') { $mixer_dev = shift @args; next; }
	die "Usage: xmixer [-m mixer]\n";
    }
    # prefix /dev/mixer if digit only argument
    $mixer_dev = "/dev/mixer$mixer_dev" if $mixer_dev =~ /^\d+$/;

    ############################################################
    ###   User Interface   #####################################
    ############################################################

    my $mw = MainWindow->new;
    $mw->title("Sound Mixer Board");
    $mw->iconname("xmixer");

    my $tkmixer = new Private::Sound::Voxware::TkMixer ($mw, $mixer_dev);

    MainLoop;
}

################################################################
###   TkMixer Implementation   #################################
################################################################

package Private::Sound::Voxware::TkMixer;

use English;
use Sound::Voxware::Mixer;
use Cdrom;

use Tk;
use Tk::Dialog;
use Tk::Toplevel;

###
### update_interface is called every 1500ms so the interface can
### reflect any changes made to the hardware behind our backs.
###
sub update_interface {
    my $self = shift;
    my $mixer = $self->{MIXER};
    my $dev;
    $mixer->load;
    foreach $dev (keys %{$self->{SLIDERS}}) {
	if ($dev eq 'rdev') {
	    ${$self->{SLIDERS}->{$dev}} = $mixer->attribute($dev);
	}
	else {
	    my($lval, $rval) = split(/:/, $mixer->attribute($dev));
	    my($left, $right) = @{$self->{SLIDERS}->{$dev}};
	    $rval = $lval unless $mixer->is_stereo($dev);
	    $left->set($lval);
	    $right->set($rval) if $right;
	}
    }
    update_cdinfo($self) if $self->{CDINTERFACE};
    after(1500, [\&update_interface, $self]);
}

sub cdstate {
    my $self = shift;
    if (! defined $self->{CD}) {
	$self->{CD} = new Cdrom $ENV{CDPLAYER};
	return Cdrom::UNKNOWN;
    }
    my $state = eval { $self->{CD}->state };
    if ($@ =~ /^EXCEPTION: cdstatus failed/) {
	# the user might have swapped CD's so get new CDINFO
	$self->{CD} = new Cdrom;
	return Cdrom::UNKNOWN;
    }
    elsif ($@) {
	$self->error_dialog($@);
	return Cdrom::UNKNOWN;
    }
    return $state;
}

sub update_cdinfo {
    my $self = shift;

    my $cd = $self->{CD};
    my $state = $self->cdstate;

    # if status is UNKNOWN then $cd might be undef so you cannot use it

    if ($state != Cdrom::UNKNOWN) {
	$self->make_trk_buttons;
    } else {
	$self->clear_trk_buttons;
    }

    #
    # Slider
    #
    if ($state == Cdrom::PLAYING || $state == Cdrom::PAUSED) {
	my $len = ($cd->track_info($cd->cur_track))[1];
	$self->{TRACKSLIDER}->set(int($cd->rel_frame / $len * 100));
    }
    else {
	$self->{TRACKSLIDER}->set(0);
    }

    #
    # currently playing track
    #
    if ($state == Cdrom::PLAYING) {
	$self->{CDTRACK}->configure(
	    -text => sprintf(" Trk: %2d ", $cd->cur_track),
	);
	$self->{CDPLAY}->configure(-image => $self->{'stop_bit'});
    }
    elsif ($state == Cdrom::PAUSED) {
	$self->{CDTRACK}->configure(
	    -text => sprintf(" Trk:<%2d>", $cd->cur_track),
	);
	$self->{CDPLAY}->configure(-image => $self->{'play_bit'});
    }
    elsif ($state == Cdrom::STOPPED) {
	$self->{CDTRACK}->configure(
	    -text => " Trk: -- ",
	);
	$self->{CDPLAY}->configure(-image => $self->{'play_bit'});
    }
    else {
	$self->{CDTRACK}->configure(
	    -text => " Trk: ?? ",
	);
    }
    #
    # Time
    #
    if ($state == Cdrom::PLAYING || $state == Cdrom::PAUSED) {
	$self->{CDTIME}->configure(
	    -text => sprintf(" %02d:%02d ",
		Cdrom::frame_to_msf($cd->rel_frame)),
	);
    }
    else {
	$self->{CDTIME}->configure(
	    -text => sprintf(" --:-- "),
	);
    }
    #
    # Total disk tracks
    #
    $self->{CDTRACKS}->configure(
	-text => sprintf(" [%2d] ", (defined $cd ? $cd->tracks : 0)),
    );

}

sub control {
    my $self = shift;
    my($side,$dev) = @ARG;
    my $locked = 0;
    my($left, $right) = @{$self->{SLIDERS}->{$dev}};
    if ($self->{MIXER}->is_stereo($dev)) {
	my($lval,$rval) = ($left->get, $right->get);
        my $Ev = ($side eq "l" ? $left->XEvent : $right->XEvent);
	# $Ev->b should give me the button but it doesn't seem to be working
	# Do lock on 512 or 256 bit set (0x300)
	if (ref $Ev && $Ev->s & 0x300) {
	    $left->set($rval) if $side eq "r";
	    $right->set($lval) if $side eq "l";
	}
	$self->{MIXER}->attribute($dev, "$lval:$rval");
    } else {
	$self->{MIXER}->attribute($dev, $left->get);
    }
    eval { $self->{MIXER}->store };
    $self->error_dialog($@) if $@;
}

sub add_scale_bindings {
    my ($scale) = @ARG;
    package Tk::Scale;
    $scale->bind("all", "<3>",['ButtonDown',Ev('x'),Ev('y')]);
    $scale->bind("all", '<B3-Motion>' => ['Drag',Ev('x'),Ev('y')]);
    $scale->bind("all", "<ButtonRelease-3>",
	sub {
	    my $w = shift;
	    my $Ev = $w->XEvent;
	    $w->CancelRepeat();
	    EndDrag($w);
	    Activate($w,$Ev->x,$Ev->y)
	});
}

sub mixer_slider {
    my $self = shift;
    my($parent, $mixer, $dev, $text) = @ARG;
    $text =~ s/\s+$//;
    my($lval, $rval) = split(/:/, $mixer->attribute($dev));

    my $frame = $parent->Frame(
	    -relief       => 'ridge',
	    -borderwidth  => 4,
	    -highlightthickness => 0,
    );
    my $label = $frame->Label(
	    -text => $text,
	    -borderwidth  => 0,
	    -highlightthickness => 0,
    );
    $label->pack(-side => 'top', -anchor => 'n', -fill => 'x', -padx => 2, -pady => 2);

    my($left,$right);
    $left = $frame->Scale(
	    -width        => '0.15i',
	    -showvalue	  => 0,
	    -orient       => 'vertical',
	    -borderwidth  => 2,
	    -highlightthickness => 0,
	    '-length'     => 75,
	    -from         => 100,
	    -to           => 0,
	    -command      => [\&control, $self, "l", $dev],
       );
    $left->set($lval);
    $left->pack(-side => 'left', -anchor => 'n', -fill => 'y', -padx => 1, -pady => 2);
    add_scale_bindings($left);
    if (defined $rval) {
	$right = $frame->Scale(
		-width        => '0.15i',
		-showvalue	  => 0,
		-orient       => 'vertical',
		-borderwidth  => 2,
		-highlightthickness => 0,
		'-length'     => 75,
		-from         => 100,
		-to           => 0,
		-command      => [\&control, $self, "r", $dev],
	   );
	$right->set($rval);
	$right->pack(-side => 'left', -anchor => 'n', -fill => 'y', -padx => 1, -pady => 2);
	add_scale_bindings($left);
    }
    if ($dev eq "volume") {
       $frame->configure(-relief => 'groove');
       $left->configure(-background => 'red'); 
       $left->configure(-activebackground => 'red'); 
       $right->configure(-background => 'red'); 
       $right->configure(-activebackground => 'red'); 
    }
    $self->{SLIDERS}->{$dev} = [ $left, $right ];
    $frame;
}

sub set_rdev {
    my $self = shift;
    my ($refrdev) = @ARG;
    $self->{MIXER}->attribute("rdev", $$refrdev);
    eval { $self->{MIXER}->store };
    $self->error_dialog($@) if $@;
}

sub mixer_rdevs {
    my $self = shift;
    my($parent, $mixer, $text) = @ARG;
    my $i;

    my $frame = $parent->Frame(
	    -relief => 'ridge',
	    -borderwidth  => 2,
	    -highlightthickness => 0,
    );
    my $label = $frame->Label(
	    -text => $text,
	    -relief => 'groove',
	    -borderwidth  => 2,
	    -highlightthickness => 0,
    );
       $label->pack(-side => 'top', -fill => 'x', -padx => 2, -pady => 2);

    # Initialize
    my $rdev = $mixer->attribute("rdev");
    $self->{SLIDERS}->{'rdev'} = \$rdev;

    my $radio;
    foreach $i (sort $mixer->rdevs) {
	$radio = $frame->Radiobutton(
		-anchor => 'w',
		-text => $i,
		-relief => 'flat',
		-highlightthickness => 0,
		-value => $i,
		-variable => \$rdev,
		-command => [\&set_rdev, $self, \$rdev],
		-pady => '1',
	    );
	$radio->pack(
		-side => 'top',
		-fill => 'x',
		-pady => '0',
		-anchor => 'w',
	    );
    }

    return $frame;
}

sub clear_trk_buttons {
    my $self = shift;
    my $o;

    foreach $o (@{$self->{TKO}}) {
	$o->destroy;
    }
    $self->{TKO} = [];
    $self->{TKBUTTONS} = 0;
}

sub make_trk_buttons {
    my $self = shift;
    my $tkframe = $self->{TKFRAME};
    my $tkrframe; # track row frame
    my $tkidx;

    return if $self->{TKBUTTONS} == $self->{CD}->tracks;

    $self->clear_trk_buttons;

    $self->{TKBUTTONS} = $self->{CD}->tracks;

    for ($tkidx = 1; $tkidx <= $self->{CD}->tracks; $tkidx++) {
	# create a new row frame for each seven buttons
	if ($tkidx % 7 == 1) {
	    $tkrframe = $tkframe->Frame(
		    -relief => 'flat',
		    -borderwidth  => 0,
		    -highlightthickness => 0,
	    )->pack(-side => 'top', -fill => 'x', -padx => 0, -pady => 0);
	    unshift(@{$self->{TKO}}, $tkrframe);
	}

	my $tkidx = $tkidx;	# localized $tkidx for the closure
	my $tko;
	$tko = $tkrframe->Button(
	    -font => 'fixed',
	    -text => sprintf("%2d", $tkidx),
	    -padx => 1,
	    -pady => 1,
	    -highlightthickness => 0,
	    -command => sub {
		    my $cd = $self->{CD};
		    $cd->cdplay(($cd->track_info($tkidx))[0], $cd->frames);
		},
	)->pack(-side => 'left', -anchor => 'nw', -padx => 2, -pady => 1);
	unshift(@{$self->{TKO}}, $tko);
    }

    return $self;
}

sub error_dialog {
    my $self = shift;
    return if $self->{IGNORING_ERRORS};
    $self->{ERROR_DIALOG}->Subwidget('message')->configure(-text => $_[0]);
    my $button = $self->{ERROR_DIALOG}->Show;
    exit(1) if $button eq 'Exit';
    $self->{IGNORING_ERRORS} = 1 if $button eq 'Ignore Errors';
}

sub tagger { shift->tag('configure', @ARG); }

# XXX: no longer used but I want to put the labels in red so
# XXX: I'm keeping it around for now.
sub addtagged {
    my($w_t,$tag,$ttext,$atext) = @ARG;
    # display styles
    my(@bold, @normal);
    if ($w_t->depth > 1) {
	@bold = (
	    -foreground	=> '#a00000',
	    -background	=> undef,
	    # future link style
	    # -foreground	=> undef,
	    # -background	=> '#43ce80',
	    -relief	=> 'raised',
	    -borderwidth => 1,
	    -lmargin1	=> '1c',
	);
	@normal = (
	    -foreground => '#a00000',
	    -background => undef,
	    -relief	=> 'flat',
	    -borderwidth => 0,
	    -lmargin1	=> '1c',
	);
    }
    else {
	@bold = (-foreground => 'white', -background => 'black');
	@normal = (-foreground => undef, -background => undef);
    }
    tagger($w_t, $tag, @normal);
    $w_t->tag('bind', $tag, '<Any-Enter>' => [\&tagger, $tag, @bold]);
    $w_t->tag('bind', $tag, '<Any-Leave>' => [\&tagger, $tag, @normal]);
    $w_t->insert('end', $ttext, $tag);
    $w_t->insert('end', $atext) if $atext;
}
sub mixer_help {
    my($self, $title) = @ARG;
    my $parent = $self->{MAINWINDOW};
    my $mixer = $self->{MIXER};
    my $w = $parent->Toplevel;
    $w->title($title);

    my $w_buttons = $w->Frame;
    $w_buttons->pack(
	-side	=> 'bottom',
	-expand	=> 'n',
	-fill	=> 'none',
	-pady	=> '2m',
    );
    my $w_dismiss = $w_buttons->Button(
        -text    => 'Dismiss',
        -command => ['destroy', $w],
    );
    $w_dismiss->pack(-side => 'left', -expand => 'no', -fill => 'none');

    my $w_t = $w->Text(
	-setgrid	=> 'true',
	-width		=> '60',
	-height		=> '18',
	-wrap		=> 'word',
	-tabs		=> '2c',
	# -state	=> 'disabled',
    );
    my $w_s = $w->Scrollbar(-command => ['yview', $w_t]);
    $w_t->configure(-yscrollcommand => ['set', $w_s]);
    $w_s->pack(-side => 'right', -fill => 'y');
    $w_t->pack(-expand => 'yes', -fill => 'both');

    $w_t->insert('0.0', " XMIXER Help ", 'title');
    &tagger($w_t, 'title',
	-font => '-*-Helvetica-Bold-R-Normal-*-140-*-*-*-*-*-*',
	-justify => 'center',
	-relief => 'raised',
	-borderwidth => 1,
    );
    $w_t->insert('end', "\n\n");
    $w_t->insert('end', "Use the right mouse button to independently move sliders.\n\n");

    my $item = 1;
    my($dev, $tag);
    foreach $dev ($mixer->devices) {
	$tag = "tag" . $item++;
	$w_t->insert('end',
	    $mixer->label($dev) . "\t-- " . $mixer->desc($dev) . "\n");
	# $w_t->tag('bind', $tag, '<1>' => sub {print "tag $tag\n";});
    }

    $w_t->insert('end', "\n");

    $self->{'play_label'} = $w_t->Label(-image => $self->{'play_bit'});
    $self->{'stop_label'} = $w_t->Label(-image => $self->{'stop_bit'});
    $self->{'step_label'} = $w_t->Label(-image => $self->{'step_bit'});
    $self->{'rewind_label'} = $w_t->Label(-image => $self->{'rewind_bit'});
    $self->{'circle_off_label'} = $w_t->Label(-image => $self->{'circle_off_bit'});
    $self->{'circle_on_label'} = $w_t->Label(-image => $self->{'circle_on_bit'});
    $self->{'pause_label'} = $w_t->Label(-image => $self->{'pause_bit'});

    $w_t->window('create', 'end', -window => $self->{'circle_off_label'});
    $w_t->insert('end', "\t-- Mute Volume.\n");
    $w_t->window('create', 'end', -window => $self->{'circle_on_label'});
    $w_t->insert('end', "\t-- Unmute Volume.\n");

    $w_t->insert('end', "\n\n");

    $w_t->insert('end', " CD Controls ", 'title');
    $w_t->insert('end', "\n\n");
    $w_t->insert('end', "You must have read permission on the CD device to control the CD player:\n");
    $w_t->insert('end', "    chmod ugo+r $CDPLAYER\n\n");
    $w_t->insert('end', "The environment variable CDPLAYER can be used to select which drive to control.\n\n");

    $w_t->window('create', 'end', -window => $self->{'rewind_label'});
    $w_t->insert('end', "\t-- Play previous track.\n");
    $w_t->window('create', 'end', -window => $self->{'play_label'});
    $w_t->insert('end', "\t-- Start playing CD or resume from pause.\n");
    $w_t->window('create', 'end', -window => $self->{'stop_label'});
    $w_t->insert('end', "\t-- Stop CD.\n");
    $w_t->window('create', 'end', -window => $self->{'pause_label'});
    $w_t->insert('end', "\t-- Pause/Resume CD.\n");
    $w_t->window('create', 'end', -window => $self->{'step_label'});
    $w_t->insert('end', "\t-- Play next track.\n");

    $w_t->insert('end', "\n\n");
    $w_t->insert('end', "Trk: TT   MM:SS   [##]\n");
    $w_t->insert('end', "\t-- TT    currently playing track\n");
    $w_t->insert('end', "\t-- MM:SS minute and second of the current track\n");
    $w_t->insert('end', "\t-- ##    total number of tracks on the disk\n");
    $w_t->insert('end', "\nWhen paused, the track number appears in angle brackets, `??' indicates it cannot find a CD and `--' indicates the CD is stopped.\n");

    $w_t->mark('set', 'insert', '0.0');
}

sub do_help {
    # we use pop() here because bind passes the widget as the
    # first argument but -command doesn't
    my $self = pop(@ARG);
    mixer_help($self, 'Xmixer Help');
}

sub buildimages {
    my $self = shift;
    my $window = $self->{MAINWINDOW};

$self->{'play_bit'} = $window->Bitmap(-data => '
#define play_width 20
#define play_height 20
static unsigned char play_bits[] = {
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x40, 0x01, 0x00,
   0xc0, 0x02, 0x00, 0x40, 0x05, 0x00, 0xc0, 0x0a, 0x00, 0x40, 0x14, 0x00,
   0xc0, 0x28, 0x00, 0x40, 0x50, 0x00, 0xc0, 0xe0, 0x00, 0x40, 0x70, 0x00,
   0xc0, 0x38, 0x00, 0x40, 0x1c, 0x00, 0xc0, 0x0e, 0x00, 0x40, 0x07, 0x00,
   0xc0, 0x03, 0x00, 0xc0, 0x01, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00};
');

$self->{'stop_bit'} = $window->Bitmap(-data => '
#define stop_width 20
#define stop_height 20
static unsigned char stop_bits[] = {
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0x07,
   0xa8, 0xaa, 0x02, 0x58, 0x55, 0x05, 0x28, 0x00, 0x03, 0x18, 0x00, 0x05,
   0x28, 0x00, 0x03, 0x18, 0x00, 0x05, 0x28, 0x00, 0x03, 0x18, 0x00, 0x05,
   0x28, 0x00, 0x03, 0x18, 0x00, 0x05, 0x28, 0x00, 0x03, 0x18, 0x00, 0x05,
   0xe8, 0xff, 0x03, 0x58, 0x55, 0x05, 0xa8, 0xaa, 0x02, 0x00, 0x00, 0x00};
');

$self->{'step_bit'} = $window->Bitmap(-data => '
#define step_width 20
#define step_height 20
static unsigned char step_bits[] = {
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x06, 0x00, 0x28, 0x0a, 0x00,
   0x30, 0x16, 0x00, 0x28, 0x2a, 0x00, 0x30, 0x56, 0x00, 0x28, 0xa2, 0x00,
   0x30, 0x46, 0x01, 0x28, 0x82, 0x02, 0x30, 0x06, 0x07, 0x28, 0x82, 0x03,
   0x30, 0xc6, 0x01, 0x28, 0xe2, 0x00, 0x30, 0x76, 0x00, 0x28, 0x3a, 0x00,
   0x30, 0x1e, 0x00, 0x28, 0x0e, 0x00, 0x38, 0x06, 0x00, 0x00, 0x00, 0x00};
');

$self->{'rewind_bit'} = $window->Bitmap(-data => '
#define rewind_width 20
#define rewind_height 20
static unsigned char rewind_bits[] = {
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8c, 0x01, 0x00, 0x4a, 0x01,
   0x00, 0x8d, 0x01, 0x80, 0x4a, 0x01, 0x40, 0x8d, 0x01, 0xa0, 0x48, 0x01,
   0x50, 0x8c, 0x01, 0x28, 0x48, 0x01, 0x1c, 0x8c, 0x01, 0x18, 0x48, 0x01,
   0x70, 0x8c, 0x01, 0xe0, 0x48, 0x01, 0xc0, 0x8d, 0x01, 0x80, 0x4b, 0x01,
   0x00, 0x8f, 0x01, 0x00, 0x4e, 0x01, 0x00, 0xcc, 0x01, 0x00, 0x00, 0x00};
');

$self->{'circle_off_bit'} = $window->Bitmap(-data => '
#define circle_off_width 20
#define circle_off_height 20
static unsigned char circle_off_bits[] = {
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x00, 0x1f, 0x00, 0xc0, 0x60, 0x00, 0x20, 0x9f, 0x00, 0xa0, 0xa0, 0x00,
   0x50, 0x40, 0x01, 0x50, 0x40, 0x01, 0x50, 0x40, 0x01, 0x50, 0x40, 0x01,
   0x50, 0x40, 0x01, 0xa0, 0xa0, 0x00, 0x20, 0x9f, 0x00, 0xc0, 0x60, 0x00,
   0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
');

$self->{'circle_on_bit'} = $window->Bitmap(-data => '
#define circle_on_width 20
#define circle_on_height 20
static unsigned char circle_on_bits[] = {
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x00, 0x1f, 0x00, 0xc0, 0x60, 0x00, 0x20, 0x9f, 0x00, 0xa0, 0xbf, 0x00,
   0xd0, 0x7f, 0x01, 0xd0, 0x7f, 0x01, 0xd0, 0x7f, 0x01, 0xd0, 0x7f, 0x01,
   0xd0, 0x7f, 0x01, 0xa0, 0xbf, 0x00, 0x20, 0x9f, 0x00, 0xc0, 0x60, 0x00,
   0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
');

$self->{'pause_bit'} = $window->Bitmap(-data => '
#define pause_width 20
#define pause_height 20
static unsigned char pause_bits[] = {
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x60, 0x00, 0xa0, 0x50, 0x00,
   0xc0, 0x60, 0x00, 0xa0, 0x50, 0x00, 0xc0, 0x60, 0x00, 0xa0, 0x50, 0x00,
   0xc0, 0x60, 0x00, 0xa0, 0x50, 0x00, 0xc0, 0x60, 0x00, 0xa0, 0x50, 0x00,
   0xc0, 0x60, 0x00, 0xa0, 0x50, 0x00, 0xc0, 0x60, 0x00, 0xa0, 0x50, 0x00,
   0xc0, 0x60, 0x00, 0xa0, 0x50, 0x00, 0xe0, 0x70, 0x00, 0x00, 0x00, 0x00};
');

    return $self;
}

sub new {
    my $template = shift;
    my ($mw, $mixer_dev) = @ARG;

    my $class = ref($template) || $template;
    my $self = bless { }, $class;

    $self->{MAINWINDOW} = $mw;
    $self->{MIXER_DEV} = $mixer_dev;
    $self->{SLIDERS} = { };

    # STATE...
    $self->{MUTED} = 0;
    $self->{CDINTERFACE} = 0;
    $self->{IGNORING_ERRORS} = 0;

    ################################################################
    ###   Mixer   ##################################################
    ################################################################

    my $mixer = Create Sound::Voxware::Mixer ($mixer_dev);
    $self->{MIXER} = $mixer;

    $self->buildimages;

    ###
    ### dialog boxes
    ###
    # Global
    $self->{ERROR_DIALOG} = $mw->Dialog(
	-title			=> 'Mixer Error',
	-bitmap         	=> 'warning',
	-text			=> '',
	-default_button		=> 'OK',
	-buttons		=> ['OK', 'Ignore Errors', 'Exit'],
    );

    ###
    ### keyboard escapes
    ###
    $mw->bind('<Any-q>'     => sub {exit});
    $mw->bind('<Control-c>' => sub {exit});

    ###
    ### Menu Bar
    ###
    my $menuBar = $mw->Frame(-relief => 'raised', -bd => 2);
    $menuBar->pack(-side => 'top', -fill => 'x');
    ### File...
    my $menubar_file = $menuBar->Menubutton(
	-text			=> 'File',
	-underline		=> 0,
	-highlightthickness	=> 0,
    );
    $menubar_file->command(
	-label     => 'Quit',
	-underline => 0,
	-command   => [sub {exit}],
    );
    $menubar_file->pack(-side => 'left');

    ###
    ### Help Popup
    ###
    my $menubar_help = $menuBar->Button(
	-text			=> 'Help',
	-underline		=> 0,
	-borderwidth 		=> 0,
	-highlightthickness	=> 0,
	-command		=> [\&do_help, $self],
    );
    # these calls to do_help insert the widget as the first argument
    $mw->bind('<Alt-H>'        => [\&do_help, $self]);
    $mw->bind('<Alt-h>'        => [\&do_help, $self]);
    $mw->bind('<Any-question>' => [\&do_help, $self]);

    $menubar_help->pack(-side => 'right');

    ###
    ### Outer Frame for Interface
    ###
    my $window = $mw->Frame;
    $window->pack(-expand => 1, -fill => 'both');

    ###
    ### Build sliders for each device channel
    ###
    my $dev;
    foreach $dev ($mixer->devices) {
	$self->mixer_slider($window, $mixer, $dev, $mixer->label($dev))
	    ->pack(-side => 'left', -fill => 'y', -padx => 2);
    }

    ###
    ### Recording device selector
    ###
    mixer_rdevs($self, $window, $mixer, "Record From")
	->pack(-side => 'top', -anchor => 'nw', -fill => 'both');

    my $ctrl_frame = $window->Frame(
	    -relief => 'ridge',
	    -borderwidth  => 2,
	    -highlightthickness => 0,
    );
    $ctrl_frame->pack(-side => 'top', -fill => 'both', -padx => 0, -pady => 2);

    my $c_frame = $ctrl_frame->Frame(
	    -relief => 'flat',
	    -borderwidth  => 0,
	    -highlightthickness => 0,
    );
    $c_frame->pack(-side => 'top', -fill => 'x', -anchor => 'center', -padx => 2, -pady => 1);

    my $cd_frame = $ctrl_frame->Frame(
	    -relief => 'flat',
	    -borderwidth  => 0,
	    -highlightthickness => 0,
    );
    $cd_frame->pack(-side => 'bottom', -fill => 'x', -padx => 0, -pady => 0);

    ###
    ### Mute
    ###
    sub xcontrol {
	my $self = shift;
	my($dev, $spec) = @ARG;
	my($lval, $rval) = split(/:/, $spec);
	my($left, $right) = @{$self->{SLIDERS}->{$dev}};
	my($oleft, $oright) = ($left->get, $right->get);
	if ($self->{MIXER}->is_stereo($dev)) {
	    $left->set($lval);
	    $right->set($rval);
	    $self->{MIXER}->attribute($dev, "$lval:$rval");
	} else {
	    $left->set($lval);
	    $self->{MIXER}->attribute($dev, $lval);
	}
	eval { $self->{MIXER}->store };
	$self->error_dialog($@) if $@;
	return "$oleft:$oright";
    }
    sub mute {
	my $self = pop(@ARG);

	if ($self->{MUTED}) {		# restore saved volume
	    $self->xcontrol('volume', $self->{MUTED});
	    $self->{MUTED} = 0;
	    $self->{MUTE}->configure(-image => $self->{'circle_off_bit'});
	}
	else {				# mute
	    $self->{MUTED} = $self->xcontrol('volume', '0:0');
	    $self->{MUTE}->configure(-image => $self->{'circle_on_bit'});
	}
    }
    $self->{MUTE} = $c_frame->Button(
	-image		=> $self->{'circle_off_bit'},
	-command	=> [\&mute, $self],
    )->pack(-side => 'left', -fill => 'x', -anchor => 'center');

    ###
    ### CD control stuff
    ###
    $self->{CD} = new Cdrom;
    if (defined $self->{CD}) {
	$self->{CDINTERFACE} = 1;
	$self->{CDPREV} = $c_frame->Button(
	    -image		=> $self->{'rewind_bit'},
	    -command	=> sub {
		my $state = $self->cdstate;
		my $cd = $self->{CD};
		return unless defined $cd;
		if ($state == Cdrom::STOPPED) {
		    # play the whole CD
		    $cd->cdplay(($cd->track_info(1))[0], $cd->frames);
		}
		elsif ($state == Cdrom::PAUSED || $state == Cdrom::PLAYING) {
		    # play previous song
		    $cd->cdplay(($cd->track_info($cd->cur_track-1))[0],
		       $cd->frames) unless $cd->cur_track == 1;
		}
		else {
		    $self->error_dialog("Couldn't Play CD, in unknown state");
		}
		update_cdinfo($self);
	    },
	)->pack(-side => 'left', -fill => 'x', -anchor => 'center');
	$self->{CDPLAY} = $c_frame->Button(
	    -image		=> $self->{'play_bit'},
	    -command	=> sub {
		my $state = $self->cdstate;
		my $cd = $self->{CD};
		return unless defined $cd;
		if ($state == Cdrom::STOPPED) {
		    # play the whole CD
		    $cd->cdplay(($cd->track_info(1))[0], $cd->frames);
		    $self->{CDPLAY}->configure(-image => $self->{'stop_bit'});
		}
		elsif ($state == Cdrom::PAUSED) {
		    # resume playing
		    $cd->resume;
		}
		elsif ($state == Cdrom::PLAYING) {
		    $cd->cdstop;
		    # next we'll play
		    $self->{CDPLAY}->configure(-image => $self->{'play_bit'});
		}
		else {
		    $self->error_dialog("Couldn't Play CD, in unknown state");
		}
		update_cdinfo($self);
	    },
	)->pack(-side => 'left', -fill => 'x', -anchor => 'center');
	$self->{CDPAUSE} = $c_frame->Button(
	    -image		=> $self->{'pause_bit'},
	    -command	=> sub {
		my $state = $self->cdstate;
		my $cd = $self->{CD};
		return unless defined $cd;
		if ($state == Cdrom::STOPPED) {
		    ; # do nothing
		}
		elsif ($state == Cdrom::PAUSED) {
		    # resume playing
		    $cd->resume;
		}
		elsif ($state == Cdrom::PLAYING) {
		    $cd->pause;
		}
		else {
		    $self->error_dialog("Couldn't Pause/Resume CD, in unknown state");
		}
		update_cdinfo($self);
	    },
	)->pack(-side => 'left', -fill => 'x', -anchor => 'center');
	$self->{CDNEXT} = $c_frame->Button(
	    -image		=> $self->{'step_bit'},
	    -command	=> sub {
		my $state = $self->cdstate;
		my $cd = $self->{CD};
		return unless defined $cd;
		if ($state == Cdrom::STOPPED) {
		    # play the whole CD
		    $cd->cdplay(($cd->track_info(1))[0], $cd->frames);
		}
		elsif ($state == Cdrom::PAUSED || $state == Cdrom::PLAYING) {
		    # play next song
		    $cd->cdplay(($cd->track_info($cd->cur_track+1))[0],
		       $cd->frames) unless $cd->cur_track == $cd->tracks;
		}
		else {
		    $self->error_dialog("Couldn't Play CD, in unknown state");
		}
		update_cdinfo($self);
	    },
	)->pack(-side => 'left', -fill => 'x', -anchor => 'center');

	$self->{CDTRACK} = $cd_frame->Label(
	    -text => " Trk: ?? ",
	    -font => 'fixed',
	    -highlightthickness => 0,
	)->pack(-side => 'left', -fill => 'x', -anchor => 'center');
	$self->{CDTIME} = $cd_frame->Label(
	    -text => " ??:?? ",
	    -font => 'fixed',
	    -highlightthickness => 0,
	)->pack(-side => 'left', -fill => 'x', -anchor => 'center');
	$self->{CDTRACKS} = $cd_frame->Label(
	    -text => " [??] ",
	    -font => 'fixed',
	    -highlightthickness => 0,
	)->pack(-side => 'left', -fill => 'x', -anchor => 'center');

	$self->{TRACKSLIDER} = $ctrl_frame->Scale(
		-width        => '0.1i',
		-showvalue    => 0,
		-orient       => 'horizontal',
		-borderwidth  => 2,
		-highlightthickness => 0,
		'-length'     => 100,
		-from         => 0,
		-to           => 100,
		-command      => sub {
			# XXX: need to seek the disk
			# XXX: watch out though, this is called whenever
			# XXX: update_cdinfo is called as well (how to tell?)
			# print "slider: ", $self->{TRACKSLIDER}->get, "\n";
		    },
	   );
	$self->{TRACKSLIDER}->set(0);
	$self->{TRACKSLIDER}->pack(-side => 'top', -anchor => 'center', -fill => 'x', -padx => 1, -pady => 0);

	$self->{TKFRAME} = $ctrl_frame->Frame(
		-relief => 'flat',
		-borderwidth  => 0,
		-highlightthickness => 0,
	)->pack(-side => 'top', -fill => 'x', -padx => 0, -pady => 0);

	$self->{TKBUTTONS} = 0;
	update_cdinfo($self);
    }

    # reschedules itself
    update_interface($self);

    return $self;
}
