#! /usr/local/bin/tkmidi -f
#
# TkSeq is a MIDI sequencer designed to work with Tcl/Tk and the tclmidi
# extensions. This is version 0.94d, Copyright (c) 1995  Greg Wolodkin
#
# This version requires tclmidi-3.0 or better.
# 
# email: greg@eecs.berkeley.edu.  See the file COPYING for more info..
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#
# ---------------------------------------------------------------------
# Some notation:  ALL-CAPS are user-definable global variables.
#                 First letter capitalized indicates a global 
#                 state variable that is not user-defined.
#
# User Configuration begins here -- edit definitions if you want
# In C/C++, these will be #defines

proc userDefaults {} {

  global DDIVISION DQUANTIZE NUMTRACKS MTHRU MAXQUANT
  global TEMPO FMASK MEAN VARIANCE HAVE_TEX BITMAPS TEAROFF
  global SHOWPROG SHOWCHAN SHOWBARS SHOWBEAT SHOWQUAN SHOWMEAS SHOWMIDC
  global PIANOSCALE CHANNEL1 NUMDEV
  global HAVE_VOXWARE_GUS PATCH_DIR GM_PATS GUS_AUTO_LOAD GUS_AUTO_THRU

  # Default device is /dev/midi0.. look for no more than 4 devices
  set NUMDEV 4

  # 32 tracks is plenty for me.  Add more if you need 'em.
  set NUMTRACKS 32

  # file mask matches everything by default
  set FMASK ""

  # set BITMAPS to point to wherever you stash the bitmaps
  set BITMAPS /usr/include/X11/bitmaps

  # default variance for random perturbations moves events by just
  # a few SMF ticks
  set MEAN 0
  set VARIANCE 1.0 

  # default quantization is 1/16th notes, but nothing is quantized
  # unless you specifically ask for it.  MAXQUANT is for Piano Roll.
  set DQUANTIZE 16 
  set MAXQUANT  16

  # I assume you have midi2tex, tex, and xdvi.  Look for midi2tex
  # below to futz with it.  It's not reliable or fast ;-)
  set HAVE_TEX 1 

  # I have a GUS which is managed by the Linux sound driver.  Enable this if
  # you do too.  Comment GM_PATS out if you don't, to save some space.
  set HAVE_VOXWARE_GUS 1
  set GUS_AUTO_LOAD 1
  set GUS_AUTO_THRU 1
  set PATCH_DIR /dos/ultrasnd/midi
  set GM_PATS {acpiano britepno synpiano honktonk epiano1 epiano2 hrpschrd \
clavinet celeste glocken musicbox vibes marimba xylophon tubebell santur \
homeorg percorg rockorg church reedorg accordn harmonca concrtna nyguitar \
acguitar jazzgtr cleangtr mutegtr odguitar distgtr gtrharm acbass fngrbass \
pickbass fretless slapbas1 slapbas2 synbass1 synbass2 violin viola cello \
contraba marcato pizzcato harp timpani marcato slowstr synstr1 synstr2 \
choir doo voices orchhit trumpet trombone tuba mutetrum frenchrn hitbrass \
synbras1 synbras2 sprnosax altosax tenorsax barisax oboe englhorn bassoon \
clarinet piccolo flute recorder woodflut bottle shakazul whistle ocarina \
sqrwave sawwave calliope chiflead voxlead voxlead lead5th basslead fantasia \
warmpad polysyn ghostie bowglass metalpad halopad sweeper aurora soundtrk \
crystal atmosphr freshair unicorn sweeper startrak sitar banjo shamisen \
koto kalimba bagpipes fiddle shannai carillon agogo steeldrm woodblk taiko \
toms syntom revcym fx-fret fx-blow seashore jungle telephon helicptr \
applause ringwhsl}

  # these are used for the creation of new MIDI files,
  # but will be overridden when reading existing MIDI files
  set DDIVISION 120
  set TEMPO 120

  # Set this to 1 if you want tear-off menus
  set TEAROFF 0

  # Set this to one if you don't like channel 0
  set CHANNEL1 1

  # Set this to one to force MIDI THRU on by default when tkseq starts
  set MTHRU 1

  # Width (in pixels) of a single (white) piano key in Piano Roll view
  set PIANOSCALE 12.000000

  # Show channel assignments and patch changes by default
  # Set these to zero if you don't like the delay they introduce
  # Or you can disable them via the `View' menu..
  set SHOWMEAS 1
  set SHOWCHAN 1
  set SHOWPROG 1

  # For the piano roll -- these are a matter of taste.
  set SHOWMIDC 1
  set SHOWBARS 1
  set SHOWBEAT 0
  set SHOWQUAN 0

  if { ! [file exists ~/.tkseqopt] } {
    dialog .foo . "No options file found...\nCreating default version" \
      info 0 OK
    createDefaultOptions
  }
  option readfile ~/.tkseqopt
}

# -----------END of user configuration --------------------------------
#
# -----------Default Options-------------------------------------------
#
proc createDefaultOptions {} {

  if {[catch "set foo [open ~/.tkseqopt w]"]} {
    dialog .foo . "Couldn't create ~/.tkseqopt!" error 0 OK
    exit
  }

  puts $foo "! Here's some defaults to make things a little more"
  puts $foo "! peaceful.  Don't take it personal, if you really"
  puts $foo "! like pink and beige sliders that's OK by me ;-)"
  puts $foo ""
  puts $foo "! Tk's resources are so odd that it's almost impossible"
  puts $foo "! to use .Xdefaults *and* built-in options.  So I chose"
  puts $foo "! to have everything here in one place.  Remember that"
  puts $foo "! in Tk, order is important.. If things don't seem to be"
  puts $foo "! working as you'd expect then try again ;-)  You might"
  puts $foo "! want to check out Prof. Ousterhout's, pp.256-7"
  puts $foo ""
  puts $foo "! Main font"
  puts $foo "*font:		*-Times-Bold-R-Normal-*-14-*-*-*-*-*-*-*"
  puts $foo ""
  puts $foo "! Fonts used for certain widget classes (fixed width is best)"
  puts $foo \
    "*Listbox.font:	-dec-terminal-bold-r-normal--14-*-*-*-*-*-iso8859-1"
  puts $foo \
    "*Text.font:	-dec-terminal-bold-r-normal--14-*-*-*-*-*-iso8859-1"
  puts $foo \
    "*Entry.font:	-dec-terminal-bold-r-normal--14-*-*-*-*-*-iso8859-1"
  puts $foo ""
  puts $foo "! Fonts used elsewhere in special cases.."
  puts $foo \
    "*subtitle.font:	-b&h-lucida-bold-i-normal-sans-12-*-*-*-*-*-iso8859-1"
  puts $foo \
    "*message.font:	-*-Lucida-Bold-R-Normal-*-*-140-*-*-*-*-*-*"
  puts $foo \
    "*text.font:	-*-Times-Medium-R-Normal-*-14-*-*-*-*-*-*-*"
  puts $foo ""
  puts $foo "! This just takes up space.."
  puts $foo "*highlightThickness:		0"
  puts $foo ""
  puts $foo "! Some default colors -- keep them simple"
  puts $foo "*background:			#b0b0b0"
  puts $foo "*selectBackground:		#989898"
  puts $foo "*activeBackground:		#989898"
  puts $foo ""
  puts $foo "*foreground:			#000000"
  puts $foo "*selectForeground:		#000000"
  puts $foo "*activeForeground:		#000000"
  puts $foo ""
  puts $foo "*subtitle.foreground:		#606060"
  puts $foo "*disabledForeground:		#909090"
  puts $foo "	"
  puts $foo "! Used for piano roll grid, and for note events"
  puts $foo "*event.foreground:		#a0a0a0"
  puts $foo "*grid.foreground:		#909090"
  puts $foo "*measure.foreground:		#0000c0"
  puts $foo "*beat.foreground:		#b02050"
  puts $foo "*quantum.foreground:		#909090"
  puts $foo ""
  puts $foo "! Coloring the keyboard"
  puts $foo "*whitekey.background:		#d0d0d0"
  puts $foo "*blackkey.background:		#000000"
  puts $foo "*activekey.background:		#80b0d0"
  puts $foo ""
  puts $foo "! Scrollbars and sliders"
  puts $foo "*scroll.troughColor:		#909090"
  puts $foo "*scale.troughColor:		#909090"
  puts $foo "*scroll.width:			10"
  puts $foo ""
  puts $foo "! Normal `control' buttons are blue.."
  puts $foo "*play.foreground:		#3050b0"
  puts $foo "*record.foreground:		#3050b0"
  puts $foo "*pause.foreground:		#3050b0"
  puts $foo "*ffwd.foreground:		#3050b0"
  puts $foo "*rewind.foreground:		#3050b0"
  puts $foo "*stop.foreground:		#3050b0"
  puts $foo ""
  puts $foo "! Active ones get brighter -- green for play, red for record"
  puts $foo "*play.activeForeground:		#60d040"
  puts $foo "*record.activeForeground:	#d04060"
  puts $foo "*pause.activeForeground:	#4060d0"
  puts $foo "*stop.activeForeground:		#4060d0"
  puts $foo "*ffwd.activeForeground:		#4060d0"
  puts $foo "*rewind.activeForeground:	#4060d0"
  close $foo
}

# -----------Tclmidi detection-----------------------------------------
#
if {[catch "mididevice"]} {
  proc mididevice {} {
    set HasTclMidi 0
    return 0
  }
  foreach i "config copy delete free get make merge move play put \
           read record rewind split stop time track version wait" {
    set TmpMsg "The midi$i command requires tclmidi."
    proc midi$i args "dialog .h . \"$TmpMsg\" error 0 OK; return 0"
  }
} else {
  set HasTclMidi 1
}

# -----------BEGIN subroutines used by top level window ---------------
#
# Initialize everything
#
proc midiCleanSlate {realclean} {
  global DDIVISION NUMTRACKS SHOWPROG SHOWCHAN CHANNEL1 TKS_VERSION
  global MTHRU NUMDEV DEVTAB
  global MidiState SmpteClk LabelNow Now CurDev MasterDev
  global Now MuteList SoloList Modified MeasView MidiThru
  global PlayFile RecFile TmpFile PlayName StopTime TimeScale
  global KeyYval KeyYtag MeterMap MetroData
  global Mtracks Mformat Mdivision
  global GusThruPid
  global PatchList PatchFile
  global PLoadList PLoadFile
  global RawPitch NoteOff

  # load user-defaults and `static' data
  if {$realclean} {
    loadMappedEventData
    userDefaults
    for {set i 0} {$i < 4} {incr i} {
      set SmpteClk($i) 0 
      set MidiThru($i) $MTHRU
    }
    set LabelNow "MIDI:"
    set CurDev 0
    set MasterDev 0
    set DEVTAB(0,dev)  ""; set DEVTAB(0,raw) "";
    set DEVTAB(0,name) ""; set DEVTAB(0,map) "";

  # Some UltraSound stuff
    set GusThruPid 0
    set PLoadList ""
    set PLoadFile ""
  }

  if {$PlayFile != ""} { 
    closeTrackInfo {}
    closePianoRoll {}
    midifree $PlayFile
    updateButtons 0 1 0 0 0 0
  }

  if {$RecFile != ""} { midifree $RecFile }
  if {$TmpFile != ""} { midifree $TmpFile }
  set PlayFile ""
  set RecFile ""
  set TmpFile ""

  set MeterMap ""
  set MidiState stopped
  set SoloList ""
  set MuteList ""
  set Modified 0
  set StopTime 0
  set MeasView 1

  set Mformat 1
  set Mtracks 1
  set Mdivision $DDIVISION

  if {$SmpteClk($CurDev) == 1} {
    set Now "No Sync"
  } else {
    set Now [tick2measure 0]
  }
  set TimeScale 25.000000

  # 12 notes
  set KeyYval(0)  0.00000; set KeyYtag(0)  {white "C"}
  set KeyYval(1)  0.09286; set KeyYtag(1)  {black "C sharp" "D flat"}
  set KeyYval(2)  0.14286; set KeyYtag(2)  {white "D"} 
  set KeyYval(3)  0.26429; set KeyYtag(3)  {black "E flat" "D sharp"} 
  set KeyYval(4)  0.28571; set KeyYtag(4)  {white "E"} 
  set KeyYval(5)  0.42857; set KeyYtag(5)  {white "F"}
  set KeyYval(6)  0.52143; set KeyYtag(6)  {black "F sharp" "G flat"}
  set KeyYval(7)  0.57143; set KeyYtag(7)  {white "G"}
  set KeyYval(8)  0.67856; set KeyYtag(8)  {black "A flat" "G sharp"}
  set KeyYval(9)  0.71429; set KeyYtag(9)  {white "A"}
  set KeyYval(10) 0.83571; set KeyYtag(10) {black "B flat" "A sharp"}
  set KeyYval(11) 0.85714; set KeyYtag(11) {white "B"}

  set MetroData(meas,start) 0
  set MetroData(meas,stop) 32
  set MetroData(chan,meas) [expr 9 + $CHANNEL1]
  set MetroData(chan,beat) [expr 9 + $CHANNEL1]
  set MetroData(patch,meas) 37
  set MetroData(patch,beat) 37
  set MetroData(vol,meas)  127
  set MetroData(vol,beat)   80
  set MetroData(dur,meas)  [expr $Mdivision / 4]
  set MetroData(dur,beat)  [expr $Mdivision / 4]

  # this wipes the main window clean
  if {[winfo exists .trkname.list]} { showTrackEverything {} }

  # This is the default noise to make
  set PatchList 0
  set PatchFile acpiano

  # These are just for safety
  set RawPitch 0
  set NoteOff "0 NoteOff 0 0 0"

  # open a default `empty' file
  if {! $realclean} {
    set PlayFile [midimake]
    midiconfig $PlayFile {tracks 1} {format 1} "division $DDIVISION"

    set PlayName "untitled.mid"
    wm iconname . $PlayName
    wm title . "tkseq $TKS_VERSION:  $PlayName"
  }
}

# ---------------------------------------------------------------------
# Generate the track channel list
#
proc showTrackEverything {tlist} {
  showTrackNames    $tlist
  showTrackPrograms $tlist
  showTrackChannels $tlist
  showTrackMuteList
  showTrackStatusLine
  # Erase what's there, but don't build the new one yet..
  if {$tlist == ""} {
    .trkmeas.list delete 0 end
     update
  }
  showTrackMeasures $tlist
}

proc showTrackStatusLine {} {
  global Mtracks Mdivision Mformat PlayFile
  global DDIVISION

  if {$PlayFile != ""} {
    scan [getConfig $PlayFile] "%d %d %d" Mdivision Mformat Mtracks
    set DDIVISION $Mdivision
  }
}

# ---------------------------------------------------------------------
# Write changed names to midi file in the form of MetaSequenceName
# events.  tlist is a list of lists, {number name} {number name} etc.
#
proc writeTrackNames {tlist} {
  global Modified PlayFile

  scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk

  if {$tlist == ""} {
    for {set i 0} {$i < $mtrk} {incr i} {
      set tlist [lappend tlist $i]
    }
  }

  foreach i $tlist {
    midirewind $PlayFile $i
    set eventlist [midiget $PlayFile $i 0]
    foreach event $eventlist {
      if {[lindex $event 1] == "MetaSequenceName"} {
         mididelete $PlayFile $i $event
         set Modified 1
      }
    }
    set tmpname [.trkname.list get $i]
    if {$tmpname != ""} {
       midiput $PlayFile $i [list 0 MetaSequenceName "$tmpname"]
       set Modified 1
    }
    midirewind $PlayFile
  }
}

# --------------------------------------------------------------------
#
proc showTrackNames {tlist} {
  global NUMTRACKS
  global PlayFile

  scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk

  if {$tlist == ""} {
    .trkname.list delete 0 end
    for {set i 0} {$i < $NUMTRACKS} {incr i} {.trkname.list insert end ""}
    if {$PlayFile == ""} { return }
    for {set i 0} {$i < $mtrk} {incr i} { set tlist "$tlist $i" }
  }

  foreach i $tlist {
    if {$i < $mtrk} {
      if {$i == 0 && $mfmt == 1} {
        set tmpname "<meta>"
      } else {
        set tmpname "<untitled>"
      }
      midirewind $PlayFile $i

      # don't check every event -- it's too time consuming. 
      set eventlist [midiget $PlayFile $i 0]
      foreach event $eventlist {
        if {[lindex $event 1] == "MetaSequenceName"} {
          set tmpname [lindex $event 2]
          break
        }
      }
    } else {
      set tmpname ""
    }
    .trkname.list delete $i
    .trkname.list insert $i $tmpname
  }
}

# --------------------------------------------------------------------
#
proc showTrackPrograms {tlist} {
  global NUMTRACKS SHOWPROG DEVTAB CHANNEL1 GM_PATS
  global PlayFile CurDev MidiState
  global PatchList PatchFile

  set mf $PlayFile
  scan [getConfig $mf] "%d %d %d" mdiv mfmt mtrk

  # Wipe the list clean if we don't want to see program changes
  if {$SHOWPROG == 0} {set mf ""; set tlist ""}

  if {$tlist == ""} {
    .trkprog.list delete 0 end
    for {set i 0} {$i < $NUMTRACKS} {incr i} {.trkprog.list insert end ""}
    if {$mf == ""} { return }
    for {set i 0} {$i < $mtrk} {incr i} { set tlist "$tlist $i" }
  }

  foreach i $tlist {
    if {$i < $mtrk} {
      set tmpprog " -- "
      set tmpcount 0
      midirewind $mf $i

      # don't check every event -- it's too time consuming. 
      # for now check no more than say the first 32 events..
      # Hmm.  I really do need to get all the program changes.

      while {[set event [midiget $mf $i next]] != "EOT"} {
        if {[lindex $event 1] == "Program"} {
          set tt [lindex $event 3]

	# Add this patch to the patch list
        # Might as well add the default GM binding for now
          if {[lsearch -exact $PatchList $tt] == -1} {
            set PatchList [lsort -integer [lappend PatchList $tt]]
            set index [lsearch -exact $PatchList $tt]
            set pname [lindex $GM_PATS $tt]
            set PatchFile [linsert $PatchFile $index $pname]
          }

          if {$tmpprog == " -- "} {

            # Might as well drop these into place now, assuming time 0.
            # FIX ME when miditime is working (at present, I can't be
            # guaranteed to get a zero initially..)

            if {$DEVTAB($CurDev,raw) != "" && $MidiState == "stopped"} {
                 set channel [string range [.trkchan.list get $i] 0 1]
                 midisend $DEVTAB($CurDev,raw) \
                    "0 Program [expr $channel - $CHANNEL1] $tt" 
            }

            if {$tt < 10} {
              set tmpprog "  $tt "
            } elseif {$tt < 100} {
              set tmpprog " $tt "
            } else {
              set tmpprog "$tt "
            }
          } else {
            if {[string first * $tmpprog] == -1} {
              if {[expr $tmpprog] != $tt} {
                set tmpprog [string trimright $tmpprog]*
              }
        #     break  Nope.. get them all
            } 
          }
        }
        # FIX ME
        # if {[incr tmpcount] > 32} { break }
      }
    } else {
      set tmpprog ""
    }
    .trkprog.list delete $i
    .trkprog.list insert $i $tmpprog
  }
}

# --------------------------------------------------------------------
#
proc showTrackChannels {tlist} {
  global CHANNEL1 SHOWCHAN NUMTRACKS
  global PlayFile

  set mf $PlayFile
  scan [getConfig $mf] "%d %d %d" mdiv mfmt mtrk

  # Wipe the list clean if we don't want to see program changes
  if {$SHOWCHAN == 0} {set mf ""; set tlist ""}

  if {$tlist == ""} {
    .trkchan.list delete 0 end
    for {set i 0} {$i < $NUMTRACKS} {incr i} {.trkchan.list insert end ""}
    if {$mf == ""} { return }
    for {set i 0} {$i < $mtrk} {incr i} { set tlist "$tlist $i" }
  }

  foreach i $tlist {
    if {$i < $mtrk} {
      set tmpchan "--"
      set tmpcount 0
      midirewind $mf $i

      # don't check every event -- it's too time consuming. 
      # for now check no more than say the first 32 notes..
      while {[set event [midiget $mf $i next]] != "EOT"} {
        set etype [lindex $event 1]
        if {[string compare [string range $etype 0 3] "Note"] == 0} {
          set tt [expr [lindex $event 2] + $CHANNEL1]
          if {$tmpchan == "--"} {
            if {$tt < 10} {
              set tmpchan " $tt "
            } else {
              set tmpchan "$tt "
            }
          } else {
            if {[string trim $tmpchan] != $tt} {
              set tmpchan "[string trimright $tmpchan]*"
              break
            } 
          }
          # FIX ME
          if {[incr tmpcount] > 32} { break }
        }
      }
    } else {
      set tmpchan ""
    }
    .trkchan.list delete $i
    .trkchan.list insert $i $tmpchan
  }
}

# --------------------------------------------------------------------
#
proc showTrackMeasures {tlist} {
  global NUMTRACKS SHOWMEAS
  global PlayFile

  set mf $PlayFile

  # Wipe the list clean if we don't want to see program changes
  if {$SHOWMEAS == 0} {set mf ""; set tlist ""}

  set gotnull 0
  scan [getConfig $mf] "%d %d %d" mdiv mfmt mtrk

  if {$tlist == ""} {
    .trkmeas.list delete 0 end
    for {set i 0} {$i < $NUMTRACKS} {incr i} {.trkmeas.list insert end ""}
    if {$mf == ""} { return }
    for {set i 0} {$i < $mtrk} {incr i} { set tlist "$tlist $i" }
  }

  foreach i $tlist {
    if {$i < $mtrk} {
      set mcount 0
      set tmplist ""
      set mtick [miditrack $mf $i end]
      scan [tick2measure $mtick] "%d:" mmeas; incr mmeas

      midirewind $mf $i
      for {set k 0} {$k < $mmeas} {incr k} {
        set tick  [measure2tick "$k:0:0"]
        set event [midiget $mf $i $tick]

        if {$event == ""} {
          if {$gotnull == 0} {
            dialog .foo . "midiget returned null: $i $tick" info 0 OK
            set gotnull 1
          }
          set event [midiget $mf $i next]
        }

        if {$event == "EOT"} {
          set k $mmeas
          break
        }
        set tick [lindex [lindex $event 0] 0]
        scan [tick2measure $tick] "%d" tmeas
        for {set j $k} {$j < $tmeas} {incr j} { set tmplist "$tmplist." }
        set tmplist "$tmplist\o"
        set k $tmeas
      }
    } else {
      set tmplist ""
    }
    .trkmeas.list delete $i
    .trkmeas.list insert $i "$tmplist"
  }
}

proc showTrackMuteList {} {
  global NUMTRACKS
  global MuteList SoloList PlayFile

  # erase whatever is there already
  .trkmute.list delete 0 end

  if {$PlayFile == ""} { return }

  scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk

  for {set i 0} {$i < $mtrk} {incr i} {
    .trkmute.list insert end "<play>"
  }
  for {set i $mtrk} {$i < $NUMTRACKS} {incr i} {
    .trkmute.list insert end ""
  }
  foreach i $MuteList {
    if {$i < $mtrk} {
      .trkmute.list delete $i
      .trkmute.list insert $i "<mute>"
    } else {
      set pos [lsearch -exact $MuteList $i]
      set MuteList [lreplace $MuteList $pos $pos]
    }
  }
  if {$SoloList != ""} {
    if {$SoloList < $mtrk} {
      .trkmute.list delete $SoloList
      .trkmute.list insert $SoloList "<solo>"
    } else {
      set SoloList ""
    }
  }
}

#  ---------------------------------------------------------------------
# Enable or disable play/record buttons based on sequencer state
# This works only if there is a MIDI device compiled into tkmidi.
#
proc updateButtons {play rec stop pause ffwd rewind} {
  global TEAROFF MidiStatus

  set plm [.mbar.realtime.menu index Play]
  set rcm [.mbar.realtime.menu index Record]
  set stm [.mbar.realtime.menu index Stop]
  set ffm [.mbar.realtime.menu index FFwd]
  set rwm [.mbar.realtime.menu index Rewind]
  set pam [.mbar.realtime.menu index Pause]

  switch -- [expr $play * $MidiStatus] {
    0 {
	.control.play configure -state disabled
	.mbar.realtime.menu entryconfigure $plm -state disabled
      }
    1 {
        .control.play configure -state normal 
    	.mbar.realtime.menu entryconfigure $plm -state normal
	bind .control.play <Any-Leave> {tkButtonLeave %W}
      }
    2 {
        .control.play configure -state active
	.mbar.realtime.menu entryconfigure $plm -state disabled
	bind .control.play <Any-Leave> {tkButtonEnter %W; break}
      }
  }
  switch [expr $rec * $MidiStatus] {
    0 {
        .control.record configure -state disabled
        .mbar.realtime.menu entryconfigure $rcm -state disabled
      }
    1 {
        .control.record configure -state normal
    	.mbar.realtime.menu entryconfigure $rcm -state normal
	bind .control.record <Any-Leave> {tkButtonLeave %W}
      }
    2 {
        .control.record configure -state active
        .mbar.realtime.menu entryconfigure $rcm -state disabled
	bind .control.record <Any-Leave> {tkButtonEnter %W; break}
      }
  }
  switch [expr $stop * $MidiStatus] {
    0 {
        .control.stop configure -state disabled 
    	.mbar.realtime.menu entryconfigure $stm -state disabled
      }
    1 { 
        .control.stop configure -state normal
        .mbar.realtime.menu entryconfigure $stm -state normal
      }
  }
  switch [expr $pause * $MidiStatus] {
    0 {
        .control.pause configure -state disabled
        .mbar.realtime.menu entryconfigure $pam -state disabled
      }
    1 { .control.pause configure -state normal
        .mbar.realtime.menu entryconfigure $pam -state normal
        bind .control.pause <Any-Leave> {tkButtonLeave %W}
      } 
    2 { .control.pause configure -state active
        .mbar.realtime.menu entryconfigure $pam -state disabled
	bind .control.pause <Any-Leave> {tkButtonEnter %W; break}
      }
  }
  switch [expr $ffwd * $MidiStatus] {
    0 {
        .control.ffwd configure -state disabled
        .mbar.realtime.menu entryconfigure $ffm -state disabled
    }
    1 {
        .control.ffwd configure -state normal
        .mbar.realtime.menu entryconfigure $ffm -state normal
        bind .control.ffwd <Any-Leave> {tkButtonLeave %W}
      }
    2 {
        .control.ffwd configure -state active
        .mbar.realtime.menu entryconfigure $ffm -state disabled
        bind .control.ffwd <Any-Leave> {tkButtonEnter %W; break}
      }
  }
  switch [expr $rewind * $MidiStatus] {
    0 {
        .control.rewind configure -state disabled
        .mbar.realtime.menu entryconfigure $rwm -state disabled
      }
    1 {
        .control.rewind configure -state normal
        .mbar.realtime.menu entryconfigure $rwm -state normal
	bind .control.rewind <Any-Leave> {tkButtonLeave %W}
      }
    2 {
        .control.rewind configure -state active
        .mbar.realtime.menu entryconfigure $rwm -state disabled
        bind .control.rewind <Any-Leave> {tkButtonEnter %W; break}
      }
  }
}

# ---------------------------------------------------------------------
# Generate the top level window
#
proc drawMainWindow {} {
  global DDIVISION DQUANTIZE HAVE_TEX BITMAPS NUMTRACKS MTHRU
  global TEAROFF SHOWPROG SHOWCHAN DEVTAB NUMDEV HAVE_VOXWARE_GUS
  global MidiThru SmpteClk LabelNow Now MidiStatus CurDev
  global Mtracks Mformat Mdivision PlayFile PlayName GusThruPid
  global PatchList PatchFile

  set MidiStatus [hasMidi]

  frame .mbar -relief raised -bd 2
  frame .trks

  menubutton .mbar.file     -text "File " -underline 0 \
  	-menu .mbar.file.menu 
  menubutton .mbar.realtime -text "Realtime " -underline 0 \
  	-menu .mbar.realtime.menu
  menubutton .mbar.settings -text "Settings " -underline 0 \
  	-menu .mbar.settings.menu 
  menubutton .mbar.view -text "View " -underline 0 \
  	-menu .mbar.view.menu
  menubutton .mbar.track -text "Track" -underline 0 \
  	-menu .mbar.track.menu

  menu .mbar.file.menu -tearoff $TEAROFF
  .mbar.file.menu add command -label "New" -underline 0 \
    -command fileNew
  .mbar.file.menu add command -label "Open" -underline 0 \
    -command fileOpenMidi
  .mbar.file.menu add separator
  .mbar.file.menu add command -label "Save" -underline 0 \
    -command fileSaveMidi
  .mbar.file.menu add command -label "Save as" -underline 5 \
    -command fileSaveMidiAs
  .mbar.file.menu add separator
  .mbar.file.menu add command -label "Exit" -underline 1 \
    -command fileQuit 

  menu .mbar.realtime.menu -tearoff $TEAROFF
  .mbar.realtime.menu add command -label "Play" -underline 0 \
    -command seqPlay -state disabled
  .mbar.realtime.menu add command -label "Record" -underline 0 \
    -command seqRecord -state disabled
  .mbar.realtime.menu add command -label "Stop" -underline 0 \
    -command seqStop -state disabled
  .mbar.realtime.menu add separator
  .mbar.realtime.menu add command -label "FFwd" -underline 0 \
    -command seqFFwd -state disabled
  .mbar.realtime.menu add command -label "Rewind" -underline 0 \
    -command seqRewind -state disabled
  .mbar.realtime.menu add command -label "Pause" -underline 0 \
    -command seqPause -state disabled

  menu .mbar.settings.menu -tearoff $TEAROFF

  # No need to choose a device if there is only one..
  if {$NUMDEV > 1} {
    .mbar.settings.menu add cascade -label "Device" -underline 0 -menu \
      .mbar.settings.menu.device
    menu .mbar.settings.menu.device -tearoff $TEAROFF
    for {set i 0} {$i < $NUMDEV} {incr i} {
      .mbar.settings.menu.device add radiobutton -label "$DEVTAB($i,name)" \
    	-underline 9 -variable CurDev -value $i
    }
  }
  .mbar.settings.menu add cascade -label "Clock" -underline 0 -menu \
    .mbar.settings.menu.clock
  .mbar.settings.menu add command -label "Enable Thru" -underline 7 \
    -command {setMidiThru $CurDev [expr ! $MidiThru($CurDev)]}

  .mbar.settings.menu add command -label "Channel Map" -underline 10 \
    -command {getChannelMap}

  if {$HAVE_VOXWARE_GUS} {
#    .mbar.settings.menu add command  -label "UltraSound" -underline 0 \
#	-command {configUltraSound}
    .mbar.settings.menu add cascade -label "UltraSound" -underline 0 \
	-menu .mbar.settings.menu.gus

    menu .mbar.settings.menu.gus -tearoff $TEAROFF
    .mbar.settings.menu.gus add command \
    -label "Enable GUS" -underline 0 \
      -command {setGusThru [expr ! $GusThruPid]}
    .mbar.settings.menu.gus add command -label "Patch Manager" \
       -command {patchManager} -underline 0
  }

  .mbar.settings.menu add separator
  .mbar.settings.menu add command -label "Metronome" -underline 1 \
    -command {windowMetronome}
  .mbar.settings.menu add cascade -label "Division" -underline 2 -menu \
    .mbar.settings.menu.div
  .mbar.settings.menu add cascade -label "Quantization" -underline 0 -menu \
    .mbar.settings.menu.quant
  .mbar.settings.menu add cascade -label "Randomization" -underline 0 \
    -menu .mbar.settings.menu.random
  .mbar.settings.menu add separator
  .mbar.settings.menu add command -label "Key" -underline 0 \
    -command {editMap Key}
  .mbar.settings.menu add command -label "Meter" -underline 0 \
    -command {editMap Meter}
  .mbar.settings.menu add command -label "Tempo" -underline 0 \
    -command {editMap Tempo}
  .mbar.settings.menu add command -label "SMPTE" -underline 0\
    -command {setSMPTEoffset}

  menu .mbar.settings.menu.clock -tearoff $TEAROFF
  .mbar.settings.menu.clock add radiobutton -label "Kernel" \
  	-underline 0 -variable SmpteClk(0) -value 0 \
  	-command updateDeviceInfo
  .mbar.settings.menu.clock add radiobutton -label "SMPTE" \
  	-underline 0 -variable SmpteClk(0) -value 1 \
  	-command updateDeviceInfo
  .mbar.settings.menu.clock add radiobutton -label "MPU-401" \
  	-underline 0 -variable SmpteClk(0) -value 2 \
  	-command updateDeviceInfo

# if {$SMPTEDEV == ""} 
  if {0} {
    .mbar.settings.menu entryconfigure \
       [.mbar.settings.menu index SMPTE] -state disabled
    .mbar.settings.menu.clock entryconfigure \
       [.mbar.settings.menu.clock index SMPTE/MTC] -state disabled
  }

  menu .mbar.settings.menu.div -tearoff $TEAROFF
  foreach i "48 72 96 120 144 168 192 216 240 360 480" {
    .mbar.settings.menu.div add radiobutton -label $i \
  	-variable DDIVISION -command newDivision -value $i
  }

  menu .mbar.settings.menu.quant -tearoff $TEAROFF
  .mbar.settings.menu.quant add radiobutton -label "whole notes" \
  	-variable DQUANTIZE -command newQuantization -value 1
  .mbar.settings.menu.quant add radiobutton -label "1/2 notes" \
  	-variable DQUANTIZE -command newQuantization -value 2
  .mbar.settings.menu.quant add radiobutton -label "1/4 notes" \
  	-variable DQUANTIZE -command newQuantization -value 4
  .mbar.settings.menu.quant add radiobutton -label "1/8 notes" \
  	-variable DQUANTIZE -command newQuantization -value 8
  .mbar.settings.menu.quant add radiobutton -label "1/4 triplets" \
  	-variable DQUANTIZE -command newQuantization -value 12
  .mbar.settings.menu.quant add radiobutton -label "1/16 notes" \
  	-variable DQUANTIZE -command newQuantization -value 16
  .mbar.settings.menu.quant add radiobutton -label "1/8 triplets" \
  	-variable DQUANTIZE -command newQuantization -value 24
  .mbar.settings.menu.quant add radiobutton -label "1/32 notes" \
  	-variable DQUANTIZE -command newQuantization -value 32
  .mbar.settings.menu.quant add radiobutton -label "1/16 triplets" \
  	-variable DQUANTIZE -command newQuantization -value 48
  .mbar.settings.menu.quant add radiobutton -label "1/64 notes" \
  	-variable DQUANTIZE -command newQuantization -value 64
  .mbar.settings.menu.quant add radiobutton -label "1/32 triplets" \
  	-variable DQUANTIZE -command newQuantization -value 96

  menu .mbar.settings.menu.random -tearoff $TEAROFF
  .mbar.settings.menu.random add command -label "Mean" \
	  -underline 0 -command setMean
  .mbar.settings.menu.random add command -label "Variance" \
	  -underline 0 -command setVariance

  menu .mbar.view.menu -tearoff $TEAROFF
  .mbar.view.menu add checkbutton -label "Channels" \
    -variable SHOWCHAN -underline 0 \
    -command {watchCursor .; showTrackChannels {}; normalCursor .}
  .mbar.view.menu add checkbutton -label "Program Changes" \
    -variable SHOWPROG -underline 0 \
    -command {watchCursor .; showTrackPrograms {}; normalCursor .}
  .mbar.view.menu add checkbutton -label "Measure Info" \
    -variable SHOWMEAS -underline 0 \
    -command {watchCursor .; showTrackMeasures {}; normalCursor .}

#  .mbar.view.menu add separator
#  .mbar.view.menu add command -label "Refresh" -underline 0 \
#    -command {showTrackEverything {}}

  menu .mbar.track.menu -tearoff $TEAROFF -postcommand trackMenu
  .mbar.track.menu add command -label "List" -underline 0 \
    -command {track Info}
  .mbar.track.menu add command -label "Mute" -underline 1 \
    -command {track Mute}
  .mbar.track.menu add command -label "Solo" -underline 1 \
    -command {track Solo}
  .mbar.track.menu add command -label "Name" -underline 0 \
    -command {track Name}
  .mbar.track.menu add command -label "View" -underline 0 \
    -command {track PianoRoll}
  if {$HAVE_TEX} {
    .mbar.track.menu add command -label "Score" -underline 0 \
      -command {track Score}
  }
  .mbar.track.menu add separator
  .mbar.track.menu add command -label "Channel" -underline 1 \
    -command {track ForceChannel}
  .mbar.track.menu add command -label "Program" -underline 0 \
    -command {track ProgramChange}
  .mbar.track.menu add command -label "Parameter" -underline 1 \
    -command {track ParameterSet}
  .mbar.track.menu add separator
  .mbar.track.menu add command -label "Copy" -underline 0 \
    -command {track Copy}
  .mbar.track.menu add command -label "Merge" -underline 0 \
    -command {track Merge}
  .mbar.track.menu add command -label "Remove" -underline 0 \
    -command {track Remove}
  .mbar.track.menu add separator
  .mbar.track.menu add command -label "Erase" -underline 0 \
    -command {track Erase}
  .mbar.track.menu add command -label "Offset" -underline 0 \
    -command {track Offset}
  .mbar.track.menu add command -label "Quantize" -underline 0 \
    -command {track Quantize}
  .mbar.track.menu add command -label "Randomize" -underline 3 \
    -command {track Randomize}
  .mbar.track.menu add command -label "Transpose" -underline 0 \
    -command {track Transpose}
  .mbar.track.menu add command -label "Volume" -underline 0 \
    -command {track Volume}

  button .mbar.help -text Help -relief flat \
     -command {displayText .h "Help" 64 20 \
      "Here is some info to get you started.\n\n\
       Mouse behavior is hopefully consistent within tkseq.. in general\
       you can left-click on something to select it, right-click to\
       unselect it, and double-left-click to invoke an action associated\
       with it (e.g. file selection).\n\n\
       The Track menu is pretty much complete.  Select a track (or\
       several) by name and then try some of the options in the \
       Track menu.\n\n\
       Recent additions include support for channel numbering to start\
       at one instead of zero.  This is *not* supported within the\
       Track->List window, as it would be too time-consuming there I\
       think.  So be careful. ;-)\n\n\
       Do you have any other useful tips I should insert here?  Bugs,\
       comments, and groovy modifications to greg@eecs.berkeley.edu. \
       Thanks, and enjoy!"}

#    -command {dialog .h . "No help just now." info 0 OK}
#    -command {dialog .h . "[exec fortune -s]" info 0 OK}

# bitmap controls
  frame .stat -relief flat -bd 2
  frame .control -relief sunken -bd 2
  frame .control.buttons

  button .control.rewind -command seqRewind -width 1.2c \
    -bitmap "@$BITMAPS/tks_rew" -state disabled
  button .control.stop -command seqStop -width 1.2c \
    -bitmap "@$BITMAPS/tks_stop" -state disabled
  button .control.ffwd  -command seqFFwd -width 1.2c \
    -bitmap "@$BITMAPS/tks_ffwd" -state disabled
  button .control.play  -command seqPlay -width 1.2c \
    -bitmap "@$BITMAPS/tks_play" -state disabled
  button .control.record -command seqRecord -width 1.2c \
    -bitmap "@$BITMAPS/tks_rec" -state disabled
  button .control.pause -command seqPause -width 1.2c \
    -bitmap "@$BITMAPS/tks_paus" -state disabled

  frame .trknumb
  frame .trkname
  frame .trkmute
  frame .trkprog
  frame .trkchan
  frame .trkmeas 
  frame .trkscr 

  label .trknumb.subtitle -text "Trk" -relief raised -bd 1
  set subt [lindex [.trknumb.subtitle configure -foreground] 4]
  listbox .trknumb.list -foreground $subt \
    -yscrollcommand "trackScan {.trknumb.list}" \
    -width 5 -height 1 -relief flat

  label .trkname.subtitle -text "Description" -relief raised -bd 1
  listbox .trkname.list \
    -yscrollcommand "trackScan {.trkname.list}" \
    -xscrollcommand ".trkname.scroll set" \
    -width 18 -height 1 -relief sunken \
    -selectmode extended

  label .trkmute.subtitle -text "Mute" -relief raised -bd 1
  listbox .trkmute.list \
    -yscrollcommand "trackScan {.trkmute.list}" \
    -width 6 -height 1 -relief flat

  label .trkprog.subtitle -text " Pr" -relief raised -bd 1
  listbox .trkprog.list \
    -yscrollcommand "trackScan {.trkprog.list}" \
    -width 4 -height 1 -relief flat

  label .trkchan.subtitle -text "Ch" -relief raised -bd 1
  listbox .trkchan.list \
    -yscrollcommand "trackScan {.trkchan.list}" \
    -width 3 -height 1 -relief flat

  label .trkmeas.subtitle -text "Measure Info" -relief raised -bd 1
  listbox .trkmeas.list \
    -yscrollcommand "trackScan {.trkmeas.list}" \
    -xscrollcommand ".trkmeas.scroll set" -relief sunken \
    -height 2 -setgrid 1

  label .trkscr.subtitle -text " " -relief raised -bd 1

  scrollbar .trkscr.scroll -command "trackScrollUD"
  scrollbar .trkmeas.scroll -command ".trkmeas.list xview" \
    -orient horizontal
  scrollbar .trkname.scroll -command ".trkname.list xview" \
    -orient horizontal

  set basewidth [lindex [.trkname.scroll configure -width] 4]
  set bordwidth [lindex [.trkname.scroll configure -borderwidth] 4]
  set highwidth [lindex [.trkname.scroll configure -highlightthickness] 4]
  set dwidth [expr $basewidth + 2*$bordwidth + 2*$highwidth]

  frame .trkscr.dummy -width $dwidth -height $dwidth
  frame .trknumb.dummy -height $dwidth
  frame .trkmute.dummy -height $dwidth
  frame .trkprog.dummy -height $dwidth
  frame .trkchan.dummy -height $dwidth

  frame .stat.file
  frame .stat.trk
  frame .stat.fmt
  frame .stat.div
  frame .stat.now
  frame .stat.center

  label .stat.lfile -text "File:" -foreground $subt
  label .stat.vfile -textvariable PlayName -width 16
  label .stat.ltrk -text "Trk:" -foreground $subt
  label .stat.vtrk -width 3 -textvariable Mtracks 
  label .stat.lfmt -text "Fmt:" -foreground $subt
  label .stat.vfmt -textvariable Mformat
  label .stat.ldiv -text "Div:" -foreground $subt
  label .stat.vdiv -textvariable Mdivision
  label .stat.lnow -textvariable LabelNow -foreground $subt -width 8
  label .stat.vnow -textvariable Now -width 10 

  pack .stat.lfile .stat.vfile -in .stat.file -side left
  pack .stat.ltrk .stat.vtrk -in .stat.trk -side left
  pack .stat.lfmt .stat.vfmt -in .stat.fmt -side left
  pack .stat.ldiv .stat.vdiv -in .stat.div -side left
  pack .stat.vnow .stat.lnow -in .stat.now -side right

  pack .stat.trk .stat.fmt .stat.div -in .stat.center \
    -side left -expand 1 -fill x

  pack .stat.file .stat.center .stat.now \
       -in .stat -side left -padx 4m -expand 1 -fill x

  pack .mbar -side top -fill x
  pack .mbar.file .mbar.realtime .mbar.settings .mbar.view .mbar.track \
  	-side left
  pack .mbar.help -side right

  pack .trks -side top -expand 1 -fill both
  pack .stat -side bottom -fill x
  pack .control -side bottom -fill x

  pack .control.rewind .control.stop .control.ffwd \
       .control.play .control.pause .control.record \
       -in .control.buttons -side left -padx 1m
  pack .control.buttons -in .control 

  pack .trknumb.subtitle -in .trknumb -side top -fill x
  pack .trknumb.list -in .trknumb -side top -expand 1 -fill y
  pack .trknumb.dummy -in .trknumb -side bottom
  pack .trkname.subtitle -in .trkname -side top -fill x
  pack .trkname.list -in .trkname -side top -expand 1 -fill y
  pack .trkname.scroll -in .trkname -side bottom -fill x
  pack .trkmute.subtitle -in .trkmute -side top -fill x
  pack .trkmute.list -in .trkmute -side top -expand 1 -fill y
  pack .trkmute.dummy -in .trkmute -side bottom
  pack .trkprog.subtitle -in .trkprog -side top -fill x
  pack .trkprog.list -in .trkprog -side top -expand 1 -fill y
  pack .trkprog.dummy -in .trkprog -side bottom
  pack .trkchan.subtitle -in .trkchan -side top -fill x
  pack .trkchan.list -in .trkchan -side top -expand 1 -fill y
  pack .trkchan.dummy -in .trkchan -side bottom
  pack .trkmeas.subtitle -in .trkmeas -side top -fill x
  pack .trkmeas.list -in .trkmeas -side top -expand 1 -fill both
  pack .trkmeas.scroll -in .trkmeas -side bottom -fill x
  pack .trknumb .trkname .trkmute .trkprog .trkchan -in .trks \
    -side left -fill y
  pack .trkmeas -in .trks -side left -expand 1 -fill both
  pack .trkscr.subtitle -in .trkscr -side top -fill x
  pack .trkscr.scroll -in .trkscr -side top -expand 1 -fill y
  pack .trkscr.dummy -in .trkscr -side bottom
  pack .trkscr -in .trks -side right -fill y
  
  wm geometry . 40x12
  wm minsize . 18 2

  bind .mbar P seqPlay
  bind .mbar R seqRecord
  bind .mbar S seqStop
  bind .trkname.list <Double-1> { track Name }
  bind .trkname.list <Button-3> {
      selection clear .trkname.list
    }
  bind .trkmute.list <Double-1> {
      selection clear .trkname.list
      .trkname.list selection set [%W nearest %y]
      trackMuteSolo [.trkname.list curselection] 
    }
  bind .trkchan.list <Double-1> {
      selection clear .trkname.list
      .trkname.list selection set [%W nearest %y]
      trackForceChannel [.trkname.list curselection]
    }
  bind .trkprog.list <Double-1> {
      selection clear .trkname.list
      .trkname.list selection set [%W nearest %y]
      windowMappedEvent [.trkname.list curselection] Patch
    }
  bind .trkmeas.list <Double-1> {
      selection clear .trkname.list
      .trkname.list selection set [%W nearest %y]
      track PianoRoll
    }

  .trknumb.list delete 0 end
  for {set i 0} {$i < $NUMTRACKS} {incr i} {
    .trknumb.list insert end [format "%3d:" $i]
  }

  # disable real-time controls if there is no midi device
  tkwait visibility . 
  if {$MidiStatus} {
    mididevice $DEVTAB($CurDev,dev) "midithru $MidiThru($CurDev)"
  }
  updateButtons 0 1 0 0 0 0
  setMidiThru $CurDev $MTHRU

  # hack to "disable" selection in these listboxes
  foreach i {numb mute chan prog meas} { disableSelect .trk$i.list }

  # make selection a little more subtle in others
  # foreach i {mute chan prog meas} {
  #   set scol [lindex [.trk$i.list configure -background] 4]
  #   .trk$i.list configure -selectbackground $scol
  # }
}

# ---------------------------------------------------------------------
# Make the cursor into a watch when we do slow stuff
#
proc watchCursor {wlist} {
  foreach i $wlist {
    if {[lindex [$i configure -cursor] 4] != "watch"} {
      $i configure -cursor watch
      update idletasks
    }
  }
}

proc normalCursor {wlist} {
  foreach i $wlist {
    $i configure -cursor {}
  }
}

# -----------BEGIN File Menu procedures ------------------------------
#
# This module is a general-purpose file selector.
# It uses the global variables PlayName and FMASK.
# and makes calls to `dialog' for help functions
#
proc getFileName {text defname} {
  global FMASK

  # these aren't *really* global
  global GFNname waitname fdir

  set GFNname $defname

  set fdir [pwd]
  toplevel .file -cursor watch
  wm transient .file .
  set x [expr [winfo x .]+80]
  set y [expr [winfo y .]+60]
  wm geometry .file "+$x+$y"
  frame .file.t 
  frame .file.l	
  frame .file.r 
  frame .file.l.t 
  frame .file.l.b 
  frame .file.dir
  frame .file.mask
  frame .file.name
  frame .file.list
  frame .file.butt
  label .file.dir.label  -text "Directory:"
  entry .file.dir.entry -width 44 -relief sunken -bd 2 -textvariable fdir
  label .file.mask.label -text "File mask:"
  entry .file.mask.entry -width 20 -relief sunken -bd 2 -textvariable FMASK
  label .file.name.label -text "File name:"
  entry .file.name.entry -width 20 -relief sunken -bd 2 \
    -textvariable GFNname

  listbox .file.list.names -relief sunken -borderwidth 2 \
  	-yscrollcommand ".file.list.scroll set" \
  	-selectmode single
  scrollbar .file.list.scroll -command ".file.list.names yview"

  button .file.butt.ok -text OK \
    -command {if {$GFNname != ""} {set waitname $GFNname}}
  button .file.butt.delete -text Delete \
    -command {if {![catch {eval "set tt [selection get]"} msg]} {
                eval exec rm $tt;
                set GFNname "";
                listFiles
              }}
  button .file.butt.help -text Help \
    -command {dialog .h .file {Please select a filename.} info 0 OK}
  button .file.butt.cancel -text Cancel -command {set waitname ""}
  message .file.message -width 3i -text $text
  pack .file.t -side top -fill x -pady 1m
  pack .file.l -side left -fill y
  pack .file.r -side right -pady 1m
  pack .file.l.t -in .file.l -side top
  pack .file.l.b -in .file.l -side right
  pack .file.dir -in .file.t -side left -fill x -padx 1m
  pack .file.mask -in .file.l.t -side top -fill x -padx 1m -pady 2m
  pack .file.name -in .file.l.t -side top -fill x -padx 1m -pady 1m
  pack .file.butt -in .file.l.b -side right
  pack .file.message -in .file.l.b -side left -expand 1 -fill both \
    -padx 3m -pady 3m
  pack .file.list -in .file.r -side left -padx 2m -pady 1m
  pack .file.dir.label .file.dir.entry -in .file.dir \
 	-side left -padx 1m 
  pack .file.mask.label .file.mask.entry -in .file.mask \
 	-side left -padx 1m
  pack .file.name.label .file.name.entry -in .file.name \
  	-side left -padx 1m
  pack .file.list.scroll -in .file.list -side right -fill y
  pack .file.list.names -in .file.list -side left -fill x
  pack .file.butt.ok .file.butt.delete .file.butt.help \
  	.file.butt.cancel -in .file.butt \
  	-side top -fill x -padx 7m -pady 1m
  listFiles
  bind .file.name.entry <Return> {set waitname $GFNname}
  bind .file.mask.entry <Return> {listFiles}
  bind .file.dir.entry  <Return> {cd $fdir; set fdir [pwd]; listFiles}
  bind .file.list.names <Button-3> {
    selection clear  .file.list.names
    set GFNname ""
  }
  # in 4.0, local bindings fire first.. duplicate the class binding.
  bind .file.list.names <Button-1> {
    selection clear .file.list.names
    %W selection set [%W nearest %y]
    if {![file isdirectory [selection get]]} {
      set GFNname [selection get]
    }
  }
  bind .file.list.names <Double-1> { 
    %W selection set [%W nearest %y]
    if {[file isdirectory [selection get]]} { 
      cd [selection get]; set fdir [pwd]; listFiles 
    } else { 
      set GFNname [selection get]; set waitname $GFNname 
    }
  }

  normalCursor .file 
  tkwait visibility .file
  grab set .file
  focus .file.name.entry

  tkwait variable waitname
  destroy .file
  return $waitname
}

proc listFiles {} {
  global FMASK

  # clear existing data
  .file.list.names delete 0 end

  # show the parent
  if {[pwd] != "/"} {
    .file.list.names insert end ".."
  }

  # default mask of * -- I don't want to see it
  if {$FMASK == ""} {
    set realmask "*"
  } else {
    set realmask $FMASK
  }

  foreach i [lsort [glob "$realmask"]] {
    .file.list.names insert end $i
  }
}
# -------------------------------------------------------
# Clean out any existing MIDI files
#
proc fileNew {} {
  global Modified

  if {$Modified} {
    if {[dialog .h . "This will erase the\nexisting MIDI sequence!" \
       warning 1 {OK} {Cancel}] == 1} {
       return
     } 
  }
  midiCleanSlate 0
}

# -------------------------------------------------------
# Open a MIDI file
#
proc fileOpenMidi {} {
  global Modified PlayName

  if {$Modified} {
    if {[dialog .h . "This will overwrite the\nexisting MIDI sequence!" \
       warning 1 {OK} {Cancel}] == 1} {
       return
    } 
  }
  set tmpname $PlayName
  if {$tmpname == "untitled.mid"} {set tmpname ""}
  set PlayName [getFileName "Open a MIDI file" $tmpname]

  watchCursor .
  fileReadMidi
  normalCursor .
}

# -------------------------------------------------------
# Read a MIDI file
#
proc fileReadMidi {} {
  global TKS_VERSION
  global PlayFile MidiState PlayName Modified
  global HAVE_VOXWARE_GUS GUS_AUTO_LOAD PatchList PatchFile

  if {$PlayName == ""} { return }

  # if we already have a MIDI file open, blow it away
  if {$PlayFile != ""} {
    seqStop
    set tmpname $PlayName
    midiCleanSlate 0
    set PlayName $tmpname
  }
  if {[catch {eval "set ff [open $PlayName]"} msg]} {
    dialog .foops . "$msg" error 0 OK
    return
  }
  if {[catch {eval "set PlayFile [midiread $ff]"} msg]} {
    dialog .foops . "This doesn't look like a MIDI file." error 0 OK
    return
  }
  close $ff

  wm iconname . $PlayName
  wm title . "tkseq $TKS_VERSION:  $PlayName"

  buildMeterMap

  # need to update/display everything
  showTrackEverything {}
  set Modified 0

  if {$HAVE_VOXWARE_GUS && $GUS_AUTO_LOAD} {
    loadGusPatch $PatchList $PatchFile
  }

  updateButtons 1 1 0 0 0 0
}

# ------------------------------------------------------------------
# Quit
#
proc fileQuit {} {
  global NUMDEV DEVTAB HAVE_VOXWARE_GUS
  global MidiState Modified

  if {$Modified == 1} {
    if {[dialog .q . \
      "There may be unsaved work.\n           Quit anyway?" \
      warning 1 {OK} {Cancel}]} {
      return
    }
  } else {
    if {[dialog .q . "Really quit tkseq?" questhead 0 {OK} {Cancel}]} {
      return
    }
  }
  for {set i 0} {$i < $NUMDEV} {incr i} {
    midistop $DEVTAB($i,dev)
  }
 
  if {$HAVE_VOXWARE_GUS} {
    setGusThru 0
  }

  exit
}

# ------------------------------------------------------------------
# Save your work using the current filename
#
proc fileSaveMidi {} {
  global PlayFile PlayName Modified
  if {$PlayFile == ""} {
    dialog .h . "There is nothing to save." error 0 OK
    return
  }
  if {$PlayName == "" || $PlayName == "untitled.mid"} {
     fileSaveMidiAs
     return
  } else {
    writeTrackNames {}
    set ff [open $PlayName w]
    midiwrite $ff $PlayFile
    close $ff
    set Modified 0
  }
}
# ------------------------------------------------------------------
# Save your work under another name

proc fileSaveMidiAs {} {
  global PlayName Modified PlayFile
  if {$PlayFile == ""} {
    dialog .h . "There is nothing to save." error 0 OK
    return
  }
  set tmpname [getFileName "Save a MIDI file" $PlayName]
  if {$tmpname == ""} {
    return
  }

  if {[file exists $tmpname]} {
    if {[dialog .h . "File `$tmpname' exists." warning 1 Overwrite Cancel]} {
      return
    }
  }
  writeTrackNames {}
  set ff [open $tmpname w]
  set PlayName $tmpname
  midiwrite $ff $PlayFile
  close $ff
  set Modified 0
}

# -----------END File Menu procedures ------------------------------------
#
# -----------BEGIN Settings Menu procedures ------------------------------
# Most of these are handled just by cascade type menus
#
proc setVariance {} {
  global VARIANCE
  set VARIANCE [getLogScale .v . \
      "Randomization Variance" "" \
      {} $VARIANCE "helpVariance" ]
}

proc setMean {} {
  global MEAN
  set MEAN [getLinearScale .v . \
      "Randomization Mean" "" \
      {} $MEAN "helpVariance" ]
}

proc setSMPTEoffset {} {
  global SMPTEoffset

  dialog .d . \
    "You must insert MetaSMPTE\nevents manually for now." info 0 OK
}

proc editMap {type} {
  global PlayFile
  windowMappedEvent 0 $type
}

proc windowMetronome {} {
  global MeterMap MetroData PlayFile StopTime

  # some defaults should be done on the fly..
  scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk

  getStopTime $PlayFile
  if {$StopTime > 0} {
    scan [tick2measure $StopTime] "%d:" MetroData(meas,stop)
  }

  # purely cosmetic..
  set MetroData(dur,meas)  [expr $mdiv / 4]
  set MetroData(dur,beat)  [expr $mdiv / 4]

  set win .metro
  toplevel $win -cursor watch
  wm transient $win .
  set x [expr [winfo x .]+320]
  set y [expr [winfo y .]+140]
  wm geometry $win "+$x+$y"

  frame $win.t  -relief raised -bd 2
  frame $win.m1 -relief raised -bd 2
  frame $win.m2 -relief raised -bd 2
  frame $win.b  -relief raised -bd 2

  label $win.message -text "Generate a Metronome Track"
  pack $win.message -in $win.t -expand 1 -fill x -ipady 3m

  frame $win.meas
  label $win.meas.subtitle -text "Duration in Measures" -anchor w
  label $win.meas.lstart -text " From " -anchor e
  entry $win.meas.start -width 4 -relief sunken -bd 2 \
    -textvariable MetroData(meas,start)
  label $win.meas.lfinish -text " to "
  entry $win.meas.finish -width 4 -relief sunken -bd 2 \
    -textvariable MetroData(meas,stop)
  pack $win.meas.subtitle -in $win.meas -side top -fill x -pady 1m 
  pack $win.meas.lstart $win.meas.start $win.meas.lfinish $win.meas.finish \
    -in $win.meas -side left -pady 1m
  pack $win.meas -in $win.m1 -side left -padx 2m

  frame $win.event
  label $win.event.subtitle -text "Event" -anchor w
  label $win.event.meas -text " Measure " -anchor e
  label $win.event.beet -text " Beat " -anchor e
  pack $win.event.subtitle $win.event.meas $win.event.beet -in $win.event \
    -side top -padx 1m -pady 1m

  frame $win.chan
  label $win.chan.subtitle -text "Ch"
  entry $win.chan.meas -width 3 -textvariable MetroData(chan,meas)
  entry $win.chan.beet -width 3 -textvariable MetroData(chan,beat)
  pack $win.chan.subtitle $win.chan.meas $win.chan.beet -in $win.chan \
    -side top -padx 1m -pady 1m

  frame $win.patch
  label $win.patch.subtitle -text "Pr"
  entry $win.patch.meas -width 3 -textvariable MetroData(patch,meas)
  entry $win.patch.beet -width 3 -textvariable MetroData(patch,beat)
  pack $win.patch.subtitle $win.patch.meas $win.patch.beet -in $win.patch \
    -side top -padx 1m -pady 1m

  frame $win.vol
  label $win.vol.subtitle -text "Vol"
  entry $win.vol.meas -width 3 -textvariable MetroData(vol,meas)
  entry $win.vol.beet -width 3 -textvariable MetroData(vol,beat)
  pack $win.vol.subtitle $win.vol.meas $win.vol.beet -in $win.vol \
    -side top -padx 1m -pady 1m

  frame $win.dur
  label $win.dur.subtitle -text "Dur" -anchor w
  entry $win.dur.meas -width 3 -textvariable MetroData(dur,meas)
  entry $win.dur.beet -width 3 -textvariable MetroData(dur,beat)
  pack $win.dur.subtitle $win.dur.meas $win.dur.beet -in $win.dur \
    -side top -padx 1m -pady 1m

  pack $win.event $win.chan $win.patch $win.vol $win.dur -in $win.m2 \
    -side left

  pack $win.t -in $win -side top -expand 1 -fill x
  pack $win.m1 -in $win -expand 1 -fill both -ipady 2m
  pack $win.m2 -in $win -expand 1 -fill both -ipadx 2m -ipady 2m 
  pack $win.b -in $win -expand 1 -fill x


  frame $win.butt 
  pack $win.butt -in $win.b -side top -expand 1 -fill x -padx 10m

  button $win.butt.ok -text "  OK  " \
    -command "watchCursor $win; makeMetronomeTrack; destroy $win"
  button $win.butt.cancel -text "Cancel" -command "destroy $win"

  pack $win.butt.ok $win.butt.cancel -in $win.butt -side left \
    -padx 4m -pady 1m -expand 1 -fill x

  normalCursor $win
  grab set $win
  focus $win.meas.finish

  tkwait window $win
}

proc makeMetronomeTrack {} {
  global MetroData PlayFile PlayName Modified
  global CHANNEL1

  scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk
  set otrk $mtrk
  set ntrk [expr $otrk + 1]
  midiconfig $PlayFile "tracks $ntrk"

  set evmeas "Note"
  set evbeat "Note"
  foreach i {chan patch vol dur} {
    set evmeas [lappend evmeas $MetroData($i,meas)]
    set evbeat [lappend evbeat $MetroData($i,beat)]
  }

  if {$CHANNEL1} {
    set evmeas [lreplace $evmeas 1 1 [expr $MetroData(chan,meas) - 1]]
    set evbeat [lreplace $evbeat 1 1 [expr $MetroData(chan,beat) - 1]]
  }
  midiput $PlayFile $otrk {0 MetaSequenceName "<metronome>"}

  for {set i $MetroData(meas,start)} {$i <= $MetroData(meas,stop)} {incr i} {
    midiput $PlayFile $otrk "[measure2tick $i:0:0] $evmeas"
    set num [lindex [getMeter $i:0:0] 2]
    for {set j 1} {$j < $num} {incr j} {
      midiput $PlayFile $otrk "[measure2tick $i:$j:0] $evbeat"
    }
  }
  addMetaEndOfTrack $PlayFile $otrk
  if {$PlayName == "untitled.mid" && $Modified == 0} {
    showTrackEverything 0
  }
  set Modified 1
  showTrackEverything $otrk
  updateButtons 1 1 0 0 0 0
}

# ------------------------------------------------------------------------ 
#
proc helpVariance {} {
  displayText .v.h "Help" 64 12 \
    "Tkseq is capable of adding subtle random perturbations\
     to the timing of your midi files.  These perturbations have normal\
     (or Gaussian) distribution with user-adjustable mean and variance.\n\n\
     Mean\
     corresponds to a deterministic offset in SMF ticks, e.g. if\
     you want to rush the snare a little, set the mean to be a small\
     negative number and randomize the track containing the snare drum.\n\n\
     Variance is a measure of how drastic the timing changes\
     will be -- larger values lead to bigger perturbations.\n\n\
     For starters, try using mean 0.0 and variance 1.0.  That\
     will move your events\
     by only a few ticks.  For more drastic effects, or to\
     simulate my brother after 27 beers, crank it up to 10.0\
     or higher.\n\n\
     Eventually the randomization routine may be\
     modified such that perturbations are normalized with\
     respect to the variable DIVISION, but for now everything\
     is in SMF ticks.\n"
}

proc helpPatchManager {} {
  global CHANNEL1

  displayText .pman.h "Help" 64 12 \
    "Choose AutoLoad to load patches based on the current MIDI song.\n\n\
     The >> and << buttons serve to move patches into RAM and out of RAM,\
     respectively.  Simply select the patch you are interested in, and\
     use one of these buttons to get it moving.  A double-click also serves\
     to move a patch from disk to RAM.\n\n\
     Audition is useful for checking out what a patch sounds like.  It puts\
     the patch into RAM if it's not already there, then drops in a program\
     change so it shows up on channel $CHANNEL1.  You can play notes via\
     your own controller, or using the mouse and the keyboard image built\
     into the patch manager window.\n\n\
     Clear does what you'd think.. it removes all patches from RAM.\n\n\
     Click on OK when you're done, and the patch manager will go away.\n"
}

proc setMidiThru {cdev val} {
  global DEVTAB MidiThru

  set MidiThru($cdev) $val

  if {[catch {eval "set ti [.mbar.settings.menu index {Enable Thru}]"}]} {
    set ti [.mbar.settings.menu index "Disable Thru"]
  }

  if {$val} {
    .mbar.settings.menu entryconfigure $ti \
      -label "Disable Thru" -underline 8
    mididevice $DEVTAB($cdev,dev) {midithru on}
  } else {
    .mbar.settings.menu entryconfigure $ti \
      -label "Enable Thru" -underline 7
    mididevice $DEVTAB($cdev,dev) {midithru off}
  }
}

proc setGusThru {val} {
  global GusThruPid

  if {[catch {eval "set ti [.mbar.settings.menu.gus index {Enable GUS}]"}]} {
    set ti [.mbar.settings.menu.gus index "Disable GUS"]
  }

  if {$val} {
    .mbar.settings.menu.gus entryconfigure $ti \
       -label "Disable GUS" -underline 0
    if {$GusThruPid == 0} {
      set GusThruPid [exec gusthru >& /dev/null &]
    }
  } else {
    .mbar.settings.menu.gus entryconfigure $ti \
      -label "Enable GUS" -underline 0
    if {$GusThruPid} {
      exec kill $GusThruPid
      set GusThruPid 0
    }
  }
  after 250
}

proc loadGusPatch {plist pfile} {
  global PATCH_DIR
  global GusThruPid PLoadList PLoadFile

  # be optimistic ;-)
  set success 1

  if {[winfo exists .pman]} {
    watchCursor .pman
  }

  set state $GusThruPid
  setGusThru 0

  if {$plist == "reset"} {
    exec gusload reset
    set PLoadList ""
    set PLoadFile ""
    if {[winfo exists .pman]} {
      normalCursor .pman
    }
    if {$state} {
      setGusThru 1
    }
    return
  }

# Fix ME I require all patches to be in the same directory

# build a list of patches we want loaded, i.e. those that are 
# already loaded plus the new ones.
  set tmpl $PLoadList; set tmpf $PLoadFile

  for {set i 0} {$i < [llength $plist]} {incr i} {
    set pnum [lindex $plist $i]; set pfil [lindex $pfile $i]

  # a new addition
    set lin [lsearch -exact $tmpl $pnum]
    if {$lin == -1} {
      set tmpl [lsort -integer [lappend tmpl $pnum]]
      set fin [lsearch -exact $tmpl $pnum]
      set tmpf [linsert $tmpf $fin $pfil]
    } else {
  # overwrite   
      set tmpl [lreplace $tmpl $lin $lin $pnum]
      set tmpf [lreplace $tmpf $lin $lin $pfil]
    }  
  }

  # Now we have the net result.. copy it to the GUS
  exec gusload reset
  for {set i 0} {$i < [llength $tmpl]} {incr i} {
    set pnum [lindex $tmpl $i]
    set pfil [lindex $tmpf $i]
    if {[catch "exec gusload $pnum $PATCH_DIR/$pfil.pat" msg]} {
      set success 0
      if {[string first "No space left" $msg] != -1} {
        dialog .err . \
    " Sample RAM is full.  Please reset\nand try again with fewer patches." \
          error 0 OK
      } else {
        dialog .err . "Error loading patch:\n$msg" error 0 OK
      }
      break
    }
  }
  if {$success} {
    set PLoadList $tmpl
    set PLoadFile $tmpf
  }
  if {$state} {
     setGusThru 1
  }
  if {[winfo exists .pman]} {
     normalCursor .pman
  }
}

proc patchManager {} {
  global PATCH_DIR PIANOSCALE PatchList PLoadList PLoadFile RawPitch
  global KeyYtag KeyYval BlackKey WhiteKey CurDev DEVTAB SHOWMIDC

# go to the patch directory, make sure it exists
  set olddir [pwd]
  if {[catch "cd $PATCH_DIR" msg]} {
     dialog .err . "Cannot find patch directory:\n$msg" error OK
     cd $olddir
     return
  }
  set w .pman
  toplevel $w -cursor watch
  set x [expr [winfo x .]+80]
  set y [expr [winfo y .]+60]
  wm title $w "Patch Manager"
  wm geometry $w "+$x+$y"

  label $w.whitekey; label $w.blackkey; label $w.activekey
  set WhiteKey [lindex [$w.whitekey configure -background] 4]
  set BlackKey [lindex [$w.blackkey configure -background] 4]
  set ActiveKey [lindex [$w.activekey configure -background] 4]

  frame $w.t -relief raised -bd 2
  frame $w.m -relief raised -bd 2
  frame $w.t.l
  frame $w.t.r
  frame $w.l	
  frame $w.c -relief sunken -bd 2
  frame $w.r 
  frame $w.b

  set octwidth [expr  7.000000 * $PIANOSCALE]
  set xnote    [expr  5.000000 * $octwidth]
  set y0       0
  set y1w      [expr  5.5 * $PIANOSCALE]
  set y1b      [expr  3.000000 * $y1w / 5.000000]

  canvas $w.keys -width $xnote -height $y1w

  message $w.t.l.message -width 2i -text " On Disk "
  listbox $w.l.list -relief sunken -borderwidth 2 \
  	-yscrollcommand "$w.l.scroll set" -width 14 -selectmode single
  scrollbar $w.l.scroll -command "$w.l.list yview"

  message $w.t.r.message -width 2i -text " In RAM "
  listbox $w.r.list -relief sunken -borderwidth 2 \
  	-yscrollcommand "$w.r.scroll set" -width 14 -selectmode single
  scrollbar $w.r.scroll -command "$w.r.list yview"

  button $w.autoload -text "Autoload" -underline 0\
    -command { loadGusPatch $PatchList $PatchFile; patchRefreshList }
  button $w.load -text ">>" -state disabled -command {
    if {[.pman.l.list curselection] != ""} {
      loadSinglePatch [selection get]
    }}
  button $w.clear -text "<<" -state disabled -command {
    if {[.pman.r.list curselection] != ""} {
      clearSinglePatch [selection get]
      .pman.load configure -state disabled
      .pman.clear configure -state disabled
      .pman.audition configure -state disabled
    }}
  button $w.audition -text "Audition" -state disabled -underline 1\
     -command { auditionPatch }
  button $w.allclear -text "Clear" -underline 0\
    -command "loadGusPatch reset {}; patchRefreshList"
  button $w.help -text "Help" -underline 0 -command { helpPatchManager }
  button $w.ok -text "OK" -underline 0 -command "destroy $w"

  pack $w.t -side top -fill x

  pack $w.t.l -in $w.t -side left  -fill x -expand 1
  pack $w.t.r -in $w.t -side right -fill x -expand 1
  pack $w.t.l.message -in $w.t.l -side left -ipadx 8m
  pack $w.t.r.message -in $w.t.r -side right -ipadx 8m

  pack $w.b -side bottom
  pack $w.keys -side bottom -in $w.b

  pack $w.m -side top -fill both
  pack $w.l -side left  -in $w.m -fill y
  pack $w.r -side right -in $w.m -fill y
  pack $w.c -side top   -in $w.m -fill x -ipady 0.3m

  pack $w.l.scroll -in $w.l -side right -fill y
  pack $w.l.list -in $w.l -side left -fill y -expand 1

  pack $w.r.scroll -in $w.r -side right -fill y
  pack $w.r.list -in $w.r -side left -fill y -expand 1

  pack $w.autoload $w.load $w.clear $w.audition $w.allclear $w.help $w.ok \
     -in $w.c -side top -fill x -expand 1 -ipady 0.5m

  for {set k 0} {$k < 5} {incr k} {
    for {set j 0} {$j < 12} {incr j} {
      if {[lindex $KeyYtag($j) 0] == "white"} {
        set width $PIANOSCALE;
        set fill $WhiteKey; set y1 $y1w
      } else {
        set width [expr $PIANOSCALE / 2.000000]
        set fill $BlackKey; set y1 $y1b
      }
      set x0 [expr ($k + $KeyYval($j)) * $octwidth]
      set x1 [expr $x0 + $width]

      # First note is C1, i.e. 36
      $w.keys create rectangle $x0 $y0 $x1 $y1 -fill $fill \
        -outline black -tags "n[expr 12*$k+$j+36] $KeyYtag($j)"
    }
  }
  $w.keys raise black white

  if {$SHOWMIDC} {
    $w.keys itemconfigure n60 -fill $ActiveKey
  }

  if {$DEVTAB($CurDev,raw) != ""} {
    bind $w.keys <Button-1> {
      set RawPitch \
        [string range [lindex [%W gettags [%W find withtag current]] 0] 1 end]
      midisend $DEVTAB($CurDev,raw) "0 NoteOn 0 $RawPitch 100"
    }
    bind $w.keys <B1-Enter> { 
      set RawPitch \
        [string range [lindex [%W gettags [%W find withtag current]] 0] 1 end]
      midisend $DEVTAB($CurDev,raw) "0 NoteOn 0 $RawPitch 100"
    }
    bind $w.keys <ButtonRelease-1> {
      midisend $DEVTAB($CurDev,raw) "0 NoteOff 0 $RawPitch 0"
    }
    bind $w.keys <B1-Leave> {
      midisend $DEVTAB($CurDev,raw) "0 NoteOff 0 $RawPitch 0"
    }
    bind $w.keys <B1-Motion> {
      set opitch $RawPitch
      set RawPitch \
        [string range [lindex [%W gettags [%W find withtag current]] 0] 1 end]
      if {$opitch != $RawPitch} {
         midisend $DEVTAB($CurDev,raw) "50 NoteOff 0 $opitch 0"
         midisend $DEVTAB($CurDev,raw) "0 NoteOn 0 $RawPitch 100"
      }
    }
  }

  bind $w.l.list <Button-3> {
     selection clear %W
     .pman.load configure -state disabled
     .pman.clear configure -state disabled
     .pman.audition configure -state disabled
  }
  bind $w.r.list <Button-3> {
     selection clear %W
     .pman.load configure -state disabled
     .pman.clear configure -state disabled
     .pman.audition configure -state disabled
  }

#  in 4.0, local bindings fire first.. duplicate the class binding.
  bind $w.l.list <Button-1> {
    selection clear %W
    %W selection set [%W nearest %y]
    .pman.load configure -state normal
    .pman.clear configure -state disabled
    .pman.audition configure -state normal
  }

  bind $w.r.list <Button-1> {
    selection clear %W
    %W selection set [%W nearest %y]
    .pman.load configure -state disabled
    .pman.clear configure -state normal
    .pman.audition configure -state normal
  }

  bind $w.l.list <Double-1> { 
    loadSinglePatch [selection get]
  }

  $w.l.list delete 0 end
  foreach i [lsort [glob "*.pat"]] {
    $w.l.list insert end [format " %12s" $i]
  }
  cd $olddir
  patchRefreshList
  normalCursor $w 
}

proc patchRefreshList {} {
  global PLoadList PLoadFile

  if {[winfo exists .pman]} {
    if {[.pman.r.list curselection] != ""} {
      set cf [string trim [string range [selection get] 4 end]]
    } else {
      set cf ""
    }
    .pman.r.list delete 0 end

    for {set i 0} {$i < [llength $PLoadList]} {incr i} {
      set pnum [lindex $PLoadList $i]
      set pfil [lindex $PLoadFile $i]
      .pman.r.list insert end [format "%3d: %8s" $pnum $pfil]
    }

    if {$cf != ""} {
      set cp [lsearch -exact $PLoadFile $cf]
      if {$cp != -1} {
        .pman.r.list selection set $cp
      }
    }
  }
}

proc loadSinglePatch {fname} {
  global GM_PATS PLoadFile

  set pname [string range $fname 0 [expr [string first . $fname] - 1]]
  set pname [string trim $pname]
  set index [lsearch -exact $GM_PATS $pname]

  # already loaded
  if {[lsearch -exact $PLoadFile $pname] != -1} {
    return
  }

  while {$index == -1} {
    set index \
      [getEntry .pman.et .pman "Loading Non-GM Patch" {} \
      "Patch Number:" "0" 10 140 100]
    if {$index < 0 | $index > 127} {
      dialog .pman.err "Patch value out of range." error 0 OK
      set index == -1
    }
    if {[expr $index - round($index)] != 0} {
      dialog .pman.err "Expecting an integer." error 0 OK
      set index == -1
    }   
  }  
  loadGusPatch $index $pname 
  patchRefreshList
}
 
proc clearSinglePatch {fstring} {
  global PLoadList

  # Because of limitations in gusload, we do this by brute force
  set pnum [string trim [string range $fstring 0 2]]
  set index [lsearch -exact $PLoadList $pnum]
  if {$index == -1} { return }
  set tmpl [lreplace $PLoadList $index $index]
  set tmpf [lreplace $PLoadFile $index $index]

  loadGusPatch reset {}
  loadGusPatch $tmpl $tmpf
  patchRefreshList
}

proc auditionPatch {} {
  global DEVTAB CHANNEL1
  global CurDev PLoadList PLoadFile

  if {[.pman.l.list curselection] != ""} {
    set fname [selection get]
    loadSinglePatch $fname
# I hate to work this hard.. :(
    set pname \
      [string trim [string range $fname 0 [expr [string first . $fname] - 1]]]
    set index [lsearch -exact $PLoadFile $pname]
    set audition [lindex $PLoadList $index]
  } elseif {[.pman.r.list curselection] != ""} {
    set audition [string trim [string range [selection get] 0 2]]
  } else {
    return
  }

  if {$DEVTAB($CurDev,raw) != ""} {
    midisend $DEVTAB($CurDev,raw) "0 Program 0 $audition" 
  }
}
 
# -------- END Settings Menu commands -------------------------------
#
# -------- BEGIN Track Menu commands --------------------------------
#
# Process track selection and perform desired action
#
proc trackMenu {} {
  set seltrklst [.trkname.list curselection]
  if {$seltrklst == ""} {
    set state disabled
  } else {
    set state normal
  }
  for {set i 0} {$i <= [.mbar.track.menu index last]} {incr i} {
    if {[.mbar.track.menu type $i] == "command"} {
      .mbar.track.menu entryconfigure $i -state $state
    }
  }
}

proc track {Command} {
  global Modified PlayFile

  set seltrklst [.trkname.list curselection]
  if {$seltrklst == ""} {
    dialog .r . "Select a track first." error 0 OK
    return
  }

  for {set k [expr [llength $seltrklst]-1]} {$k >=0} {incr k -1} {
    # an empty track has no name.  remove it from the list
    set seltrk [lindex $seltrklst $k]
    set i [lindex $seltrklst $k]
    set tmpname [.trkname.list get $i]
    if {$tmpname == ""} {
       set seltrklst [lreplace $seltrklst $k $k]
    }
  }  
  # are there any left?
  if {$seltrklst != ""} {
    set seltrklst [lsort -integer $seltrklst]
    track$Command $seltrklst
  } else {
    dialog .r . "Those tracks are empty." error 0 OK
  }
}
# ---------------------------------------------------------------------
# Make a copy of an existing track, storing it in the next open slot
#
proc trackCopy {tlist} {
  global Modified PlayFile

  scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk
  set newlist ""
  set otrk $mtrk
  set ntrk [expr [llength $tlist]+$otrk]
  midiconfig $PlayFile "tracks $ntrk"

  watchCursor .
  for {set i $otrk} {$i < $ntrk} {incr i} {
    set k [lindex $tlist [expr $i-$otrk]]
    set newlist "$newlist $i"
    midicopy "$PlayFile $i" 0 "$PlayFile $k" 0 \
              [expr "[miditrack $PlayFile $k end] + 1"]
  }  
  normalCursor .

  showTrackEverything $newlist
  set Modified 1
}

# ---------------------------------------------------------------------
# remove all events, leaving only MetaSequenceName and MetaEndOfTrack
#
proc trackErase {tlist} {
  global Modified PlayFile

  watchCursor .
  foreach k $tlist {
    mididelete $PlayFile $k range 0 [miditrack $PlayFile $k end]
    writeTrackNames $k
    fillTrackInfo $k 0
  }  
  showTrackEverything $tlist
  normalCursor .
  set Modified 1
}

# ---------------------------------------------------------------------
# Put an event list in a pop-up window -- has some editing capabilities
#
proc trackInfo {tlist} {
  global TEAROFF

  foreach k $tlist {
    set selname [.trkname.list get $k]
    # open the info window only if it's not there already
    if {[winfo exists .ti$k]} {
      wm deiconify .ti$k
      fillTrackInfo $k 1
    } else {
      toplevel .ti$k -cursor watch
#      wm minsize .ti$k 1 1
      frame .ti$k.mbar -relief raised -bd 1
      pack .ti$k.mbar -side top -fill x
      listbox .ti$k.list -relief sunken -bd 1 \
        -yscrollcommand ".ti$k.scroll set" -width 40 -height 20 \
        -selectmode extended
      scrollbar .ti$k.scroll -command ".ti$k.list yview"
      pack .ti$k.scroll -side right -fill y
      pack .ti$k.list -side left -fill both -expand 1
      menubutton .ti$k.mbar.trk -text File -underline 0 \
        -menu .ti$k.mbar.trk.m 
      menubutton .ti$k.mbar.edit -text Edit -underline 0 \
        -menu .ti$k.mbar.edit.m 
      menubutton .ti$k.mbar.view -text View -underline 0 \
        -menu .ti$k.mbar.view.m 

      menu .ti$k.mbar.trk.m -tearoff $TEAROFF
      .ti$k.mbar.trk.m add command -label Reread -underline 0 \
        -command "remapTrackInfo $k $k"
      .ti$k.mbar.trk.m add command -label Dismiss -underline 0 \
         -command "destroy .ti$k" 
      menu .ti$k.mbar.edit.m -tearoff $TEAROFF
      .ti$k.mbar.edit.m add command -label Delete -underline 0 \
        -command "deleteEvents $k"
      .ti$k.mbar.edit.m add command -label Modify -underline 0 \
        -command "modifyEvents $k"
      .ti$k.mbar.edit.m add command -label Copy -underline 0 \
        -command "copyEvents $k"
      menu .ti$k.mbar.view.m -tearoff $TEAROFF
      .ti$k.mbar.view.m add radiobutton -label Measures -underline 0 \
        -variable MeasView -value 1 -command "remapTrackInfo $k $k"
      .ti$k.mbar.view.m add radiobutton -label Ticks -underline 0 \
        -variable MeasView -value 0 -command "remapTrackInfo $k $k"

      pack .ti$k.mbar.trk .ti$k.mbar.edit .ti$k.mbar.view -side left 
      bind .ti$k.list <Button-3> "selection clear .ti$k.t"
      wm title .ti$k "Track $k: $selname"
      wm iconname .ti$k "Track $k"
      fillTrackInfo $k 0
      normalCursor .ti$k
    }
  }
}
#
#------------------------------------------------------------------------
# Find the time of the last event in a MIDI file
#
proc getStopTime {mf} {
  global StopTime

  set eottime 0
  scan [getConfig $mf] "%d %d %d" mdiv mfmt mtrk

  for {set i 0} {$i < $mtrk} {incr i} {
    set ticks [miditrack $mf $i end]
    if {$ticks > $eottime} { set eottime $ticks }
  }
  set StopTime $eottime
}

#----------------------------------------------------------------------
# Merge several tracks.  Be careful about multiple MetaEOTs.
#
proc trackMerge {tlist} {
  global Modified PlayFile

  set ntrk [llength $tlist]
  if {$ntrk == 1} { 
    dialog .d . "That was easy." info 0 OK
    return
  }

# get configuration of existing file..
  scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk

# make scratch space
  set tmpf [midimake]
  midiconfig $tmpf "division $mdiv" "format $mfmt" "tracks 1" 
  set mlist "{$tmpf 0}"
  set dtrk [lindex $tlist 0]
  set strk [lrange $tlist 1 end]

  watchCursor .
  for {set i 0; set tname ""} {$i < $ntrk} {incr i} {
    set trk [lindex $tlist $i]
    set mlist "$mlist {$PlayFile $trk}"
    set tname [format "%s+%s" $tname [.trkname.list get $trk]]
  }
  if {[catch {eval "midimerge $mlist"} msg]} {
    # Oops.  Merge failed.
    dialog .d . "Merge failed! Mail greg@eecs.berkeley.edu" error 0 OK
    normalCursor .
    midifree $tmpf
    return
  } else {
    # put everything in the lowest position -- the merge worked
    set eottime [expr [miditrack $tmpf 0 end] + 1]
    mididelete $PlayFile $dtrk range 0 $eottime
    midicopy "$PlayFile $dtrk" 0 "$tmpf 0" 0 $eottime
    fixMetaEndOfTrack $PlayFile $dtrk

    # name it after all the merged tracks
    .trkname.list delete $dtrk
    .trkname.list insert $dtrk "[string range $tname 1 end]"
    writeTrackNames $dtrk

    midiremove $PlayFile $strk 1
    midifree $tmpf
    normalCursor .
  }

  # did one of the merged tracks have an event list up?
  foreach i $tlist {
    if {[winfo exists .ti$i]} {
      remapTrackInfo $i $dtrk
      break
    }
  }
  # shuffle the rest down 
  set h [expr $dtrk + 1]
  for {set i $h; set k $h} {$i < $mtrk} {incr i} {
    if {[lsearch -exact $tlist $i] == -1} {
      remapTrackInfo $i $k
      incr k
    } else {
      closeTrackInfo $i
    }
  }

  showTrackEverything $tlist
  set Modified 1
}

#----------------------------------------------------------------------
#
proc remapTrackInfo {old new} {
  # there may be nothing to remap
  if {![winfo exists .ti$old]} { return }

  set mapped [winfo ismapped .ti$old]
  set start [lindex [.ti$old.scroll get] 0]
  set geom [wm geometry .ti$old]

  # if the window exists with the right name, just fix it up
  if {$new == $old} {
    set selname [.trkname.list get $old]
    wm title .ti$old "Track $old: $selname"
    watchCursor .ti$old
    fillTrackInfo $old 1
    normalCursor .ti$old

  # otherwise create it from scratch
  } else {
    destroy .ti$old

  # create the new one with same state
    trackInfo $new
    wm geometry .ti$new $geom 
    .ti$new.list yview moveto $start
    if {! $mapped} { wm iconify .ti$new }
  }
}

#----------------------------------------------------------------------
# Add tracks to the mute list.  Muted tracks are stripped out 
# later, just before playing.
#
proc trackMute {tlist} {
  global MuteList PlayFile

  scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk

  # either add it if it's not there, or remove it if it is.
  foreach ltrk $tlist {
    set lpos [lsearch -exact $MuteList $ltrk]
    if {$lpos == -1} {
      if {$mfmt == 1 && $ltrk == 0} {
        dialog .muterr . "Format 1, Track 0.  Really?" error 0 Nahh.. 
        return
      } else {
        set MuteList [lsort -integer "$MuteList $ltrk"]
      }
    } else {
      set MuteList [lreplace $MuteList $lpos $lpos]
    }
  }
  showTrackMuteList
}

proc trackSolo {trk} {
  global SoloList PlayFile

  scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk

  if {[llength $trk] != 1} {
    dialog .solo . "Ummm.. which one should I solo?" questhead 0 Nevermind
    return
  }

  if {$mfmt == 1 && $trk == 0} {
    dialog .muterr . "Format 1, Track 0.  Really?" error 0 Nahh..
    return
  }

  # either remove it if it is solo'd, or force it if it's not.
  set foo $SoloList
  if {$foo == ""} {
    set SoloList $trk
  } elseif {$foo != $trk} {
    set SoloList $trk
  } else {
    set SoloList ""
  }
  showTrackMuteList
}

# this only works for a single track -- bound to a mouse click
proc trackMuteSolo {trk} {
  global MuteList SoloList PlayFile

  scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk

  if {$mfmt == 1 && $trk == 0} {
    dialog .muterr . "Format 1, Track 0.  Really?" error 0 Nahh..
    return
  }

  # Solo -> Mute -> Play -> Solo, etc..
  set mutepos [lsearch -exact $MuteList $trk]

  # case 1: Solo->Mute
  if {$SoloList == $trk} {
    set SoloList "" 
    if {$mutepos == -1} {
      set MuteList [lsort -integer "$MuteList $trk"]
    }

  # case 2: Mute->Play
  } elseif {$mutepos != -1} {
    set MuteList [lreplace $MuteList $mutepos $mutepos]

  # case 3: Play->Solo
  } else {
    set SoloList $trk
  }

  showTrackMuteList
}
# -------------------------------------------------------------------
#
proc trackForceChannel {tlist} {
  global CHANNEL1 SHOWCHAN
  global PlayFile

  if {$PlayFile == ""} { return } 

  set lost 0
  set tlist [fixTrackList $tlist]
  scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk

  foreach i $tlist {
    set ochan [.trkchan.list get $i]
    if {$ochan == "--"} {
      dialog .d . "Track $i has no channel\nrelated messages." info 0 OK
      return
    }
    if {$ochan == "**"} {
      set ochan 99
    } else {
      set ochan [expr $ochan]
    }

    set nchan [getChannel $i $ochan]
    if {$nchan == 99} {return}

    set nchan [expr $nchan - $CHANNEL1]

    # now edit every event
    watchCursor .
    set tmpf [midimake]
    midiconfig $tmpf "division $mdiv" "tracks 1" "format 0"
    midirewind $PlayFile $i
    while {[set event [midiget $PlayFile $i next]] != "EOT"} {
      set etype [string range [lindex $event 1] 0 3]
      if {$etype != "Meta" && $etype != "Syst"} {
        set event [lreplace $event 2 2 $nchan]
      }
      if {[catch {midiput $tmpf 0 "$event"} msg]} {
        dialog .lost . "Unable to put event:\n $event" info 0 {OK}
        incr lost
      }
    }
    set eottime [expr [miditrack $PlayFile $i end] + 1]
    mididelete $PlayFile $i range 0 $eottime
    midicopy "$PlayFile $i" 0 "$tmpf 0" 0 $eottime
    midifree $tmpf

    remapTrackInfo $i $i

#    dialog .lost . "$lost events were lost\nas duplicates." info 0 OK
    normalCursor .
  }
  if {$SHOWCHAN} { showTrackChannels $tlist }
  set Modified 1
}

proc trackProgramChange {tlist} {
  windowMappedEvent $tlist Patch
}

proc trackParameterSet {tlist} {
  windowMappedEvent $tlist Parameter
}

#
#-----------------------------------------------------------------

proc loadMappedEventData {} {
  global Evnt Elist

  set Elist "Patch Parameter Tempo Meter Key"

#  Time Program Channel Patch
  set Evnt(Patch,N) 4
  set Evnt(Patch,T) 1
  set Evnt(Patch,H) "Program Changes for Track"
  set Evnt(Patch,0) Time
  set Evnt(Patch,1) Program
  set Evnt(Patch,2) Channel
  set Evnt(Patch,3) Patch

#  Time Parameter Channel Name Value
  set Evnt(Parameter,N) 5
  set Evnt(Parameter,T) 1
  set Evnt(Parameter,H) "Parameter Changes for Track"
  set Evnt(Parameter,0) Time
  set Evnt(Parameter,1) Parameter
  set Evnt(Parameter,2) Channel
  set Evnt(Parameter,3) Parameter
  set Evnt(Parameter,4) Value

#  Time MetaTempo Tempo
  set Evnt(Tempo,N) 3
  set Evnt(Tempo,T) 0
  set Evnt(Tempo,H) "Tempo Changes"
  set Evnt(Tempo,0) Time
  set Evnt(Tempo,1) MetaTempo
  set Evnt(Tempo,2) Tempo

#  Time MetaTime Num Den Clocks 32nds
  set Evnt(Meter,N) 6
  set Evnt(Meter,T) 0
  set Evnt(Meter,H) "Time Signature Changes"
  set Evnt(Meter,0) Measure
  set Evnt(Meter,1) MetaTime
  set Evnt(Meter,2) Numerator
  set Evnt(Meter,3) Denominator
  set Evnt(Meter,4) Clocks
  set Evnt(Meter,5) 32nds

#  Time MetaKey Pitch Maj/Min
  set Evnt(Key,N) 4
  set Evnt(Key,T) 0
  set Evnt(Key,H) "Key Signature Changes"
  set Evnt(Key,0) Measure
  set Evnt(Key,1) MetaKey
  set Evnt(Key,2) Key
  set Evnt(Key,3) Maj/Min
}

proc windowMappedEvent {tlist ix} {
  global SHOWPROG
  global Gmf Gtrk Gdat Evnt PlayFile PlayName Modified

  if {$PlayFile == ""} { 
    return
  } else {
    set Gmf($ix) $PlayFile
  }

  set win .[string tolower $ix]
  set tlist [fixTrackList $tlist]

  set track $Evnt($ix,T)
  set head  $Evnt($ix,H)
  if {$Evnt($ix,2) == "Channel"} { 
    set channel 1 
  } else {
    set channel 0
  }

  foreach trk $tlist {
    set Gtrk($ix) $trk
    if {$track} { set head "$Evnt($ix,H) $trk" }

    toplevel $win -cursor watch
    wm transient $win .
    set x [expr [winfo x .]+320]
    set y [expr [winfo y .]+140]
    wm geometry $win "+$x+$y"

    frame $win.t -relief raised -bd 2
    frame $win.b -relief raised -bd 2
    frame $win.l
    frame $win.r 
    frame $win.l.t 
    frame $win.l.b 

    for {set i 0} {$i < $Evnt($ix,N)} {incr i} { 
      if {$i != 1} {
        set Gdat($ix,$i) ""
        frame $win.dat$i
        label $win.dat$i.label -width 12 -text "$Evnt($ix,$i):" -anchor e
        entry $win.dat$i.entry -width 9 -relief sunken \
          -bd 2 -textvariable Gdat($ix,$i)
      }
    }

    frame $win.list
    frame $win.butt

    listbox $win.list.names -relief sunken -borderwidth 2 \
    	-yscrollcommand "$win.list.scroll set" \
    	-width 32 -selectmode single
    scrollbar $win.list.scroll -command "$win.list.names yview"

    button $win.butt.modify -text Apply -state disabled \
      -command "modifyMappedEvent $ix"
    button $win.butt.add -text Add -command "addMappedEvent $ix 0"
    button $win.butt.remove -text Remove -state disabled \
      -command "removeMappedEvent $ix"
    button $win.butt.ok -text OK -command "destroy $win"

    message $win.message -width 3.5i -text "$head"

    pack $win.t -side top -fill x
    pack $win.b -side bottom -fill y
    pack $win.l -in $win.b -side left -fill y
    pack $win.r -in $win.b -side right -fill y
    pack $win.l.t -in $win.l -side top -fill x
    pack $win.l.b -in $win.l -side right -fill x

    pack $win.butt -in $win.l.b -side right
    pack $win.message -in $win.t -fill x \
      -ipadx 1m -ipady 1m
    pack $win.list -in $win.r -side left -fill y

    for {set i 0} {$i < $Evnt($ix,N)} {incr i} {
      if {$i != 1} {
        pack $win.dat$i -in $win.l.t -side top -fill x -padx 0.5m
        pack $win.dat$i.label $win.dat$i.entry -in $win.dat$i \
	  -side left -padx 1m 
      }
    }

    pack $win.list.scroll -in $win.list -side right -fill y
    pack $win.list.names -in $win.list -side left -fill y
    pack $win.butt.modify $win.butt.add $win.butt.remove \
    	$win.butt.ok -in $win.butt \
    	-side top -fill both -pady 0.5m -padx 1m -expand y

    bind $win.list.names <Button-1> "fillMappedEvent $ix %y"
    bind $win.list.names <Button-3> "clearMappedEvent $ix"

    listMappedEvent $ix

    tkwait visibility $win

    if {$ix == "Key"} {
      windowKeyboardMap $win
    }

    normalCursor $win
    grab set $win
    focus $win.dat0.entry

    tkwait window $win

    if {$ix == "Patch"} {
      if {$SHOWPROG} { showTrackPrograms $trk }
    }
    remapTrackInfo $trk $trk
    showTrackEverything $trk
  }
}

proc windowKeyboardMap {win} {
  global DEVTAB CurDev
  global Gdat GetFlat TagList SelKey KeyYtag KeyYval 
  global WhiteKey BlackKey ActiveKey HotKey HotList
  global RawPitch

  set GetFlat 0
  set Gdat(Key,2) ""
  set Gdat(Key,3) ""
  set TagList ""
  set SelKey ""

  set w $win.board
  toplevel $w -class Dialog
  wm transient $w $win
  set x [expr [winfo x $win]-220]
  set y [expr [winfo y $win]+110]
  wm geometry $w "+$x+$y"

  # these are used to grab resources.. that's all.
  label $w.whitekey; label $w.blackkey; label $w.activekey
  set WhiteKey [lindex [$w.whitekey configure -background] 4]
  set BlackKey [lindex [$w.blackkey configure -background] 4]
  set ActiveKey [lindex [$w.activekey configure -background] 4]

  frame $w.l -relief raised -bd 2
  frame $w.r -relief raised -bd 2
  canvas $w.keys -width 7c -height 5c
  radiobutton $w.maj -text Major -variable Gdat(Key,3) -value "major" \
    -command "keyToggle" -relief raised -bd 2
  radiobutton $w.min -text Minor -variable Gdat(Key,3) -value "minor" \
    -command "keyToggle" -relief raised -bd 2
  button $w.flat -text "#/b" -state disabled \
    -command "incr GetFlat; keyToggle"

  pack $w.l $w.r -in $w -side left -padx 1m -pady 1m
  pack $w.keys -in $w.l -side left 

  pack $w.maj $w.min -in $w.r -side top -fill x -ipadx 2m -ipady 1m
  pack $w.flat -in $w.r -side top -fill x

  set pscale 30

  for {set j 0} {$j < 12} {incr j} {
    set keytype [lindex $KeyYtag($j) 0]
    if {$keytype == "white"} {
      set fill $WhiteKey
      set width $pscale; set y1 [expr 5.5 * $pscale]
    } else {
      set fill $BlackKey
      set width [expr $pscale / 2.000000]
      set y1 [expr 5.5 * $pscale * 0.6]
    }
    set y0 0
    set x0 [expr $KeyYval($j) * 7.000000 * $pscale]
    set x1 [expr $x0 + $width]

    $w.keys create rectangle $x0 $y0 $x1 $y1 -fill $fill \
      -outline $BlackKey -tags "n[expr $j+60] $KeyYtag($j)"
  }
  $w.keys raise black white

  bind $w.keys <Button-1> { 
    if {$SelKey != ""} {
      set keytype [lindex $TagList 1]
      if {$keytype == "white"} {
        %W itemconfigure $SelKey -fill $WhiteKey
      } else {
        %W itemconfigure $SelKey -fill $BlackKey
      }
    }
    set SelKey [%W find withtag current]
    %W itemconfigure $SelKey -fill $ActiveKey
    set TagList [%W gettags $SelKey]

    if {$DEVTAB($CurDev,raw) != ""} {
      set RawPitch [string range [lindex $TagList 0] 1 end]
      midisend $DEVTAB($CurDev,raw) "0 NoteOn 0 $RawPitch 100"
    }
    set GetFlat 0;
    keyToggle
  } 

  if {$DEVTAB($CurDev,raw) != ""} {
    bind $w.keys <ButtonRelease-1> {
      midisend $DEVTAB($CurDev,raw) "0 NoteOff 0 $RawPitch 0"
    }
    bind $w.keys <B1-Leave> {
      midisend $DEVTAB($CurDev,raw) "0 NoteOff 0 $RawPitch 0"
    }
  }
}

proc keyToggle {} {
  global Gdat GetFlat TagList

  if {$TagList == "" } {return}

  set GetFlat [expr $GetFlat % 2]
  if {[lindex $TagList 1] == "black"} {
    set foo [expr 2 + $GetFlat]
    .key.board.flat configure -state normal
  } else {
    set foo 2
    .key.board.flat configure -state disabled
  }
  set Gdat(Key,2) "[lindex $TagList $foo]"
}

#------------------------------------------------------------------
#
# These are general

proc listMappedEvent {type} {
  global CHANNEL1 Gmf Gtrk Evnt

  set win .[string tolower $type]
  set mf $Gmf($type); set trk $Gtrk($type)

  $win.list.names delete 0 end
  midirewind $mf $trk
  while {[set event [midiget $mf $trk next]] != "EOT"} {
    if {[lindex $event 1] == $Evnt($type,1)} {
      set ticks [lindex $event 0]
      set event [lreplace $event 0 0 [tick2measure $ticks]]
      if {$Evnt($type,2) == "Channel"} {
        set chan [lindex $event 2]
        set event [lreplace $event 2 2 [expr $chan + $CHANNEL1]]
      }
      $win.list.names insert end $event
    }
  }
}

proc removeMappedEvent {type} {
  set win .[string tolower $type]
  if { "[$win.list.names curselection]" != "" } {
    delMappedEvent $type [selection get]; clearMappedEvent $type
  }
}

proc modifyMappedEvent {type} {
  set win .[string tolower $type]
  if { "[$win.list.names curselection]" != "" } { addMappedEvent $type 1 }
}

proc clearMappedEvent {type} {
  global Evnt Gdat
  set win .[string tolower $type]
  selection clear $win.list.names
  $win.butt.modify configure -state disabled
  $win.butt.remove configure -state disabled
  for {set i 0} {$i < $Evnt($type,N)} {incr i} {set Gdat($type,$i) ""}
}

proc fillMappedEvent {type yval} {
  global Evnt Gdat

  # Hack to duplicate class binding
  set win .[string tolower $type]
  $win.list.names selection set [$win.list.names nearest $yval]

  set event [selection get]
  $win.butt.modify configure -state normal
  $win.butt.remove configure -state normal
  for {set i 0} {$i < $Evnt($type,N)} {incr i} {
    set Gdat($type,$i) [lindex $event $i]
  }
  if {$Evnt($type,0) == "Measure"} {
    scan [lindex $event 0] "%d:" Gdat($type,0)
  }
}

proc addMappedEvent {type old} {
  global CHANNEL1
  global Gmf Gtrk Gdat Evnt Modified
  set win .[string tolower $type]

  set Gdat($type,1) Evnt($type,1)

  set mf $Gmf($type)
  set trk $Gtrk($type)

  for {set i 0} {$i < $Evnt($type,N)} {incr i} {
    if {$Gdat($type,$i) == ""} { 
      dialog $win.err $win "Fill in all the blanks!" error 0 OK
      return 1
    }
  }

  if {$Evnt($type,0) == "Measure"} {
    scan $Gdat($type,0) "%d:" ttmp
    set ttmp "$ttmp:0:0"
  } else {
    set ttmp $Gdat($type,0)
  }

  set foobar [catch "set rtime [measure2tick $ttmp]"]
  if {$foobar} {
    dialog $win.pt $win "Invalid time specification.  Ex: 0:00:000" \
      error 0 OK
    return 1
  }

  set event ""
  if {$Evnt($type,2) == "Channel"} {
    set foobar [catch "set rchan [expr $Gdat($type,2) - $CHANNEL1]"]
    if {$foobar || $rchan < 0 || $rchan > 15} {
      dialog $win.pt $win "Invalid channel specification." error 0 OK
      return 1
    }
    set event [lappend event $rtime $Evnt($type,1) $rchan]; set i 3
  } else {
    set event [lappend event $rtime $Evnt($type,1)]; set i 2
  }

  for {} {$i < $Evnt($type,N)} {incr i} {
    set event [lappend event "$Gdat($type,$i)"] 
  }

  # remove the old event
  if {$old == 1} { delMappedEvent $type [selection get] }

  midiput $mf $trk $event
  clearMappedEvent $type; listMappedEvent $type

# make sure we didn't fuck up the MetaEndOfTrack..
  fixMetaEndOfTrack $mf $trk

  set Modified 1
  if {$type == "Meter"} { buildMeterMap }

  return 0
}

proc delMappedEvent {type event} {
  global CHANNEL1
  global Gmf Gtrk Evnt Modified

  set meas [lindex $event 0]
  set event [lreplace $event 0 0 [measure2tick $meas]]
  if {$Evnt($type,2) == "Channel"} {
    set chan [lindex $event 2]
    set event [lreplace $event 2 2 [expr $chan - $CHANNEL1]]
  }
  mididelete $Gmf($type) $Gtrk($type) $event
  listMappedEvent $type

  set Modified 1
}

# -------------------------------------------------------------------
#
proc fixTrackList {tlist} {
  global PlayFile

  scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk

  set newlist ""
  foreach trk $tlist {
    if {$trk >= 0 && $trk < $mtrk} {
      set newlist [lappend newlist $trk]
    }
  }

  return $newlist
}

# -------------------------------------------------------------------
#
proc trackName {tlist} {
  # hack to make the window pop up near the tracks we are naming
  set foo [lindex [.trkscr.scroll get] 0]
  set foobar [expr round(([lindex $tlist 0]-$foo)*6)]
  foreach trk $tlist {
    set tmpname \
      [getEntry .et . "New Track Name" {} "Track $trk:" \
      "[.trkname.list get $trk]" 18 140 $foobar]
    .trkname.list delete $trk
    .trkname.list insert $trk $tmpname
    writeTrackNames $trk
    remapTrackInfo $trk $trk
  } 
  set Modified 1
}
# -------------------------------------------------------------
#
proc trackQuantize {tlist} {
  global DQUANTIZE
  global Modified PlayFile

  watchCursor .
  set lost [midiquantize $PlayFile $tlist $DQUANTIZE]
  set Modified 1
  normalCursor .
  if {$lost != 0} {
    dialog .d . "quantize: $lost events lost\nas duplicates." info 0 OK 
  }
  foreach trk $tlist {
    remapTrackInfo $trk $trk
  }
}     

# --------------------------------------------------------------
# Randomize the timing of a group of tracks
#
proc trackRandomize {tlist} {
  global MEAN VARIANCE
  global Modified PlayFile
  watchCursor .
  set lost [midirandomize $PlayFile $tlist $MEAN $VARIANCE]
  set Modified 1
  normalCursor .
  if {$lost != 0} {
    dialog .d . "randomize: $lost events lost\nas duplicates." \
      info 0 OK
  }
  foreach trk $tlist {
    remapTrackInfo $trk $trk
  }
}

# -------------------------------------------------------------
#
proc trackOffset {tlist} {
  global Modified PlayFile
  global offset
  
  set lost 0
  set offset \
    [getEntry .et . "Timing offset" {} "in SMF ticks:" \
    "0" 10 140 100]
  watchCursor .
  midioffset $PlayFile $tlist [measure2tick $offset]
  normalCursor .
  set Modified 1
  if {$lost != 0} {
    dialog .d . "offset: $lost events lost\nas duplicates." \
      info 0 OK
  }
  foreach trk $tlist {
    remapTrackInfo $trk $trk
  }
}

# -------------------------------------------------------------
#
proc trackTranspose {tlist} {
  global Modified PlayFile

  # this isn't *really* global
  global halfsteps

  set lost 0
  set halfsteps \
    [getEntry .et . "Transpose up" {} "in semitones:" \
    "0" 4 140 100]
  watchCursor .
  miditranspose $PlayFile $tlist $halfsteps
  normalCursor .
  set Modified 1
  if {$lost != 0} {
    dialog .d . "transpose: $lost events lost\nas duplicates." \
      info 0 OK
  }
  foreach trk $tlist {
    remapTrackInfo $trk $trk
  }
}

proc trackVolume {tlist} {
  global Modified PlayFile

  # this isn't *really* global
  global scaleby

  set lost 0
  set scaleby \
    [getEntry .et . "Volume adjust" {} "scale by:" \
    "1.0" 4 140 100]
  watchCursor .
  midivolume $PlayFile $tlist $scaleby
  normalCursor .
  set Modified 1
  if {$lost != 0} {
    dialog .d . "volume: $lost events lost\nas duplicates." \
      info 0 OK
  }
  foreach trk $tlist {
    remapTrackInfo $trk $trk
  }
}

# -------------------------------------------------------------
# Completely remove selected tracks, shuffling the others down
# into the open slot
#
proc trackRemove {tlist} {
  global Modified PlayFile

  scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk
  watchCursor .
  midiremove $PlayFile $tlist 1
  normalCursor .

  if {$mtrk == [llength $tlist]} {
    midiCleanSlate 0
  } else {
    # shuffle the rest down 
    set h [lindex $tlist 0]
    for {set i $h; set k $h} {$i < $mtrk} {incr i} {
      if {[lsearch -exact $tlist $i] == -1} {
        remapTrackInfo $i $k
        incr k
      } else {
        closeTrackInfo $i
      }
    }
    showTrackEverything {}
  }
  set Modified 1
}

# ---------------------------------------------------------------------
# An attempt at scoring -- not for the faint of heart.
# For this to work you will need midi2tex, musictex, TeX, and xdvi,
# not to mention a great deal of luck and patience.
# 
# Software can be found at any ctan site, e.g. ftp.cdrom.com, 
# /pub/tex/ctan/macros/musictex/software/midi2tex
#
proc trackScore {tlist} {
  global PlayFile

  # make sure we have track zero in there..
  if {[lsearch -exact $tlist 0] == -1} {
    set tlist "0 $tlist"
  }
  # now we have to invert the list, so we keep instead of remove
  scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk

  for {set i 0; set klist ""} {$i < $mtrk} {incr i} {
    if {[lsearch -exact $tlist $i] == -1} {
      set klist "$klist $i"
    }
  }
  # generate a copy of the selected tracks
  set tmpf [midiremove $PlayFile $klist 0]
  set tt scoretmp 
  set scorecommand \
    [list "midi2tex $tt.mid" "tex $tt.TEX" "xdvi $tt"]
  set ff [open $tt.mid w]
  watchCursor .
  midiwrite $ff $tmpf
  normalCursor .
  close $ff
  midifree $tmpf
  shellCmd midi2tex 72 15 "$scorecommand"
}

# -----------BEGIN PianoRoll subroutines -----------------------
#

proc pianoRollScroll {w a b} {
  $w.note yview $a $b
  $w.keys yview $a $b
}

proc trackPianoRoll {tlist} {
  global TEAROFF PIANOSCALE SHOWMIDC DEVTAB CHANNEL1
  global SHOWBARS SHOWBEAT SHOWQUAN DQUANTIZE
  global TimeScale KeyYval KeyYtag CurDev
  global ShowBars ShowBeat ShowQuan ShowMidC PlayFile
  global WhiteKey BlackKey ActiveKey 

  watchCursor .

  scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk

  foreach trk $tlist {
    set w .pr$trk
    set ShowBars($trk) $SHOWBARS
    set ShowBeat($trk) $SHOWBEAT
    set ShowQuan($trk) $SHOWQUAN
    set ShowMidC($trk) $SHOWMIDC

    set octwidth [expr  7.000000 * $PIANOSCALE]
    set lastnote [miditrack $PlayFile $trk end]
    set xnote    [expr  ($lastnote + 1) * $TimeScale / $mdiv]
    if {$xnote < 508} { set xnote 508 }

    scan [tick2measure $lastnote] "%d:%d:%d" lastmeas bb tt
    if {$bb != 0 || $tt != 0} { incr lastmeas }

    set ynote    [expr  8.000000 * $octwidth]
    set x1       [expr  5.500000 * $PIANOSCALE]
    set x0b      [expr  2.000000 * $x1 / 5.000000]

    if {[winfo exists $w]} { destroy $w }
    toplevel $w -cursor watch
    wm title $w "Piano Roll: Track $trk"
    wm iconname $w "Track $trk"
    wm minsize $w 0 0
    wm maxsize $w 1600 1280
    wm geometry $w 600x400

    frame $w.r -relief raised -bd 2
    frame $w.l -relief raised -bd 2
    frame $w.c -relief sunken -bd 2
    frame $w.mbar -relief raised -bd 1

    canvas $w.keys -width $x1 -height $ynote \
      -scrollregion "0 0 $x1 $ynote" \
      -yscrollcommand "$w.r.scroll set"
    canvas $w.note -width $xnote -height $ynote \
      -scrollregion "0 0 $xnote $ynote" \
      -xscrollcommand "$w.c.scroll set" \
      -yscrollcommand "$w.r.scroll set"
    scrollbar $w.c.scroll -command "$w.note xview" -orient horizontal
    scrollbar $w.r.scroll -command "pianoRollScroll $w"

    # these are used to grab resources.. that's all.
    label $w.whitekey; label $w.blackkey; label $w.activekey
    set WhiteKey [lindex [$w.whitekey configure -background] 4]
    set BlackKey [lindex [$w.blackkey configure -background] 4]
    set ActiveKey [lindex [$w.activekey configure -background] 4]

    label $w.quantum; label $w.beat; label $w.measure
    set quantcolor [lindex [$w.quantum configure -foreground] 4]
    set beatcolor  [lindex [$w.beat configure -foreground] 4]
    set meascolor  [lindex [$w.measure configure -foreground] 4]

    label $w.grid; label $w.event;
    set backcolor  [lindex [$w.note configure -background] 4]
    set eventcolor [lindex [$w.event configure -foreground] 4]
    set gridcolor  [lindex [$w.grid configure -foreground] 4]

    pack  $w.mbar -side top -expand 1 -fill x
    menubutton $w.mbar.file -text File -underline 0 \
      -menu $w.mbar.file.m 
    menu $w.mbar.file.m -tearoff $TEAROFF
    $w.mbar.file.m add command -label Dismiss -underline 0 \
      -command "closePianoRoll $trk" 

    menubutton $w.mbar.view -text "View" -underline 0 \
  	-menu $w.mbar.view.menu
    menubutton $w.mbar.zoom -text "Zoom" -underline 0 \
  	-menu $w.mbar.zoom.menu

    menu $w.mbar.view.menu -tearoff $TEAROFF
    $w.mbar.view.menu add checkbutton -label "Middle C" -underline 7 \
      -variable ShowMidC($trk) -command "showMiddleC $trk" \
      -selectcolor $ActiveKey
    $w.mbar.view.menu add checkbutton -label "Measures" -underline 0 \
      -variable ShowBars($trk) -command "showMeasures $trk" \
      -selectcolor $meascolor
    $w.mbar.view.menu add checkbutton -label "Beats" -underline 0 \
      -variable ShowBeat($trk) -command "showBeats $trk" \
      -selectcolor $beatcolor
    $w.mbar.view.menu add checkbutton -label "Quantization" -underline 0 \
      -variable ShowQuan($trk) -command "showQuantization $trk" \
      -selectcolor $quantcolor

    menu $w.mbar.zoom.menu -tearoff $TEAROFF
    $w.mbar.zoom.menu add command -label "In    " -underline 0 \
      -command "pianoRollZoom $trk 2.000000"
    $w.mbar.zoom.menu add command -label "Out   " -underline 0 \
      -command "pianoRollZoom $trk 0.500000"

    pack $w.mbar.file $w.mbar.view $w.mbar.zoom -in $w.mbar -side left 

    set basewidth [lindex [$w.c.scroll configure -width] 4]
    set bordwidth [lindex [$w.c.scroll configure -borderwidth] 4]
    set highwidth [lindex [$w.c.scroll configure -highlightthickness] 4]
    set dwidth [expr $basewidth + 2*$bordwidth + 2*$highwidth]
    frame $w.dummy0 -width $dwidth -height $dwidth
    frame $w.dummy1 -height $dwidth

    pack $w.dummy0 -in $w.r -side bottom
    pack $w.r.scroll -in $w.r -side top -expand 1 -fill y
    pack $w.r -in $w -side right -expand 1 -fill both

    pack $w.dummy1 -in $w.l -side bottom
    pack $w.keys -in $w.l -side top -expand 1 -fill y
    pack $w.l -in $w -side left -expand 1 -fill both

    pack $w.c.scroll -in $w.c -side bottom -expand 1 -fill x
    pack $w.note -in $w.c -side top -expand 1 -fill both 
    pack $w.c -in $w -side top -expand 1 -fill both

    for {set k 0} {$k < 8} {incr k} {
      for {set j 0} {$j < 12} {incr j} {
        if {[lindex $KeyYtag($j) 0] == "white"} {
          set width $PIANOSCALE; set fill $WhiteKey; set x0 0
        } else {
          set width [expr $PIANOSCALE / 2.000000]
          set fill $BlackKey; set x0 $x0b
        }
        set y0 [expr ($k + $KeyYval($j)) * $octwidth]
        set y1 [expr $y0 + $width]

        # First note is C0 or 24
        $w.keys create rectangle $x0 $y0 $x1 $y1 -fill $fill \
          -outline black -tags "n[expr 12*$k+$j+24] $KeyYtag($j)"

        set yrule [expr $octwidth * (($k * 12.00000) + $j) / 12.000000]
        $w.note create line 0 $yrule $xnote $yrule \
          -fill $gridcolor -tags "zoom grid bm bb bq"
      }
    }
    $w.keys raise black white

    if {$DEVTAB($CurDev,raw) != ""} {
      bind $w.keys <Button-1> { 
        set pitch [string range \
          [lindex [%W gettags [%W find withtag current]] 0] 1 end]
        set track   [string range %W 3 [expr [string first .k %W] - 1]]
        set channel [string range [.trkchan.list get $track] 0 1]
        if {$channel == "--"} {
          set channel $CHANNEL1
        }
        set channel [expr $channel - $CHANNEL1]
        midisend $DEVTAB($CurDev,raw) "0 NoteOn $channel $pitch 100"
        set NoteOff "0 NoteOff $channel $pitch 0"
      }
      bind $w.keys <B1-Enter> { 
        set pitch [string range \
          [lindex [%W gettags [%W find withtag current]] 0] 1 end]
        set track   [string range %W 3 [expr [string first .k %W] - 1]]
        set channel [string range [.trkchan.list get $track] 0 1]
        if {$channel == "--"} {
          set channel $CHANNEL1
        }
        set channel [expr $channel - $CHANNEL1]
        midisend $DEVTAB($CurDev,raw) "0 NoteOn $channel $pitch 100"
        set NoteOff "0 NoteOff $channel $pitch 0"
      }
      bind $w.keys <ButtonRelease-1> {
        midisend $DEVTAB($CurDev,raw) $NoteOff
      }
      bind $w.keys <B1-Leave> {
        midisend $DEVTAB($CurDev,raw) $NoteOff
      }
      bind $w.keys <B1-Motion> {
        set opitch [lindex $NoteOff 3]
        set pitch [string range \
          [lindex [%W gettags [%W find withtag current]] 0] 1 end]
        if {$opitch != $pitch} {
           set channel [lindex $NoteOff 2]
           midisend $DEVTAB($CurDev,raw) $NoteOff
           midisend $DEVTAB($CurDev,raw) "0 NoteOn $channel $pitch 100"
           set NoteOff [lreplace $NoteOff 3 3 $pitch] 
        }
      }
    }

    # draw the grid, including measures, beats, and quantization
    $w.note create rectangle 100 100 100 100 -fill $backcolor \
      -tags "zoom marker" -outline $backcolor
    pianoRollGrid $trk

    # now fill in all the notes
    fillPianoRoll $trk 

    showMiddleC $trk
    normalCursor $w
  }
  normalCursor .
}

proc pianoRollGrid {trk} {
  global PIANOSCALE
  global TimeScale ShowMidC PlayFile Mquantize

  # we need this to be separate since changes in time signature
  # will futz with this grid..

  scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk
  set w .pr$trk

  # first delete the existing grid
  $w.note delete measure
  $w.note delete beat
  $w.note delete quantum

  # the finest quantization level
  set Mquantize 32

  # get the current magnification and scale back to 1, restoring later.
  set mag [lindex [$w.note coords marker] 0]
  $w.note scale zoom 0 0 [expr 100.0 / $mag] 1

  set ynote    [expr  56.000000 * $PIANOSCALE]
  set lastnote [miditrack $PlayFile $trk end]
  scan [tick2measure $lastnote] "%d:%d:%d" lastmeas bb tt
  if {$bb != 0 || $tt != 0} { incr lastmeas }

  set quantcolor [lindex [$w.quantum configure -foreground] 4]
  set beatcolor  [lindex [$w.beat configure -foreground] 4]
  set meascolor  [lindex [$w.measure configure -foreground] 4]
  set qpb [expr $Mquantize / 4]

  for {set i 0} {$i <= $lastmeas} {incr i} {
    set num [lindex [getMeter $i:0:0] 2]
    for {set j 0} {$j < $num} {incr j} {
      set xval [expr [measure2tick $i:$j:0] * $TimeScale / $mdiv]
      $w.note create line $xval 0 $xval $ynote -fill $beatcolor \
        -tags "beat zoom bm"
      for {set k 0} {$k < $qpb} {incr k} {
        set qval [expr $xval + $k * $TimeScale / $qpb]
        $w.note create line $qval 0 $qval $ynote -fill $quantcolor \
          -tags "quantum zoom bm bb"
      }
    }
    set xval [expr [measure2tick $i:0:0] * $TimeScale / $mdiv]
    $w.note create line $xval 0 $xval $ynote -fill $meascolor \
      -tags "measure zoom"
  }

  # restore zoom, as promised
  $w.note scale zoom 0 0 [expr $mag / 100.0] 1

  showQuantization $trk
  showBeats $trk
  showMeasures $trk
}

proc pianoRollZoom {trk xscale} {
  set screg [lindex [.pr$trk.note configure -scrollregion] 4]
  set xval  [lindex $screg 2]
  set newsr [lreplace $screg 2 2 [expr $xscale * $xval]]

  .pr$trk.note scale zoom 0 0 $xscale 1
  .pr$trk.note configure -scrollregion $newsr
}

proc showMiddleC {trk} {
  global ShowMidC ActiveKey WhiteKey

  if {$ShowMidC($trk)} {
    .pr$trk.keys itemconfigure n60 -fill $ActiveKey
  } else {
    .pr$trk.keys itemconfigure n60 -fill $WhiteKey
  }
}

proc showMeasures {trk} {
  global ShowBars

  if {$ShowBars($trk)} {
    set gridcolor [lindex [.pr$trk.measure configure -foreground] 4]
    .pr$trk.note itemconfigure measure -fill $gridcolor
    .pr$trk.note raise measure bm
  } else {
    set fillwith [lindex [.pr$trk.note configure -background] 4]
    .pr$trk.note itemconfigure measure -fill $fillwith 
    .pr$trk.note lower measure grid
  }
  .pr$trk.note raise event
}

proc showBeats {trk} {
  global ShowBeat

  if {$ShowBeat($trk)} {
    set gridcolor  [lindex [.pr$trk.beat configure -foreground] 4]
    .pr$trk.note itemconfigure beat -fill $gridcolor
    .pr$trk.note raise beat bb
  } else {
    set fillwith [lindex [.pr$trk.note configure -background] 4]
    .pr$trk.note itemconfigure beat -fill $fillwith 
    .pr$trk.note lower beat grid
  }
  showMeasures $trk
}

proc showQuantization {trk} {
  global DQUANTIZE MAXQUANT
  global PlayFile ShowQuan Mquantize

  if {$DQUANTIZE != $Mquantize} {
    set tmpQuant [expr {($MAXQUANT < $DQUANTIZE) ? $MAXQUANT : $DQUANTIZE}]
    set xscale [expr $Mquantize.0000 / $tmpQuant.0000] 
    .pr$trk.note scale quantum 0 0 $xscale 1
    set Mquantize $tmpQuant
  }

  if {$ShowQuan($trk)} {
    set quantcolor  [lindex [.pr$trk.quantum configure -foreground] 4]
    .pr$trk.note itemconfigure quantum -fill $quantcolor
    .pr$trk.note raise quantum bq
    if {$DQUANTIZE > $MAXQUANT} {
      dialog .pr$trk.h .pr$trk \
        "  Piano roll quantization is limited to 1/$MAXQUANT notes.\nChange MAXQUANT if you need finer resolution." \
        info 0 OK
    }
  } else {
    set fillwith [lindex [.pr$trk.note configure -background] 4]
    .pr$trk.note itemconfigure quantum -fill $fillwith 
    .pr$trk.note lower quantum grid
  }
  showBeats $trk
}

proc fillPianoRoll {tlist} {
  global PIANOSCALE 
  global TimeScale PlayFile

  scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk

  # width of a piano key, sort-of
  set yscale [expr 7.000000 * $PIANOSCALE / 12.000000]

  foreach i $tlist {
    if {![winfo exists .pr$i]} { break }

    midirewind $PlayFile $i
    raise .pr$i

    # get the current magnification and scale back to 1, restoring later.
    set mag [lindex [.pr$i.note coords marker] 0]
    .pr$i.note scale zoom 0 0 [expr 100.0 / $mag] 1

    # erase whatever is there already
    .pr$i.note delete note

    set sum 0; set num 0; set start 0
    set gridcolor  [lindex [.pr$i.grid configure -foreground] 4]
    set eventcolor [lindex [.pr$i.event configure -foreground] 4]

    while {[set event [midiget $PlayFile $i next]] != "EOT"} {
      set etype [lindex $event 1]
      if {$etype == "Note"} {
        # Time Scale is adjustable
        set x0 [expr [lindex $event 0] * $TimeScale / $mdiv]
        set x1 [expr $x0 + [lindex $event 5] * $TimeScale / $mdiv]

	if {$num == 0} { set start $x0 }

        # Piano Roll goes from C0 to C8
        set y0 [expr ([lindex $event 3] - 24.000000) * $yscale] 
        set y1 [expr $y0 + $yscale]

        set sum [expr $sum + $y0]; incr num

        .pr$i.note create rectangle $x0 $y0 $x1 $y1 \
           -tags "event zoom" -fill $eventcolor
      }
    }
    set xsize [lindex [.pr$i.note configure -width] 4]
    set ysize [lindex [.pr$i.note configure -height] 4]

    tkwait visibility .pr$i.note

    if {$num != 0} {
      set center [expr $sum / $num]
      set visib  [winfo height .pr$i.note]
      set hidden [expr $center - $visib / 2.000000]
      set inthid [expr round($hidden / $yscale) * $yscale]
      pianoRollScroll .pr$i moveto [expr $inthid / $ysize]
    }
    if {$start != 0} {
      set visib  [winfo width .pr$i.note]
      scan [tick2measure [expr $start * $mdiv / $TimeScale]] "%d" tmeas
      set start [expr [measure2tick $tmeas:0:0] * $TimeScale / $mdiv]
      .pr$i.note xview moveto [expr $start / $xsize]
    }

    .pr$i.note scale zoom 0 0 [expr $mag / 100.0] 1
  }
}

# -----------BEGIN TrackInfo subroutines -----------------------
#
proc closeTrackInfo {tlist} {
  global PlayFile

  scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk

  # handle the cases where no tracks are specified
  if {$tlist == ""} {
    for {set i 0} {$i < $mtrk} {incr i} {
      set tlist "$tlist $i"
    }
  }
  foreach i $tlist {
    if {[winfo exists .ti$i]} { destroy .ti$i }
  }
}

proc closePianoRoll {tlist} {
  global PlayFile

  scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk

  # handle the cases where no tracks are specified
  if {$tlist == ""} {
    for {set i 0} {$i < $mtrk} {incr i} {
      set tlist "$tlist $i"
    }
  }
  foreach i $tlist {
    if {[winfo exists .pr$i]} {
       .pr$i.note delete all
       .pr$i.keys delete all
       destroy .pr$i
    } 
  }
}

# ---------------------------------------------------------------------
#
proc fillTrackInfo {tlist keep} {
  global MeasView PlayFile

  foreach i $tlist {
    if {![winfo exists .ti$i]} { break }

    midirewind $PlayFile $i
    raise .ti$i

    # erase whatever is there already
    .ti$i.list delete 0 end

    while {[set event [midiget $PlayFile $i next]] != "EOT"} {
      if {$MeasView} {
        set tt [lindex $event 0]
        set tt [tick2measure $tt]
        set event [lreplace $event 0 0 $tt]
      }
      .ti$i.list insert end $event
    }

    # keep current position if requested
    if {$keep} {
      .ti$i.list yview moveto [lindex [.ti$i.scroll get] 0]
    }
  }
}

proc tick2measure {tick} {
  global Mdivision 

  set tick [expr round($tick)]

  # get meter/tick mapping for this measure
  scan [getMeter $tick] "%d %d %d %d" btick bmeas num den

  set tpb  [expr $Mdivision * 4 / $den]
  set tick [expr $tick - $btick]

  set beat [expr $tick / $tpb]

  set meas [expr $bmeas + $beat / $num]
  set beat [expr $beat % $num]
  set frac [expr $tick % $tpb]

  set foo [format "%d:%2d:%3d" $meas $beat $frac]
  regsub -all " " $foo "0" foo
  return $foo
}

proc measure2tick {measure} {
  global Mdivision

  if {[scan $measure "%d:%d:%d" meas beat frac] == 1} {
    # already just a tick
    return $meas
  }
  # get meter/tick mapping for this measure
  scan [getMeter $measure] "%d %d %d %d" btick bmeas num den

  # Mdivision ticks per quarter note.  
  set tpb [expr $Mdivision * 4 / $den]
  set tpm [expr $tpb * $num]

  # Compute ticks 
  set foo [expr $btick + ($meas - $bmeas) * $tpm + $beat * $tpb + $frac]
  return $foo
}

proc getMeter {x} {
  global MeterMap

# x may be a tick or a measure m:b:t
  if {[scan $x "%d:%d:%d" meas beat frac] == 1} {
    set si 0
    set sx $x
  } else {
    set si 1
    set sx $meas
  }

# default is 4/4, if no meter events are found
  set meter {0 0 4 4}	

# brute force for now
  foreach m $MeterMap {
    if {[lindex $m $si] > $sx} { break }
    set meter $m
  }

  return $meter
}

proc buildMeterMap {} {
  global MeterMap PlayFile

  scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk

  set MeterMap ""
  set btick 0
  set bmeas 0
  set num 4
  set den 4

  midirewind $PlayFile 0
  while {[set event [midiget $PlayFile 0 next]] != "EOT"} {
    set etype [lindex $event 1]
    if {$etype == "MetaTime"} {
      set tpb  [expr $mdiv * 4 / $den]
      set tock [expr [lindex $event 0] - $btick]
      set beat [expr $tock / $tpb]
      set bmeas [expr $bmeas + $beat / $num]

      scan $event "%d MetaTime %d %d" btick num den
      lappend MeterMap "$btick $bmeas $num $den"
    }
  }

  # update any piano rolls that might be open
  for {set i 0} {$i < $mtrk} {incr i} {
    if {[winfo exists .pr$i]} { pianoRollGrid $i }
  }
}

proc newQuantization {} {
  global PlayFile

  scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk
  for {set i 0} {$i < $mtrk} {incr i} {
    if {[winfo exists .pr$i]} { showQuantization $i }
  }
}

proc newDivision {} {
  global DDIVISION
  global PlayFile

  scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk
  if {$mdiv == $DDIVISION} {
    return
  }

  if {$mdiv > $DDIVISION} {
    if {[dialog .ddiv . " Reducing DIVISION may\nadversely affect timing." \
      info 0 OK Cancel]} {
      set DDIVISION $mdiv
      return
    }
  }

  watchCursor .
  scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk

  set tmpf [midimake]
  midiconfig $tmpf "division $DDIVISION" "format $mfmt" "tracks $mtrk"

  for {set i 0} {$i <  $mtrk} {incr i} {
    midicopy "$tmpf $i" 0 "$PlayFile $i" 0 \
      [expr "[miditrack $PlayFile $i end] + 1"]
  }
  midifree $PlayFile
  set PlayFile $tmpf

  normalCursor .
  showTrackStatusLine
}

proc getConfig {mf} {
  if {$mf == ""} {
    set mdiv 0
    set mfmt 0
    set mtrk 0
  } else {
    set config [midiconfig $mf division format tracks]
    set mdiv [lindex [lindex $config 0] 1]
    set mfmt [lindex [lindex $config 1] 1]
    set mtrk [lindex [lindex $config 2] 1]
  }
  return "$mdiv $mfmt $mtrk"
}

proc measureInfo {event} {
  global MeasView

  set tick [lindex $event 0]
  if {[scan $tick "%d:%d:%d" meas beat frac] == 1} {
    set tick [tick2measure $tick]
    scan $tick "%d:%d:%d" meas beat frac
  }
  return [list $meas $beat $frac]
}

# ---------------------------------------------------------------------
# this procedure deletes MIDI events via the track info screen.
#
proc deleteEvents {i} {
  global Modified PlayFile

  set elist [.ti$i.list curselection]
  if {$elist != ""} {
    foreach j $elist {
      set event [.ti$i.list get $j]
      set tick [lindex $event 0]
      set event [lreplace $event 0 0 [measure2tick $tick]]
      mididelete $PlayFile $i $event
    }
    # refresh the display
    showTrackEverything $i
    trackInfo $i
    set Modified 1
  }
}

# ---------------------------------------------------------------------
# this procedure edits a list of MIDI events selected via Track Info
#
proc modifyEvents {i} {
  global Modified MeasView PlayFile

  # make sure we own it before we modify it
  set elist [.ti$i.list curselection]
  if {$elist != ""} {
      foreach j $elist {
        set event [.ti$i.list get $j]
        set newevent \
          [getEntry .ti$i.ee .ti$i.list  "Modify Event" {} "Track $i:" \
            "$event" 40 {} {}]

        set tick [lindex $event 0]
        set event [lreplace $event 0 0 [measure2tick $tick]]
        set tick [lindex $newevent 0]
        set newevent [lreplace $newevent 0 0 [measure2tick $tick]]

        mididelete $PlayFile $i $event
        while {[catch {midiput $PlayFile $i "$newevent"} msg]} {
          if {[dialog .ti$i.di .ti$i "Unable to put event." warning \
            0 {Try again} {Keep original}]} {
            midiput $PlayFile $i $event
            break
          } else {
            set newevent \
              [getEntry .ti$i.ee .ti$i.list  "Modify Event" {} "Track $i:" \
              "$newevent" 40 {} {}]
          }
        }
      }
      # refresh the display
      showTrackEverything $i
      trackInfo $i
      set Modified 1
  }
}

# ---------------------------------------------------------------------
# this procedure copies a list of MIDI events selected via Track Info,
# for now pasting them to the end of the track
#
proc copyEvents {i} {
  global Modified MeasView PlayFile

  # make sure we own it before we modify it
  set elist [.ti$i.list curselection]
  if {$elist != ""} {

      # compute the number of the last measure
      set tt [expr [llength $elist] - 1]
      set first [lindex [measureInfo [.ti$i.list get [lindex $elist 0]]] 0]
      set last [lindex [measureInfo [.ti$i.list get [lindex $elist $tt]]] 0]
      set nummeasures [expr $last - $first + 1]

      dialog .ti$i.di .ti$i "I count $nummeasures measures" info 0 {OK}

      set numcopies \
      [getEntry .ti$i.et .ti$i "Copy Events" {} "number of copies:" 1 4 {} {}]

      # Take care of EOT
      midirewind $PlayFile $i
      set ticks [miditrack $PlayFile $i end]
      set pastepoint [expr [lindex [measureInfo $ticks] 0] + 1]
      dialog .ti$i.di .ti$i "Pastepoint at measure $pastepoint" info 0 {OK}

      foreach j $elist {
        set event [.ti$i.list get $j]
        set minfo [measureInfo $event]
        set meas [expr $pastepoint + [lindex $minfo 0] - $first]
        set beat [lindex $minfo 1]
        set frac [lindex $minfo 2]
        for {set k 0} {$k < $numcopies} {incr k} {
          set newm [expr $k * $nummeasures + $meas]
          set tick [measure2tick "$newm:$beat:$frac"]
          set event [lreplace $event 0 0 $tick]
          if {[catch {midiput $PlayFile $i "$event"} msg]} {
            dialog .ti$i.di .ti$i
               "Unable to put event $newm:$beat:$frac $event" info 0 {OK}
          }
        }
      }
      fixMetaEndOfTrack $PlayFile $i

      # refresh the display
      showTrackEverything $i
      trackInfo $i
      set Modified 1
  }
}
# ---------------------------------------------------------------------

proc trackScrollUD {args} {
  eval .trknumb.list yview $args
  eval .trkname.list yview $args
  eval .trkmute.list yview $args
  eval .trkprog.list yview $args
  eval .trkchan.list yview $args
  eval .trkmeas.list yview $args
}

# This is a hack.  General solution?
proc trackScan {a line fract} {
  foreach i {numb name mute chan prog meas} {
    if {[string compare .trk$i.list $a] != 0} {
      eval .trk$i.list yview moveto $line
    }
  }
#  foreach i {meas} {
#    if {[string compare .trk$i.text $a] != 0} {
#      eval .trk$i.text yview moveto $line
#    }
#  }
  eval .trkscr.scroll set $line $fract
}

# -------- END of track commands -----------------------------------
      
# -------- BEGIN sequencer control buttons -------------------------
# These should be self-explanatory
#
proc seqPause {} {
  dialog .d . "Pause has not been\nimplemented yet." info 0 OK
}
# ------------------------------------------------------------------
#
proc seqRewind {} {
  dialog .d . \
    "Rewind happens every time you\npress Stop (for now)." info 0 OK
}
# ------------------------------------------------------------------
#
proc seqFFwd {} {
  dialog .d . \
    "Fast forward has not been\nimplemented yet." info 0 OK
}

# ------------------------------------------------------------------
# This only starts recording.  The Stop function cleans up afterwards
# Maybe that should be cleaned up with a tkwait.
#
proc seqRecord {} {
  global TEMPO DEVTAB NUMDEV
  global MidiState PlayFile RecFile TmpFile LabelNow CurDev MasterDev
  global Modified MuteList SoloList Mdivision PlayF

  if {$MidiState == "stopped"} {
    # make sure we have a play file
    if {$PlayFile == ""} {
      set PlayFile [midimake]
      midiconfig $PlayFile "tracks 1" "division $Mdivision"
      midiput $PlayFile 0 "0 MetaTempo $TEMPO"
    }
    # create space for the new song
    set RecFile [midimake]
    set pconfig [midiconfig $PlayFile division]
    midiconfig $RecFile "tracks 1" [lindex $pconfig 0]
    watchCursor .
    if {$SoloList != ""} {
      set TmpFile [midikeep $PlayFile $SoloList 0]
      set foo $TmpFile
    } elseif {$MuteList != ""} {
      set TmpFile [midiremove $PlayFile $MuteList 0]
      set foo $TmpFile
    } else {
      set foo $PlayFile
    }
    getStopTime $foo

# Now strip off any channels we don't want, and play what's left
    for {set i 0} {$i < $NUMDEV} {incr i} {
      if {$DEVTAB($i,map) == {}} {
        set PlayF($i,pointer) $foo
        set PlayF($i,data) ""
      } else {
        dialog .h . "Midimap support doesn't work yet." info 0 OK
	set PlayF($i,pointer) $foo
	set PlayF($i,data) ""
#        set PlayF($i,data) [midimap $foo $DEVTAB($i,map)]
#        set PlayF($i,pointer) $PlayF($i,data)
      }
      if {$i != $MasterDev} {
        midiplay $DEVTAB($i,dev) $PlayF($i,pointer)
      }
    }
    normalCursor .

# The master must start last
    midirecord $DEVTAB($MasterDev,dev) $RecFile $PlayF($MasterDev,pointer)

    updateButtons 0 2 1 0 0 0
    set MidiState recording
    WaitForStop$LabelNow
  }
}
# ------------------------------------------------------------------
#
proc seqPlay {} {
  global DEVTAB NUMDEV
  global MidiState PlayFile LabelNow Modified TmpFile
  global MuteList SoloList CurDev MasterDev PlayF

# First strip off any tracks we don't want..

  watchCursor .

  if {$PlayFile != "" && $MidiState == "stopped"} {
    midistop $DEVTAB($CurDev,dev)
    if {$SoloList != ""} {
      set TmpFile [midikeep $PlayFile $SoloList 0]
      set foo $TmpFile
    } elseif {$MuteList != ""} {
      set TmpFile [midiremove $PlayFile $MuteList 0]
      set foo $TmpFile
    } else {
      set foo $PlayFile
    }
    getStopTime $foo

# Now strip off any channels we don't want, and play what's left
    for {set i 0} {$i < $NUMDEV} {incr i} {
      if {$DEVTAB($i,map) == {}} {
        set PlayF($i,pointer) $foo
        set PlayF($i,data) ""
      } else {
        dialog .h . "Midimap support doesn't work yet." info 0 OK
	set PlayF($i,pointer) $foo
	set PlayF($i,data) ""
#        set PlayF($i,data) [midimap $foo $DEVTAB($i,map)]
#        set PlayF($i,pointer) $PlayF($i,data)
      }
      if {$i != $MasterDev} {
        midiplay $DEVTAB($i,dev) $PlayF($i,pointer)
      }
    }
    normalCursor .

# The master must start last
    midiplay $DEVTAB($MasterDev,dev) $PlayF($MasterDev,pointer)

    updateButtons 2 0 1 0 0 0 
    set MidiState playing
    WaitForStop$LabelNow
  }
}
# --------------------------------------------------------------------
#
proc seqStop {} {
  global DEVTAB NUMDEV
  global MidiState PlayFile RecFile TmpFile Now CurDev
  global PlayName StopTime Modified PlayF

   scan [getConfig $PlayFile] "%d %d %d" mdiv mfmt mtrk

  if {$MidiState != "stopped"} {
    set prev_state $MidiState
    set MidiState stopped
    set StopTime 0
    midistop $DEVTAB($CurDev,dev)  
    # free scratch space (created in seqRecord or seqPlay)
    if {$TmpFile != ""} {
      midifree $TmpFile
      set TmpFile ""
    }
    for {set i 0} {$i < $NUMDEV} {incr i} {
      if {$PlayF($i,data) != ""} {
        midifree $PlayF($i,data)
        set PlayF($i,pointer) ""
        set PlayF($i,data) ""
      }
    }
    if {$prev_state == "recording"} {
      midirewind $RecFile
      midirewind $PlayFile
      if {$PlayName == "untitled.mid" && $Modified == 0} {
	if {[dialog .h . "Keep track?" questhead 0 OK Discard]} {
          midiCleanSlate 0
        } else {
          # use format 1
          midiconfig $PlayFile "format 1" "tracks 2"
          set Modified 1
          # add EOT to new track and split it up
	  addMetaEndOfTrack $RecFile 0
          midisplit "$RecFile 0" "$PlayFile 0" "$PlayFile 1"
	  midifree $RecFile; set RecFile ""
          showTrackEverything "0 1"
          writeTrackNames {}
        }
      } else {
        if {[dialog .h . "Merge new track into $PlayName?" \
            questhead 0 OK Discard]} {
          midifree $RecFile
          set RecFile ""
        } else {
          # create a format one file consisting of the play file
          # plus the recorded track
          set newtrack $mtrk
          incr mtrk
	  set newfile [midimake]
	  midiconfig $newfile "format 1" "division $mdiv" "tracks $mtrk"
	  if {$mfmt} {
	    # copy each track to newfile
	    for {set i 0} {$i < $newtrack} {incr i} {
	      midicopy "$newfile $i" 0 "$PlayFile $i" \
	        0 [expr "[miditrack $PlayFile $i end] + 1"]
	    }
	    # including the new one
	    set newlist $newtrack
	    midicopy "$newfile $newtrack" 0 "$RecFile 0" \
	      0 [expr "[miditrack $RecFile 0 end] + 1"]
	  } else {
	    # split track 0 into newfile (adding yet another track)
            set newlist ""
            set newtrack $mtrk
            incr mtrk 2
	    midiconfig $newfile "tracks $mtrk"
	    midisplit "$PlayFile 0" "$newfile 0" "$newfile 1"
     	    midicopy "$newfile 2" 0 "$RecFile 0" 0 \
     	      [expr "[miditrack $RecFile 0 end] + 1"]
	  }
	  # add EOT to recorded track
	  addMetaEndOfTrack $newfile $newtrack
	  midifree $PlayFile
	  midifree $RecFile
	  set PlayFile $newfile
          set Modified 1
          showTrackEverything $newlist
          writeTrackNames $newlist
        }
      }
    }
    set RecFile ""
    if {$PlayFile == ""} {
      updateButtons 0 1 0 0 0 0
    } else {
      updateButtons 1 1 0 0 0 0
    }
  } 
}

# ------------------------------------------------------------------
# Make labels and data regarding time change when clock is changed
#
proc updateDeviceInfo {} {
  global DEVTAB
  global Now LabelNow SmpteClk CurDev

  # select device
  if {$SmpteClk($CurDev) == 1} {
    midifeature $DEVTAB($CurDev,dev) smpte_timing
    set LabelNow "SMPTE:"
    set foo [midifeature $DEVTAB($CurDev,dev) get_smpte]
    if {$foo == "NOSYNC" || $foo == "ERR"} {
      set Now "No Sync"
    } else {
      set Now [string range $foo 0 7]
    }
  } else {
    if {$SmpteClk($CurDev) == 2} {
      midifeature $DEVTAB($CurDev,dev) mpu401_timing
    } else {
      midifeature $DEVTAB($CurDev,dev) kernel_timing
    }
    set LabelNow "MIDI:"
    set foo [miditime $DEVTAB($CurDev,dev)]
    if {$foo == "ERR"} {
      set Now [tick2measure 0]
    } else {
      set Now [tick2measure $foo]
    }
  }
  update idletasks
}

# ------------------------------------------------------------------
# Called by play and record in MIDI mode to display MIDI time
#
proc WaitForStopMIDI: {} {
  global DEVTAB
  global Now StopTime MidiState CurDev

  # MIDI mode -- user intervention or when track playback is finished
  if {$MidiState != "stopped"} {
    set foo [miditime $DEVTAB($CurDev,dev)]
    if {$foo > $StopTime && $MidiState == "playing"} {
      seqStop; return
    }
    set Now [tick2measure $foo]
    update
    after 100 WaitForStopMIDI:
  }
}

proc WaitForStopSMPTE: {} {
  global DEVTAB
  global Now SmpteClk StopTime MidiState CurDev

  # SMPTE mode -- stop through user intervention only
  if {$MidiState != "stopped"} {
    set foo [midifeature $DEVTAB($CurDev,dev) get_smpte]
    if {$foo == "NOSYNC"} {
      set Now "No Sync"
    } else {
      set Now [string range $foo 0 7]
    }
    update
    after 500 WaitForStopSMPTE:
  }
}

# -------- END of sequencer control buttons -------------------------

# -------- BEGIN utilities which should be written in C++ -----------
# They use no globals (other than their return values, which may or
# may not really be necessary..)
#
#--------------------------------------------------------------------
# Use this one if all is well..
#
proc addMetaEndOfTrack {mf trk} {
  midiput $mf $trk "[miditrack $mf $trk end] MetaEndOfTrack"
}

# Use this one if things might be messed up..
#
proc fixMetaEndOfTrack {mf trk} {

  midirewind $mf $trk

  watchCursor .
  while {[set event [midiget $mf $trk next]] != "EOT"} {
    if {[lindex $event 1] == "MetaEndOfTrack"} {
      mididelete $mf $trk $event
    }
  }
  addMetaEndOfTrack $mf $trk
  normalCursor .
}

proc midiquantize {mf tlist quantize} {
  global lost

  scan [getConfig $mf] "%d %d %d" mdiv mfmt mtrk

  # quarter note gets mdiv ticks
  set iqdiv [expr 4*$mdiv/$quantize]
  set fqdiv [expr $iqdiv*1.0000]
  set lost 0

  set tmpf [midimake]
  midiconfig $tmpf "division $mdiv" "tracks 1" "format $mfmt"

  foreach i $tlist {
    midirewind $mf $i
    midirewind $tmpf $i

    while {[set event [midiget $mf $i next]] != "EOT"} {
      set ticks [lindex $event 0]
      set newticks [expr round($ticks/$fqdiv)*$iqdiv]
      set newevent [lreplace $event 0 0 $newticks]
      if {[catch {midiput $tmpf 0 "$newevent"}]} {
        incr lost
      }
    }
    mididelete $mf $i range 0 [expr "[miditrack $mf $i end] + 1"]
    midicopy "$mf $i" 0 "$tmpf 0" 0 [expr "[miditrack $tmpf 0 end] + 1"]
    mididelete $tmpf 0 range 0 [expr "[miditrack $tmpf 0 end] + 1"]
    fixMetaEndOfTrack $mf $i
  }
  midifree $tmpf
  return $lost
}

#
proc midioffset {mf tlist offset} {
  global lost

  scan [getConfig $mf] "%d %d %d" mdiv mfmt mtrk
  set lost 0

  set tmpf [midimake]
  midiconfig $tmpf "division $mdiv" "tracks 1" "format $mfmt"

  foreach i $tlist {
    midirewind $mf $i
    midirewind $tmpf $i

    while {[set event [midiget $mf $i next]] != "EOT"} {
      set ticks [lindex $event 0]
      set newticks [expr $ticks+$offset]
      if {$newticks < 0} { set newticks 0 }
      set newevent [lreplace $event 0 0 $newticks]
      if {[catch {midiput $tmpf 0 "$newevent"}]} {
        incr lost
      }
    }
    mididelete $mf $i range 0 [expr "[miditrack $mf $i end] + 1"]
    midicopy "$mf $i" 0 "$tmpf 0" 0 [expr "[miditrack $tmpf 0 end] + 1"]
    mididelete $tmpf 0 range 0 [expr "[miditrack $tmpf 0 end] + 1"]
    fixMetaEndOfTrack $mf $i
  }
  midifree $tmpf
  return $lost
}

#
proc miditranspose {mf tlist offset} {
  global lost

  scan [getConfig $mf] "%d %d %d" mdiv mfmt mtrk
  set lost 0

  set tmpf [midimake]
  midiconfig $tmpf "division $mdiv" "tracks 1" "format $mfmt"

  foreach i $tlist {
    midirewind $mf $i
    midirewind $tmpf $i

    while {[set event [midiget $mf $i next]] != "EOT"} {
      set etype [string range [lindex $event 1] 0 3]
      if {$etype == "Note"} {
        set newevent [lreplace $event 3 3 [expr [lindex $event 3]+$offset]]
      } else {
        set newevent $event
      }
      if {[catch {midiput $tmpf 0 "$newevent"}]} {
        incr lost
      }
    }
    mididelete $mf $i range 0 [expr "[miditrack $mf $i end] + 1"]
    midicopy "$mf $i" 0 "$tmpf 0" 0 [expr "[miditrack $tmpf 0 end] + 1"]
    mididelete $tmpf 0 range 0 [expr "[miditrack $tmpf 0 end] + 1"]
    fixMetaEndOfTrack $mf $i
  }
  midifree $tmpf
  return $lost
}

proc midivolume {mf tlist scale} {
  global lost

  scan [getConfig $mf] "%d %d %d" mdiv mfmt mtrk
  set lost 0

  set tmpf [midimake]
  midiconfig $tmpf "division $mdiv" "tracks 1" "format $mfmt"

  foreach i $tlist {
    midirewind $mf $i
    midirewind $tmpf $i

    while {[set event [midiget $mf $i next]] != "EOT"} {
      set etype [string range [lindex $event 1] 0 3]
      if {$etype == "Note"} {
        set oldvol [lindex $event 4]
        set newvol [expr round($oldvol*$scale)]
        set newvol [expr ($newvol < 127) ? $newvol : 127]
        set newevent [lreplace $event 4 4 $newvol]
      } else {
        set newevent $event
      }
      if {[catch {midiput $tmpf 0 "$newevent"}]} {
        incr lost
      }
    }
    mididelete $mf $i range 0 [expr "[miditrack $mf $i end] + 1"]
    midicopy "$mf $i" 0 "$tmpf 0" 0 [expr "[miditrack $tmpf 0 end] + 1"]
    mididelete $tmpf 0 range 0 [expr "[miditrack $tmpf 0 end] + 1"]
    fixMetaEndOfTrack $mf $i
  }
  midifree $tmpf
  return $lost
}

# ---------------------------------------------------------------------
# Requires that you have my rand extensions compiled into tcl
#
proc midirandomize {mf tlist mu var} {
  global lost 

  scan [getConfig $mf] "%d %d %d" mdiv mfmt mtrk

  set tmpf [midimake]
  midiconfig $tmpf "division $mdiv" "tracks 1" "format $mfmt"

  rand {dist normal} {type integer} "mean $mu" "variance $var" 
  rand "seed [exec date +%s]"
  set lost 0

  foreach i $tlist {
    # remove MetaEOT
    midirewind $mf $i
    while {[set event [midiget $mf $i next]] != "EOT"} {
      set ticks [lindex $event 0]
      set newticks [expr $ticks + [rand]]
      if {$newticks < 0} { set newticks 0 }
      set newevent "$newticks [lrange $event 1 end]"
      if {[catch {midiput $tmpf 0 "$newevent"}]} {
        incr lost
      }
    }
    mididelete $mf $i range 0 [expr "[miditrack $mf $i end] + 1"]
    midicopy "$mf $i" 0 "$tmpf 0" 0 [expr "[miditrack $tmpf 0 end] + 1"]
    mididelete $tmpf 0 range 0 [expr "[miditrack $tmpf 0 end] + 1"]
    fixMetaEndOfTrack $mf $i
  }
  midifree $tmpf
  return $lost
}

# ---------------------------------------------------------------------
# Strip selected tracks out of a MIDI file
#
# Initially I wrote this function to copy a midi file, omitting any 
# muted tracks.  Then I thought it was similar to what I wanted to do
# to remove tracks.  It is but it isn't.  Anyway, usage:
#
# midiremove [song] [tracklist] [overwrite] :: this command returns
# a midi song which looks just like [song] except it's missing the
# tracks specified in tracklist.  If [overwrite] is true, the new 
# song is the old song, i.e. tracks are deleted.  If [overwrite] is
# false, a copy of the song is made and the original is untouched.
#
proc midiremove {mf tlist overwrite} {
  global newfile

  scan [getConfig $mf] "%d %d %d" mdiv mfmt mtrk
  set newsize [expr $mtrk - [llength $tlist]]

  if {$overwrite} {
    set newfile $mf
    for {set i 0} {$i < $mtrk} {incr i} {
      # remove one indicated track
      if {[lsearch -exact $tlist $i] != -1} {
        mididelete $mf $i range 0 [expr "[miditrack $mf $i end] + 1"]
        # and find one above to fall into its place
        for {set k [expr $i+1]} {$k < $mtrk} {incr k} {
          if {[lsearch -exact $tlist $k] == -1} {
            midicopy "$mf $i" 0 "$mf $k" 0 \
              [expr "[miditrack $mf $k end] + 1"]
            # marking the old copy for deletion as well..
            set tlist "$tlist $k"
            break
          }
        }
      }
    }
    midiconfig $mf "tracks $newsize"
  } else {
    # I know I could just copy the entire song and use the above 
    # function but I want this to be fast for play w/ muted tracks
    set newfile [midimake]
    midiconfig $newfile "tracks $newsize" "division $mdiv" "format $mfmt"
    for {set i 0; set k 0} {$i < $mtrk} {incr i} {
      # copy the non-indicated tracks
      if {[lsearch -exact $tlist $i] == -1} {
        midicopy "$newfile $k" 0 "$mf $i" 0 \
          [expr "[miditrack $mf $i end] + 1"]
        incr k
      }
    }
  }
  return $newfile
}

proc midikeep {mf tlist overwrite} {

  scan [getConfig $mf] "%d %d %d" mdiv mfmt mtrk
  if {$mfmt == 1 && [lsearch -exact $tlist 0] == -1} {
    set tlist [lappend tlist 0]
  }
  for {set i 0; set rlist ""} {$i < $mtrk} {incr i} {
    if {[lsearch -exact $tlist $i] == -1} {
      set rlist [lappend rlist $i]
    }
  }
  set newfile [midiremove $mf $rlist $overwrite]
  return $newfile
}

# ---------------------------------------------------------------------
# Remove all events on channels listed by clist

proc midimap {mf clist} {

  # work with a copy of the original
  set newf [midiremove $mf {} 0]

  scan [getConfig $newf] "%d %d %d" mdiv mfmt mtrk
  for {set i 0} {$i < $mtrk} {incr i} {
    midirewind $newf $i

    while {[set event [midiget $mf $i next]] != "EOT"} {
      set etype [string range [lindex $event 1] 0 3]
      if {$etype != "Meta"} {
        if {[lsearch -exact $clist [lindex $event 2]] == -1} {
          mididelete $newf $i $event
        }
      }
    }
    fixMetaEndOfTrack $newf $i
  }
  return $newf
}

# ---------------------------------------------------------------------
# Requires that you have my rand extensions compiled into tcl
    
  


# --------------END stuff that should be coded into C++ ---------------

# --------------BEGIN (useful?) reusable dialog-box stuff ----------------

# See John Ousterhout's book -- that's where this came from
#
proc dialog {w parent text bitmap default args} {
  global button

  toplevel $w -class Dialog
  wm transient $w $parent
  set x [expr [winfo x $parent]+80]
  set y [expr [winfo y $parent]+60]
  wm geometry $w "+$x+$y"
  frame $w.top -relief raised -bd 1
  pack $w.top -side top -fill both
  frame $w.bot -relief raised -bd 1
  pack $w.bot -side bottom -fill both
  message $w.top.message -width 5i -text $text 
  pack $w.top.message -side right -expand 1 -fill both \
    -padx 3m -pady 3m
  if {$bitmap != ""} {
    label $w.top.bitmap -bitmap $bitmap
    pack $w.top.bitmap -side left -padx 3m -pady 3m
  }
  set i 0
  foreach but $args {
    button $w.bot.but$i -text $but -command "set button $i"
    if {$i == $default} {
      frame $w.bot.def -relief sunken -bd 1
      raise $w.bot.but$i
      pack $w.bot.def -side left -expand 1 -padx 3m -pady 2m
      pack $w.bot.but$i -in $w.bot.def -side left -padx 2m \
        -pady 2m -ipadx 2m -ipady 1m
    } else {
      pack $w.bot.but$i -side left -expand 1 -padx 3m -pady 3m \
        -ipadx 2m -ipady 1m
    }
    incr i
  }
  if {$default >= 0} {
    bind $w <Return> "$w.bot.but$default flash; set button $default"
    bind $w <Button-3> "$w.bot.but$default flash; set button $default"
  }

  tkwait visibility $w
  grab set $w
  focus $w.bot.but$default

  tkwait variable button
  destroy $w
  return $button
}

#-------------------------------------------------------------------
#
proc getChannel {trk ochan} {
  global CHANNEL1
  global getval foo

  set w .tc$trk
  set parent .

  toplevel $w -class Dialog
  wm transient $w .
  set x [expr [winfo x $parent]+320]
  set y [expr [winfo y $parent]+140]
  wm geometry $w "+$x+$y"

  frame $w.top -relief raised -bd 2
  pack $w.top -side top -fill both 
  frame $w.mid -bd 1
  pack $w.mid -side top -fill x -expand 1
  for {set i 0} {$i < 4} {incr i} {
    frame $w.mid.m$i
    pack $w.mid.m$i -in $w.mid -side left -fill x -expand 1
  }

  frame $w.bot -relief raised -bd 1
  pack $w.bot -side bottom -fill both
  message $w.top.message -width 3i -text "Channel for Track $trk"
  pack $w.top.message -side right -fill both -padx 1m -pady 1m

  button $w.bot.ok -text "OK" -command {if {$getval != 99} {set foo 1}}
  button $w.bot.cancel -text "Cancel" -command {set getval 99; set foo 1}

  for {set i 0} {$i < 4} {incr i} {
    for {set j 0} {$j < 4} {incr j} {
      set ch0 [expr 4 * $i + $j + $CHANNEL1]
      set ch1 $ch0
      if {$ch1 < 10} {
        set ch1 " $ch1"
      }
      radiobutton $w.mid.b$ch0 -text $ch1 -variable getval -value $ch1 \
        -command "$w.bot.ok configure -state normal"
      pack $w.mid.b$ch0 -in $w.mid.m$i -side top -expand 1 -fill both
    }
  }

  pack $w.bot.ok $w.bot.cancel -side left -padx 2m -pady 2m \
        -ipadx 1m -fill x -expand 1

  if {$ochan == 99} {
    $w.bot.ok configure -state disabled
  } else {
    $w.mid.b$ochan select
  }

  tkwait visibility $w
  grab set $w

  tkwait variable foo
  destroy $w

  return $getval
}

# ------------------------------------------------------------------
#
proc getChannelMap {} {
  global CHANNEL1 DEVTAB
  global CurDev ChanMap
  global foo

  set w .mm$CurDev
  set parent .

  set omap $DEVTAB($CurDev,map)
  for {set i 0} {$i < 16} {incr i} {
    if {[lsearch $omap $i] == -1} {
      set ChanMap($i) 1
    } else {
      set ChanMap($i) 0
    }
  } 

  toplevel $w -class Dialog
  wm transient $w .
  set x [expr [winfo x $parent]+320]
  set y [expr [winfo y $parent]+140]
  wm geometry $w "+$x+$y"

  frame $w.top -relief raised -bd 2
  pack $w.top -side top -fill both 
  frame $w.mid -bd 1
  pack $w.mid -side top -fill x -expand 1
  for {set i 0} {$i < 4} {incr i} {
    frame $w.mid.m$i
    pack $w.mid.m$i -in $w.mid -side left -fill x -expand 1
  }

  frame $w.bot -relief raised -bd 1
  pack $w.bot -side bottom -fill both
  message $w.top.message -width 3i \
    -text "Channel Map for $DEVTAB($CurDev,name)"
  pack $w.top.message -side right -fill both -padx 1m -pady 1m

  button $w.bot.ok -text "OK" -command {set foo 1}
  button $w.bot.cancel -text "Cancel" -command {set foo -1}

  for {set i 0} {$i < 4} {incr i} {
    for {set j 0} {$j < 4} {incr j} {
      set ch0 [expr 4 * $i + $j]
      set ch1 [expr 4 * $i + $j + $CHANNEL1]
      if {$ch1 < 10} {
        set ch1 " $ch1"
      }
      checkbutton $w.mid.b$ch0 -text $ch1 -variable ChanMap($ch0)
      pack $w.mid.b$ch0 -in $w.mid.m$i -side top -expand 1 -fill both
    }
  }

  pack $w.bot.ok $w.bot.cancel -side left -padx 2m -pady 2m \
        -ipadx 1m -fill x -expand 1

  tkwait visibility $w
  grab set $w

  tkwait variable foo

  if {$foo == 1} {
    set newmap ""
    for {set i 0} {$i < 16} {incr i} {
      if {$ChanMap($i) == 0} {
        lappend newmap $i
      }
    }
    set DEVTAB($CurDev,map) $newmap
  }

  destroy $w
}

# ------------------------------------------------------------------
# Similar but with a parameter rather than a button number.
# FIX ME I need a Cancel button too.
#
proc getEntry {w parent text bitmap varname default width xoffset yoffset} {
  global getval foo
  if {$default != ""} {
    set getval $default
  } else {
    set getval 1.0
  }
  if {$width == ""}   { set width 5 }
  if {$xoffset == ""} { set xoffset 0 }
  if {$yoffset == ""} { set yoffset 0 }
  toplevel $w -class Dialog
  wm transient $w $parent
  set x [expr [winfo x $parent]+80+$xoffset]
  set y [expr [winfo y $parent]+60+$yoffset]
  wm geometry $w "+$x+$y"
  frame $w.top -relief raised -bd 1
  pack $w.top -side top -fill both
  frame $w.bot -relief raised -bd 1
  pack $w.bot -side bottom -fill both
  message $w.top.message -width 3i -text $text
  pack $w.top.message -side right -expand 1 -fill both \
    -padx 3m -pady 3m
  if {$bitmap != ""} {
    label $w.top.bitmap -bitmap $bitmap
    pack $w.top.bitmap -side left -padx 3m -pady 3m
  }
  label $w.bot.lval -text "$varname"
  entry $w.bot.val -width $width -relief sunken -bd 1 -textvariable getval
  button $w.bot.ok -text " OK " -command "update idletasks; set foo 1"
  pack $w.bot.lval $w.bot.val $w.bot.ok -side left -padx 3m -pady 3m \
        -ipadx 2m -ipady 1m -expand 1
  bind $w.bot.val <Return> {set foo 1}

  tkwait visibility $w
  grab set $w
  focus $w.bot.val

  tkwait variable foo
  destroy $w
  return $getval
}

# ---------------------------------------------------------------------
# The best things in life vary on a log scale ;-)
#
proc getLogScale {w parent text bitmap varname default help} {
  global getval slideval foo
  if {$default != ""} {
    if {$default > 100.0} { set default 100.0 }
    if {$default < 0.010} { set default 0.010 }
    set getval $default
  } else {
    set getval 1.0
  }
  set slideval [expr round(log10($getval)*100)]
  toplevel $w -class Dialog
  wm transient $w $parent
  set x [expr [winfo x $parent]+80]
  set y [expr [winfo y $parent]+60]
  wm geometry $w "+$x+$y"
  frame $w.top -relief raised -bd 1
  frame $w.mid -relief sunken -bd 1
  frame $w.bot -relief raised -bd 1
  pack $w.top $w.mid $w.bot -side top -fill both
  message $w.top.message -width 3i -text $text
  pack $w.top.message -side right -expand 1 -fill both \
    -padx 3m -pady 3m
  if {$bitmap != ""} {
    label $w.top.bitmap -bitmap $bitmap
    pack $w.top.bitmap -side left -padx 3m -pady 3m
  }
  if {$varname == ""} {
    scale $w.mid.scale -showvalue 0 -orient horizontal \
      -from -200 -to 200 -command logScale
  } else {
    scale $w.mid.scale -showvalue 0 -orient horizontal \
      -from -200 -to 200 -command logScale -label $varname
  }
  entry $w.mid.val -width 3 -relief sunken -bd 2 -textvariable getval
  $w.mid.scale set $slideval
  button $w.bot.ok -text " OK " -command "update idletasks; set foo 1" 

  pack $w.mid.scale \
    -side left -padx 2m -pady 1m \
    -expand 1 -fill both
  pack $w.mid.val -side right -padx 2m -pady 2m -ipady 0.15m \
    -expand 1 -fill x
  if {$help != ""} {
    button $w.bot.help -text Help -command "$help"
    pack $w.bot.ok $w.bot.help -side left -padx 3m -pady 1m \
      -ipadx 2m -expand 1 -fill x
  } else {
    pack $w.bot.ok -padx 3m -pady 1m -ipadx 2m
  }
  bind $w.mid.val <Return> {set foo 1}

  tkwait visibility $w
  grab set $w
  focus $w.mid.val

  tkwait variable foo
  destroy $w
  return $getval
}
# ---------------------------------------------------------------------
# Linear is useful too
#
proc getLinearScale {w parent text bitmap varname default help} {
  global getval slideval foo
  if {$default != ""} {
    if {$default > 50} { set default 50 }
    if {$default < -50} { set default -50 }
    set getval $default
  } else {
    set getval 0
  }
  set slideval $getval
  toplevel $w -class Dialog
  wm transient $w $parent
  set x [expr [winfo x $parent]+80]
  set y [expr [winfo y $parent]+60]
  wm geometry $w "+$x+$y"
  frame $w.top -relief raised -bd 1
  frame $w.mid -relief sunken -bd 1
  frame $w.bot -relief raised -bd 1
  pack $w.top $w.mid $w.bot -side top -fill both
  message $w.top.message -width 3i -text $text
  pack $w.top.message -side right -expand 1 -fill both \
    -padx 3m -pady 3m
  if {$bitmap != ""} {
    label $w.top.bitmap -bitmap $bitmap
    pack $w.top.bitmap -side left -padx 3m -pady 3m
  }
  if {$varname == ""} {
    scale $w.mid.scale -showvalue 0 -orient horizontal \
      -from -50 -to 50 -command linearScale
  } else {
    scale $w.mid.scale -showvalue 0 -orient horizontal \
      -from -50 -to 50 -command linearScale -label $varname
  }
  entry $w.mid.val -width 3 -relief sunken -bd 2 -textvariable getval
  $w.mid.scale set $slideval
  button $w.bot.ok -text " OK " -command "update idletasks; set foo 1" 

  pack $w.mid.scale \
    -side left -padx 2m -pady 1m \
    -expand 1 -fill both
  pack $w.mid.val -side right -padx 2m -pady 2m -ipady 0.15m \
    -expand 1 -fill x
  if {$help != ""} {
    button $w.bot.help -text Help -command "$help"
    pack $w.bot.ok $w.bot.help -side left -padx 3m -pady 1m \
      -ipadx 2m -expand 1 -fill x
  } else {
    pack $w.bot.ok -padx 3m -pady 1m -ipadx 2m
  }
  bind $w.mid.val <Return> {set foo 1}

  tkwait visibility $w
  grab set $w
  focus $w.mid.val

  tkwait variable foo
  destroy $w
  return $getval
}

proc logScale {value} {
  global getval
  set getval [expr pow(10.0000,$value/100.0000)]
}

proc linearScale {value} {
  global getval
  set getval $value
}

# ---------------------------------------------------------------------
#
proc displayText {w title width height text} {
  global TEAROFF

  toplevel $w
  wm title $w $title
  wm minsize $w 1 1
  set x [expr [winfo x .]+200]
  set y [expr [winfo y .]-100] ; if {$y < 0} {set y 10}
  wm geometry $w "+$x+$y"
  frame $w.mbar -relief raised -bd 1
  pack $w.mbar -side top -fill x
  menubutton $w.mbar.file -text File -underline 0 \
    -menu $w.mbar.file.m
  pack $w.mbar.file -side left
  menu $w.mbar.file.m -tearoff $TEAROFF
  $w.mbar.file.m add command -label Dismiss -command "destroy $w"
  text $w.text -width $width -height $height -wrap word \
    -yscrollcommand "$w.scroll set" -setgrid 1
  scrollbar $w.scroll -command "$w.text yview"
  pack $w.text -side left -expand 1 -fill both
  pack $w.scroll -side right -fill y
  $w.text insert end $text
  $w.text configure -state disabled
}

# ------------------------------------------------------------------------ 
# FIX ME I'm not very general yet
#
proc shellCmd {title width height cmdlist} {
  global TEAROFF

  if {! [winfo exists .sh]} {
    toplevel .sh
    wm title .sh $title
    wm minsize .sh 1 1
    text .sh.text -setgrid 1 -yscrollcommand ".sh.scroll set" \
      -width $width -height $height
    scrollbar .sh.scroll -command ".sh.text yview"
    frame .sh.mbar -relief raised -bd 1
    pack .sh.mbar -side top -fill x
    menubutton .sh.mbar.file -text File -underline 0 \
      -menu .sh.mbar.file.m
    pack .sh.mbar.file -side left
    menu .sh.mbar.file.m -tearoff $TEAROFF
      .sh.mbar.file.m add command -label Dismiss -command "destroy .sh"
    pack .sh.text -side left 
    pack .sh.scroll -side right -fill y
  } else {
    .sh.text insert end "\n\n"
    .sh.text yview moveto 1.0
  }
  .sh.text insert end "Running midi2tex -> tex -> xdvi.. watch for errors.\n"
  .sh.text insert end "Be warned that this only works for small parts!\n"
  update idletasks

  foreach cmd $cmdlist {
     .sh.text insert end "[exec sh -c $cmd]"
     .sh.text yview moveto 1.0
  }
}

# ---------------------------------------------------------------------
# This is a real hack, but allows me to disable selection in a listbox
# without crafting a new widget.
#
proc disableSelect {t} {
  bindtags $t $t
}  

# The mididevice command is not enough.  Maybe the module isn't loaded
proc hasMidi {} {
  global NUMDEV DEVTAB MidiStatus

  set i 0
  set done 0
  set retry 0
  set NUMDEV 0

  # first check for old version of tclmidi.. this is a hack
  if {![catch {miditime smf}]} {
    dialog .h . \
    "You must upgrade to tclmidi-3.0 or above\n     in order to use this version of tkseq." \
     error 0 OK
    exit
  }

  # is MIDI support compiled into tclmidi?
  if {[mididevice]} {
    # Find all devices
    while {! $done} {
      set done [catch {set DEVTAB($i,dev) [mididevice /dev/midi$i]}]
      if {$done} {
        # we *DO* need at least one device available
        if {$i == 0} {
          set msg \
          "Cannot open device /dev/midi$i...\nRealtime controls may not work."
          set done [dialog .h . "$msg" info 0 Retry Disable]
        }
      } else {
        set DEVTAB($i,name) "/dev/midi$i"
        set DEVTAB($i,raw) [mididevice /dev/rmidi$i]
	set DEVTAB($i,map) {}
        incr i
      }
    }    
    set NUMDEV $i
  } else {
    if {$MIDIDEV != ""} {
      set msg "No MIDI hardware support.\nRealtime controls disabled."
      dialog .h . "$msg" error 0 OK
    }
  }
  return [expr $NUMDEV > 0]
}

# -------------BEGIN Main ---------------------------------------------
#
# first time through we need to initialize the play and record pointers
set TKS_VERSION 0.94d
set PlayFile ""
set RecFile ""
set TmpFile ""

if {$tk_version < 4.0} {
  dialog .h . "This version of TkSeq\nrequires Tk 4.0 or better." error 0 OK
  exit 0
}

midiCleanSlate 1
drawMainWindow
midiCleanSlate 0

watchCursor .

if {$argc == 1} {
  set PlayName $argv
  fileReadMidi
} else {
  if {$HAVE_VOXWARE_GUS && $GUS_AUTO_LOAD} {
    loadGusPatch $PatchList $PatchFile
  }
} 
if {$HAVE_VOXWARE_GUS && $GUS_AUTO_THRU} {
  setGusThru 1
}

normalCursor .

# -------------END Main -----------------------------------------------

