#include "snd.h"
#include <X11/cursorfont.h>

/* TODO:
     play just mix input taking mix console into account (currently handles speed and chan[0] amp)
     edit history side back to clm note list ('script')
     any change to a given mix console (undo/redo...) should clear the associated groups' tempfiles
*/

static void allocate_erase_grf_points(mix_context *ms)
{
  if (ms->p0 == NULL)
    {
      ms->p0 = (XPoint *)calloc(POINT_BUFFER_SIZE,sizeof(XPoint));
      ms->p1 = (XPoint *)calloc(POINT_BUFFER_SIZE,sizeof(XPoint));
    }
}

static void backup_erase_grf_points(mix_context *ms, int nj)
{
  int i;
  XPoint *points,*points1;
  points = points_address(0);
  points1 = points_address(1);
  ms->lastpj = nj;
  for (i=0;i<nj;i++)
    {
      ms->p0[i] = points[i];
      ms->p1[i] = points1[i];
    }
}

void mix_save_graph(snd_state *ss, mix_context *ms,int j)
{
  if (ss->movies)
    {
      allocate_erase_grf_points(ms);
      backup_erase_grf_points(ms,j);
    }
}

void erase_and_draw_grf_points(snd_state *ss, mix_context *ms, chan_info *cp, int nj)
{
  int i,j,min,previous_j;
  chan_context *cx;
  axis_context *ax;
  XPoint *points;
  points = points_address(0);
  previous_j = ms->lastpj;
  cx = cp->tcgx;
  if (!cx) cx = cp->cgx;
  ax = cx->ax;
  min = ((nj < previous_j) ? nj : previous_j);
  if (ss->graph_style == graph_lines)
    {
      for (i=0,j=1;i<min-1;i++,j++)
	{
	  XDrawLine(ax->dp,ax->wn,cx->igc,ms->p0[i].x,ms->p0[i].y,ms->p0[j].x,ms->p0[j].y);
	  XDrawLine(ax->dp,ax->wn,cx->gc,points[i].x,points[i].y,points[j].x,points[j].y);
	}
      if (nj > previous_j)
	{
	  for (i=min-1;i<nj-1;i++) XDrawLine(ax->dp,ax->wn,cx->gc,points[i].x,points[i].y,points[i+1].x,points[i+1].y);
	}
      else
	{
	  if (previous_j > nj)
	    {
	      for (i=min-1;i<previous_j-1;i++) XDrawLine(ax->dp,ax->wn,cx->igc,ms->p0[i].x,ms->p0[i].y,ms->p0[i+1].x,ms->p0[i+1].y);
	    }
	}
    }
  else /* dots */
    {
      for (i=0;i<min;i++)
	{
	  draw_point(ax->dp,ax->wn,cx->igc,ms->p0[i],ss->dot_size);
	  draw_point(ax->dp,ax->wn,cx->gc,points[i],ss->dot_size);
	}
      if (nj > previous_j)
	{
	  for (i=min;i<nj;i++) draw_point(ax->dp,ax->wn,cx->gc,points[i],ss->dot_size);
	}
      else
	{
	  if (previous_j > nj)
	    {
	      for (i=min;i<previous_j;i++) draw_point(ax->dp,ax->wn,cx->igc,ms->p0[i],ss->dot_size);
	    }
	}
    }
  backup_erase_grf_points(ms,nj);
}

void erase_and_draw_both_grf_points(snd_state *ss, mix_context *ms, chan_info *cp, int nj)
{
  int i,j,min,previous_j;
  chan_context *cx;
  axis_context *ax;
  XPoint *points,*points1;
  points = points_address(0);
  points1 = points_address(1);
  previous_j = ms->lastpj;
  cx = cp->tcgx;
  if (!cx) cx = cp->cgx;
  ax = cx->ax;
  min = ((nj < previous_j) ? nj : previous_j);
  if (ss->graph_style == graph_lines)
    {
      for (i=0,j=1;i<min-1;i++,j++)
	{
	  XDrawLine(ax->dp,ax->wn,cx->igc,ms->p0[i].x,ms->p0[i].y,ms->p0[j].x,ms->p0[j].y);
	  XDrawLine(ax->dp,ax->wn,cx->gc,points[i].x,points[i].y,points[j].x,points[j].y);
	  XDrawLine(ax->dp,ax->wn,cx->igc,ms->p1[i].x,ms->p1[i].y,ms->p1[j].x,ms->p1[j].y);
	  XDrawLine(ax->dp,ax->wn,cx->gc,points1[i].x,points1[i].y,points1[j].x,points1[j].y);
	}
      if (nj > previous_j)
	{
	  for (i=min-1;i<nj-1;i++) 
	    {
	      XDrawLine(ax->dp,ax->wn,cx->gc,points[i].x,points[i].y,points[i+1].x,points[i+1].y);
	      XDrawLine(ax->dp,ax->wn,cx->gc,points1[i].x,points1[i].y,points1[i+1].x,points1[i+1].y);
	    }
	}
      else
	if (previous_j > nj)
	  {
	    for (i=min-1;i<previous_j-1;i++) 
	      {
		XDrawLine(ax->dp,ax->wn,cx->igc,ms->p0[i].x,ms->p0[i].y,ms->p0[i+1].x,ms->p0[i+1].y);
		XDrawLine(ax->dp,ax->wn,cx->igc,ms->p1[i].x,ms->p1[i].y,ms->p1[i+1].x,ms->p1[i+1].y);
	      }
	  }
    }
  else /* dots */
    {
      for (i=0;i<min;i++)
	{
	  draw_point(ax->dp,ax->wn,cx->igc,ms->p0[i],ss->dot_size);
	  draw_point(ax->dp,ax->wn,cx->gc,points[i],ss->dot_size);
	  draw_point(ax->dp,ax->wn,cx->igc,ms->p1[i],ss->dot_size);
	  draw_point(ax->dp,ax->wn,cx->gc,points1[i],ss->dot_size);
	}
      if (nj > previous_j)
	{
	  for (i=min;i<nj;i++) 
	    {
	      draw_point(ax->dp,ax->wn,cx->gc,points[i],ss->dot_size);
	      draw_point(ax->dp,ax->wn,cx->gc,points1[i],ss->dot_size);
	    }
	}
      else
	{
	  if (previous_j > nj)
	    {
	      for (i=min;i<previous_j;i++) 
		{
		  draw_point(ax->dp,ax->wn,cx->igc,ms->p0[i],ss->dot_size);
		  draw_point(ax->dp,ax->wn,cx->igc,ms->p1[i],ss->dot_size);
		}
	    }
	}

    }
  backup_erase_grf_points(ms,nj);
}


/* ---------------- MIX CURSOR ---------------- */

static Cursor mix_cursor;

static void make_mix_cursor(snd_state *ss)
{
  mix_cursor = XCreateFontCursor(XtDisplay(main_SHELL(ss)),XC_left_ptr);
}

static void mix_mouse_enter(Widget w, XtPointer clientData, XEvent *event, Boolean *flag)
{
  XDefineCursor(XtDisplay(w),XtWindow(w),mix_cursor);
}

static void mix_mouse_leave(Widget w, XtPointer clientData, XEvent *event, Boolean *flag)
{
  XUndefineCursor(XtDisplay(w),XtWindow(w));
}


/* ---------------- MIX ICONS ---------------- 
 *
 * bitmaps for play (a speaker), close (an x), open/minify boxes, '?', and so on
 */

#define p_speaker_width 12
#define p_speaker_height 12
static unsigned char p_speaker_bits[] = {
   0x00, 0x07, 0xc0, 0x04, 0x30, 0x04, 0x0e, 0x04, 0x06, 0x04, 0x06, 0x04,
   0x06, 0x04, 0x06, 0x04, 0x0e, 0x04, 0x30, 0x04, 0xc0, 0x04, 0x00, 0x07};

#define p_cross_width 12
#define p_cross_height 12
static unsigned char p_cross_bits[] = {
   0x00, 0x00, 0x02, 0x04, 0x04, 0x02, 0x08, 0x01, 0x90, 0x00, 0x60, 0x00,
   0x60, 0x00, 0x90, 0x00, 0x08, 0x01, 0x04, 0x02, 0x02, 0x04, 0x00, 0x00};

/* an open box (maxify icon -- name "mixer" is out-of-date) */
#define p_mixer_width 12
#define p_mixer_height 12
static unsigned char p_mixer_bits[] = {
   0x00, 0x00, 0xfe, 0x07, 0x02, 0x06, 0x02, 0x06, 0x02, 0x06, 0x02, 0x06,
   0x02, 0x06, 0x02, 0x06, 0x02, 0x06, 0xfe, 0x07, 0xfc, 0x07, 0x00, 0x00};

#define p_mini_width 12
#define p_mini_height 12
static unsigned char p_mini_bits[] = {
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x01, 0x90, 0x01,
   0xf0, 0x01, 0xf0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

static Pixmap mixer_r, cross_r, speaker_r, mini_r;
static int icons_created = 0;

Pixmap make_pixmap(snd_state *ss, unsigned char *bits, int width, int height, int depth, GC gc)
{
  Pixmap rb,nr;
  state_context *sx;
  sx = ss->sgx;
  rb = XCreateBitmapFromData(sx->mdpy,RootWindowOfScreen(XtScreen(sx->mainpane)),bits,width,height);
  nr = XCreatePixmap(sx->mdpy,RootWindowOfScreen(XtScreen(sx->mainpane)),width,height,depth);
  XCopyPlane(sx->mdpy,rb,nr,gc,0,0,width,height,0,0,1);
  XFreePixmap(sx->mdpy,rb);
  return(nr);
}

static void create_icons(Widget w, snd_state *ss)
{
  XGCValues v;
  GC gc;
  int depth;
  icons_created = 1;

  XtVaGetValues(w,XmNforeground,&v.foreground,XmNbackground,&v.background,XmNdepth,&depth,NULL);
  gc = XtGetGC(w,GCForeground | GCBackground,&v);

  mixer_r = make_pixmap(ss,p_mixer_bits,p_mixer_width,p_mixer_height,depth,gc);
  cross_r = make_pixmap(ss,p_cross_bits,p_cross_width,p_cross_height,depth,gc);
  speaker_r = make_pixmap(ss,p_speaker_bits,p_speaker_width,p_speaker_height,depth,gc);
  mini_r = make_pixmap(ss,p_mini_bits,p_mini_width,p_mini_height,depth,gc);
}



/* ---------------- WIDGET POOL ----------------
 *
 * the widgets used to handle mix-marks are independent of the mix-data structs;
 * off-screen mix data has no associated widget; on-screen mixes have a
 * widget while they remain on screen, and remain unsaved (active);  the 
 * mix-mark widget pool is local to a given channel.  The intent here
 * is to minimize widgets as far as possible.
 *
 * It would be better if these widget pools were global across channels, but
 * that requires either a way to change the main widget's parent widget (i.e.
 * move it to another channel graph's tree), or the use of dialog widgets
 * which means much more difficult placement decisions and display code.
 *
 * The chan_info mix's pointer points to a controlling struct that holds
 * the pointer to the mixer-widget pool and the list of active mixes.
 *
 * Due to unforeseen widget resizing woes, the consoles are also sorted by 
 * in_chans (so that the console need not actually resize itself vertically).
 */

enum {mm_main,           /* form holds console, catches mouse acts (click, drag) and cursor motion */
      mm_fmain,
      mm_title,          /* top row of widgets */
      mm_name,
      mm_beg,
      mm_play,           /* togglebutton, click to play "solo" or to stop */
      mm_close,          /* pushbutton; if clicked, remove mini-console altogether (no undo) */
      mm_open,           /* pushbutton; if just title, open to full console, else close to title row */
      mm_console,        /* holds the rest of the console under the title bar */
      mm_title_sep,      /* if open, horizontal sep below title */
      mm_amp,            /* the "amp:" push button */
      mm_speed,          /* if open, srate control */
      mm_speed_label,
      mm_spdscl,
      mm_spdsep,
      mm_chans           /* if open, start of controls (per input channel) */
};         

#define mm_amp_label 0
#define mm_scl 1
#define MIX_CHAN_SIZE 2

#define title_row_start mm_beg
#define title_row_end mm_open


void release_mixmark_widgets(mixmark *m)
{
  if ((m->w) && ((m->w[mm_main]) && (XtIsManaged(m->w[mm_main])))) 
    {
      XtUnmanageChild(m->w[mm_main]);
      m->active = 0;
    }
}

static void activate_mixmark_widgets(mixmark *m)
{
  if ((m->w) && ((m->w[mm_main]) && (!XtIsManaged(m->w[mm_main])))) 
    {
      XtManageChild(m->w[mm_main]);
      m->active = 1;
    }
}

mix_context *set_mixdata_context(chan_info *cp)
{
  mix_context *g;
  snd_info *sp;
  sp = cp->sound;
  if (sp->combining != CHANNELS_SEPARATE) cp=sp->chans[0];
  g = (mix_context *)calloc(1,sizeof(mix_context));
  g->graph = chan_widget(cp,W_chn_graph);
  return(g);
}

static int mix_initialized = 0;

void initialize_mix(snd_state *ss)
{
  if (!mix_initialized)
    {
      mix_initialized = 1;
      if (!mix_cursor) make_mix_cursor(ss);
    }
}


/* ---------------- MIX CONSOLE CALLBACKS ---------------- */

void toggle_group(mixdata *md, int on, int button)
{
  console_state *cs,*ocs;
  mixmark *m;
  snd_state *ss;
  cs = md->current_cs;
  if (on) cs->groups |= (1<<button); else cs->groups &= (~(1<<button));
  ocs = md->states[md->curcons]; 
  ocs->groups = cs->groups;
  m = md->mixer;
  if (m)
    {
      ss = md->ss;
      XmToggleButtonSetState(m->w[mm_chans + md->in_chans*MIX_CHAN_SIZE + (ss->ngroups - button - 1)],on,FALSE);
    }
  notify_group(md,button,on);
  remix_file(md);
}

static void group_button_callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  XmToggleButtonCallbackStruct *cb = (XmToggleButtonCallbackStruct *)callData; /* cb->set = button state */
  mixmark *m = (mixmark *)clientData;
  mixdata *md;
  int button;
  md = m->owner;
  XtVaGetValues(w,XmNuserData,&button,NULL);
  toggle_group(md,cb->set,button);
  toggle_syncd_group(md,cb->set,button); /* calls toggle_group */
}

static snd_info *mix_play_sp = NULL;

static void mix_console_start_play_callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  /* a push button -- plays as long as the button is held down */
  /* use sync button to decide whether to play all chans or current chan */
  mixmark *m = (mixmark *)clientData;
  mixdata *md;
  console_state *cs;
  snd_info *sp,*rsp;
  chan_info *cp;
  md = m->owner;
  cs = md->current_cs;
  cp = md->cp;
  sp = cp->sound;
  select_channel(sp,cp->chan);
  if (!(md->add_snd)) md->add_snd = make_mix_readable(md);
  rsp = md->add_snd;
  mix_play_sp = rsp;
  rsp->srate = cs->speed;
  rsp->amp = cs->scalers[0]; /* TODO: should be by channel! */
  start_playing(rsp,0);
}

static void mix_console_stop_play_callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  if ((mix_play_sp) && (mix_play_sp->playing)) 
    {
      stop_playing(mix_play_sp->playing); 
      mix_play_sp->playing = 0;
      mix_play_sp = NULL;
    }
}

static void mix_console_close_callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  /* a push button -- remove console associated with mix (i.e. make mix unchangeable) */
  /* this is not undo-able */
  mixmark *m = (mixmark *)clientData;
  mixdata *md;
  int loc;
  chan_info *cp;
  mix_state *ms;
  md = m->owner;
  cp = md->cp;
  select_channel(cp->sound,cp->chan);
  ms = cp->mixes;
  loc = md->loc; /* in linux the free call happens before the array reference! */
  /* now we've worked our way all the way back to the mixdata pool... */
  ms->mix_pool[loc] = free_mixdata(md);
}

static int current_mix_x = 0;

/* basically copied from snd-xsnd snd_amp_changed with different ranges since the widgets are smaller in this case */
static char ampbuf[5]={'1',snd_string_decimal,'0','0','\0'};
static char srcbuf[5]={'1',snd_string_decimal,'0','0','\0'};

static float mix_amp_scaler = 1.0;
static float mix_speed_scaler = 1.0;
static float mix_tempo_scaler = 1.0;
void set_mix_amp_scaler(float amp) {mix_amp_scaler = amp;}
float get_mix_amp_scaler(void) {return(mix_amp_scaler);}
void set_mix_speed_scaler(float amp) {mix_speed_scaler = amp;}
float get_mix_speed_scaler(void) {return(mix_speed_scaler);}
void set_mix_tempo_scaler(float amp) {mix_tempo_scaler = amp;}
float get_mix_tempo_scaler(void) {return(mix_tempo_scaler);}

static float amp_int_to_float(int val) {if (val == 0) return(0.0); else return(exp((float)((val-50)*mix_amp_scaler)/20.0));}
static int amp_float_to_int(float scl_amp) {if (scl_amp == 0.0) return(0); else return(50.5 + 20.0*log(scl_amp)/mix_amp_scaler);}

static float speed_int_to_float(int val) {return(exp((float)(val-50)*mix_speed_scaler/20.0));}
static int speed_float_to_int(float spd) {return(50.5+20*log(spd)/mix_speed_scaler);}

static float tempo_int_to_float(int val) {return(exp((float)(val-50)*mix_tempo_scaler/20.0));}
static int tempo_float_to_int(float spd) {return(50.5+20*log(spd)/mix_tempo_scaler);}
/* a bigger "scaler" increases the exponent, increasing the scale range */

static float change_amp_label(Widget w, float amp)
{
  char *sfs;
  sfs=prettyf(amp,2);
  fill_number(sfs,ampbuf);
  make_name_label(w,ampbuf);
  return(amp);
}

static void reflect_mix_amp(Widget scl, Widget lab, float scl_amp, float amp)
{
  change_amp_label(lab,amp);
  XmScaleSetValue(scl,amp_float_to_int(scl_amp));
}

static float change_speed_label(Widget w, snd_state *ss, float true_speed)
{
  /* unmodified speed incoming -- cs->scl_speed * cs->gs_speed for example */
  /* return quantized speed (cs->speed normally) */
  float spd;
  spd = ur_srate_changed(true_speed,srcbuf,ss->speed_style,ss->speed_tones); 
  make_name_label(w,srcbuf);
  return(spd);
}

static float reflect_mix_speed(Widget scl, Widget lab, snd_state *ss, float true_speed, int scl_spd)
{
  /* true_speed as above, scl_spd = int version of scl_speed (old_speed normally) */
  XmScaleSetValue(scl,scl_spd);
  return(change_speed_label(lab,ss,true_speed));
}

void set_mix_speed(mixdata *md, console_state *cs)
{
  mixmark *m;
  snd_state *ss;
  ss = md->ss;
  m = md->mixer;
  if ((m) && (m->active)) 
    {
      cs->speed = reflect_mix_speed(m->w[mm_spdscl],m->w[mm_speed_label],md->ss,cs->scl_speed * cs->gs_speed,cs->old_speed);
      set_mix_title_beg(md,m);
    }
  else cs->speed = ur_srate_changed(cs->scl_speed * cs->gs_speed,srcbuf,ss->speed_style,ss->speed_tones); 
}

static void change_tempo_label(Widget w, float true_tempo)
{
  ur_srate_changed(true_tempo,srcbuf,SPEED_AS_FLOAT,0);
  make_name_label(w,srcbuf);
}

static void reflect_mix_tempo(Widget scl, Widget lab, float true_tempo)
{
  XmScaleSetValue(scl,tempo_float_to_int(true_tempo));
  change_tempo_label(lab,true_tempo);
}

void set_mix_title_beg(mixdata *md, mixmark *m)
{
  console_state *cs;
  XmString s1;
  char *str;
  if (m->state != MD_M)
    {
      cs = md->current_cs;
      str = (char *)calloc(32,sizeof(char));
      if (md->beg_in_samps)
	sprintf(str,"%d : %d",cs->beg,cs->beg+cs->len);
      else sprintf(str,"%.3f : %.3f",(float)(cs->beg)/snd_SRATE(md->cp),(float)(cs->beg+cs->len)/snd_SRATE(md->cp));
      s1=XmStringCreate(str,"button_font");
      XtVaSetValues(m->w[mm_beg],XmNlabelString,s1,NULL);
      XmStringFree(s1);
      free(str);
    }
}

static void beg_click_callback(Widget w,XtPointer clientData,XtPointer callData) 
{
   mixmark *m = (mixmark *)clientData;
   mixdata *md;
   md = m->owner;
   md->beg_in_samps = (!(md->beg_in_samps));
   set_mix_title_beg(md,m);
}

static void amp_click_callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  /* 1 click: set to 1 and 0's ("identity"), 2 click: mute (all 0's) */
  /* cntrl-click once to reset to previous saved state */
  XmPushButtonCallbackStruct *cb = (XmPushButtonCallbackStruct *)callData;
  mixmark *m = (mixmark *)clientData;
  mixdata *md;
  int chan;
  console_state *cs;
  chan_info *cp;
  int i,j;
  XButtonEvent *ev;
  ev = (XButtonEvent *)(cb->event);
  md = m->owner;
  cp = md->cp;
  chan = cp->chan;
  cs = md->current_cs;
  if (cb->click_count == 1)
    {
      for (i=0,j=mm_chans;i<md->in_chans;i++,j+=MIX_CHAN_SIZE) 
	{
	  if (ev->state & (snd_ControlMask | snd_MetaMask))
	    cs->scl_scalers[i] = cs->old_scalers[i];
	  else 
	    {
	      if (i != md->main_chan) 
		cs->scl_scalers[i] = 0.0; 
	      else cs->scl_scalers[i] = 1.0;
	    }
	  cs->scalers[i] = cs->scl_scalers[i] * cs->gs_amp;
	  reflect_mix_amp(m->w[j+mm_scl],m->w[j+mm_amp_label],cs->scl_scalers[i],cs->scalers[i]);
	}
    }
  else
    {
      if (cb->click_count == 2)
	{
	  /* can't get here without going through click==1, so just reset chan from callData */
	  if (md->main_chan >= 0) 
	    {
	      cs->scl_scalers[md->main_chan] = 0.0;
	      cs->scalers[md->main_chan] = 0.0;
	      reflect_mix_amp(m->w[mm_chans+mm_scl+md->main_chan*MIX_CHAN_SIZE],m->w[mm_chans+mm_amp_label+md->main_chan*MIX_CHAN_SIZE],0.0,0.0);
	    }
	}
    }
  select_channel(cp->sound,chan);
  remix_file(md);
}

void reamp(mixdata *md, int chan, float amp)
{
  console_state *cs;
  mixmark *m;
  cs = md->current_cs;
  m = md->mixer;
  cs->scl_scalers[chan] = amp;
  cs->scalers[chan] = amp;
  if (m) reflect_mix_amp(m->w[mm_chans+mm_scl+chan*MIX_CHAN_SIZE],m->w[mm_chans+mm_amp_label+chan*MIX_CHAN_SIZE],amp,amp);
}

void respeed(mixdata *md, float spd)
{ /* used in syncd mixes and (setf (mix-speed)) */
  console_state *cs;
  mixmark *m;
  snd_state *ss;
  ss = md->ss;
  cs = md->current_cs;
  m = md->mixer;
  cs->scl_speed = spd;
  if (m)
    cs->speed = reflect_mix_speed(m->w[mm_spdscl],m->w[mm_speed_label],md->ss,cs->scl_speed * cs->gs_speed,speed_float_to_int(spd));
  else cs->speed = ur_srate_changed(spd,srcbuf,ss->speed_style,ss->speed_tones); 
  cs->len = ceil(md->in_samps / cs->speed);
  if (m) set_mix_title_beg(md,m);
}

static void speed_click_callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  /* set speed to 1 (control-click for reset to previous?) */
  /* speed is like motion in that it assumes cross-sync linkages whereas amp does not */
  XmPushButtonCallbackStruct *cb = (XmPushButtonCallbackStruct *)callData;
  mixmark *m = (mixmark *)clientData;
  mixdata *md;
  console_state *cs;
  XButtonEvent *ev;
  int val;
  chan_info *cp;
  ev = (XButtonEvent *)(cb->event);
  md = m->owner;
  cs = md->current_cs;
  if (ev->state & (snd_ControlMask | snd_MetaMask))
    {
      cs->scl_speed = speed_int_to_float(cs->old_speed);
      val = cs->old_speed;
    }
  else
    {
      cs->scl_speed = 1.0;
      val = 50;
    }
  cs->speed = reflect_mix_speed(m->w[mm_spdscl],m->w[mm_speed_label],md->ss,cs->scl_speed * cs->gs_speed,val);
  cs->len = ceil(md->in_samps / cs->speed);
  set_mix_title_beg(md,m);
  cp = md->cp;
  select_channel(cp->sound,cp->chan);
  speed_sync_chain(md,cs->scl_speed,val);
  if (cs->groups) update_groups_and_envs(md);
  remix_file(md);
}

static void m_amp_drag_callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  /* amp slider drag */
  console_state *cs;
  chan_info *cp;
  mixdata *md;
  mix_context *ms;
  int chan;
  mixmark *m = (mixmark *)clientData;
  XmScaleCallbackStruct *cb = (XmScaleCallbackStruct *)callData;
  md = m->owner;
  ms = md->wg;
  cp = md->cp;
  /* select_channel(cp->sound,cp->chan); */
  XtVaGetValues(w,XmNuserData,&chan,NULL);
  cs = md->current_cs;
  cs->scl_scalers[chan] = amp_int_to_float(cb->value);
  cs->scalers[chan] = cs->scl_scalers[chan] * cs->gs_amp;
  change_amp_label(m->w[mm_chans + mm_amp_label + chan*MIX_CHAN_SIZE],cs->scalers[chan]);
  if (!(ms->lastpj)) {ms->lastpj = make_graph(cp,cp->sound,cp->state); mix_save_graph(md->ss,md->wg,ms->lastpj);}
  make_temporary_graph(cp,md,cs);
}

static void m_amp_value_changed_callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  /* amp slider button release */
  int chan;
  mixdata *md;
  mix_context *ms;
  console_state *cs;
  chan_info *cp;
  mixmark *m = (mixmark *)clientData;
  XmScaleCallbackStruct *cb = (XmScaleCallbackStruct *)callData;
  md = m->owner;
  ms = md->wg;
  ms->lastpj = 0;
  XtVaGetValues(w,XmNuserData,&chan,NULL);
  cs = md->current_cs;
  cs->scl_scalers[chan] = amp_int_to_float(cb->value);
  cs->old_scalers[chan] = cs->scl_scalers[chan];
  cs->scalers[chan] = cs->scl_scalers[chan] * cs->gs_amp;
  change_amp_label(m->w[mm_chans + mm_amp_label + chan*MIX_CHAN_SIZE],cs->scalers[chan]);
  cp = md->cp;
  select_channel(cp->sound,cp->chan);
  remix_file(md);
}

static void m_speed_drag_callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  /* speed slider drag */
  console_state *cs;
  chan_info *cp;
  mixdata *md;
  mix_context *ms;
  int chan;
  snd_state *ss;
  mixmark *m = (mixmark *)clientData;
  XmScaleCallbackStruct *cb = (XmScaleCallbackStruct *)callData;
  md = m->owner;
  ms = md->wg;
  XtVaGetValues(w,XmNuserData,&chan,NULL);
  cs = md->current_cs;
  cp = md->cp;
  ss = md->ss;
  cs->scl_speed = speed_int_to_float(cb->value);
  cs ->speed = change_speed_label(m->w[mm_speed_label],md->ss,cs->scl_speed * cs->gs_speed);
  if (!(ms->lastpj)) {ms->lastpj = make_graph(cp,cp->sound,cp->state); mix_save_graph(md->ss,md->wg,ms->lastpj);}
  if (ss->mix_duration_brackets) erase_mix_duration_bracket(md,m->x,m->y);      
  cs->len = ceil(md->in_samps / cs->speed);
  if (cs->gs_amp_env) refigure_one_amp_env(ss,md);
  make_temporary_graph(cp,md,cs);
  set_mix_title_beg(md,m);
  if (ss->mix_duration_brackets) draw_mix_duration_bracket(md,m->x,m->y);      
  speed_or_disable_sync_chain(cp->sound,md,cs->scl_speed,cb->value);
  update_group_browser_labels(md->ss,md,cs);
}

static void m_speed_value_changed_callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  /* speed slider button release */
  int chan;
  mixdata *md;
  mix_context *ms;
  console_state *cs;
  chan_info *cp;
  mixmark *m = (mixmark *)clientData;
  XmScaleCallbackStruct *cb = (XmScaleCallbackStruct *)callData;
  md = m->owner;
  ms = md->wg;
  ms->lastpj = 0;
  XtVaGetValues(w,XmNuserData,&chan,NULL);
  cs = md->current_cs;
  cs->scl_speed = speed_int_to_float(cb->value);
  cs ->speed = change_speed_label(m->w[mm_speed_label],md->ss,cs->scl_speed * cs->gs_speed);
  cs->old_speed = cb->value;
  cs->len = ceil(md->in_samps / cs->speed);
  cp = md->cp;
  select_channel(cp->sound,cp->chan);
  speed_sync_chain(md,cs->scl_speed,cb->value);
  if (cs->groups) update_groups_and_envs(md);
  remix_file(md);
  update_group_browser_labels(md->ss,md,cs);
}



/* manage the mix console (3 states: M, title row, title+scalers) */

static void set_m(mixdata *md, mixmark *m)
{
  XmString s1;
  s1=XmStringCreate("M","button_font");
  XtVaSetValues(m->w[mm_name],XmNlabelString,s1,NULL);
  XmStringFree(s1);
}

static void set_mix_title_name(mixdata *md, mixmark *m)
{
  XmString s1;
  s1=XmStringCreate(md->name,"button_font");
  XtVaSetValues(m->w[mm_name],XmNlabelString,s1,NULL);
  XmStringFree(s1);
}

static void set_console(mixdata *md, mixmark *m)
{
  console_state *cs;
  int i,j;
  snd_state *ss;
  ss = md->ss;
  cs = md->current_cs;
  for (i=0,j=mm_chans;i<md->in_chans;i++,j+=MIX_CHAN_SIZE)
    reflect_mix_amp(m->w[j+mm_scl],m->w[j+mm_amp_label],cs->scl_scalers[i],cs->scalers[i]);
  reflect_mix_speed(m->w[mm_spdscl],m->w[mm_speed_label],ss,cs->scl_speed * cs->gs_speed,cs->old_speed);
  for (i=0,j=(ss->ngroups - 1);i<ss->ngroups;i++,j--)
    {
      XmToggleButtonSetState(m->w[mm_chans + md->in_chans*MIX_CHAN_SIZE + i],(cs->groups & (1<<j)),FALSE);
    }
}

static void open_console(mixdata *md, mixmark *m)
{
  XtVaSetValues(m->w[mm_open],XmNlabelPixmap,mini_r,NULL);
  XtManageChild(m->w[mm_console]);
}

static void close_console(mixdata *md, mixmark *m)
{
  XtUnmanageChild(m->w[mm_console]);
  XtVaSetValues(m->w[mm_open],XmNlabelPixmap,mixer_r,NULL);
}

static void open_title(mixdata *md, mixmark *m)
{
  int i;
  for (i=title_row_start;i<=title_row_end;i++) XtManageChild(m->w[i]);
}

static void close_title(mixdata *md, mixmark *m)
{
  int i;
  for (i=title_row_start;i<=title_row_end;i++) XtUnmanageChild(m->w[i]);
}

static void title_to_m(mixdata *md, mixmark *m)
{
  close_title(md,m);
  set_m(md,m);
}

static void m_to_title(mixdata *md, mixmark *m)
{
  set_mix_title_name(md,m);
  set_mix_title_beg(md,m);
  open_title(md,m);
}

static void console_to_m(mixdata *md, mixmark *m)
{
  close_console(md,m);
  close_title(md,m);
  set_m(md,m);
}

static void m_to_console(mixdata *md, mixmark *m)
{
  set_mix_title_name(md,m);
  set_mix_title_beg(md,m);
  open_title(md,m);
  set_console(md,m);
  open_console(md,m);
}

static void console_to_title(mixdata *md, mixmark *m)
{
  close_console(md,m);
  set_mix_title_name(md,m);
  set_mix_title_beg(md,m);
}

static void title_to_console(mixdata *md, mixmark *m)
{
  set_mix_title_name(md,m);
  set_mix_title_beg(md,m);
  set_console(md,m);
  open_console(md,m);
}

static void console_to_console(mixdata *md, mixmark *m)
{
  set_mix_title_name(md,m);
  set_mix_title_beg(md,m);
  set_console(md,m);
}

static void title_to_title(mixdata *md, mixmark *m)
{
  set_mix_title_name(md,m);
  set_mix_title_beg(md,m);
}

void fixup_mixmark(mixdata *md)
{
  mixmark *m;
  Dimension wid,hgt;
  m = md->mixer;
  switch (md->state)
    {
    case MD_M:
      switch (m->state)
	{
	case MD_M: break;
	case MD_TITLE: title_to_m(md,m); break;
	case MD_CS: console_to_m(md,m); break;
	}
      break;
    case MD_TITLE:
      switch (m->state)
	{
	case MD_M: m_to_title(md,m); break;
	case MD_TITLE: title_to_title(md,m); break;
	case MD_CS: console_to_title(md,m); break;
	}
      break;
    case MD_CS:
      switch (m->state)
	{
	case MD_M: m_to_console(md,m); break;
	case MD_TITLE: title_to_console(md,m); break;
	case MD_CS: console_to_console(md,m); break;
	}
      break;
    }
  m->state = md->state;
  if (m->active == 0) activate_mixmark_widgets(m);
  XtVaGetValues(m->w[mm_main],XmNwidth,&wid,XmNheight,&hgt,NULL);
  md->width = wid;
  md->height = hgt;
}

static void mix_console_help_callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  mixmark *m = (mixmark *)clientData;
  snd_state *ss;
  mixdata *md;
  md = m->owner;
  ss = md->ss;
  snd_help(ss,"Mix",get_mixing_help());
}

static void mix_console_open_callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  mixmark *m = (mixmark *)clientData;
  mixdata *md;
  chan_info *cp;
  md = m->owner;
  cp = md->cp;
  select_channel(cp->sound,cp->chan);
  if (md->state == MD_CS) md->state = MD_TITLE; else md->state = MD_CS;
  fixup_mixmark(md);
}

static int mix_dragged = 0;                /* are we dragging the mouse while inside a console */
static Time mix_down_time;                 /* click vs drag check */
static XtWorkProcId watch_mix_proc = 0;    /* work proc if mouse outside graph causing axes to move */
static int last_mix_x = 0;                 /* mouse position within console title bar (console coordinates) */

int mix_dragging(void) {return(mix_dragged);} /* snd-xchn.c */

/* for axis movement (as in mark drag off screen) */
static void move_mix(mixmark *m, int evx);

static Boolean watch_mix(XtPointer m)
{
  /* there is apparently a race condition here or something that leaves this work proc
   * running even after XtRemoveWorkProc has been called on it, so we need to
   * check watch_mix_proc and return true if it's 0.
   */
  if (watch_mix_proc)
    {
      move_mix((mixmark *)m,current_mix_x);
      return(FALSE);
    }
  else return(TRUE);
}

static int xoff,yoff;

static void mix_title_button_press(Widget w, XtPointer clientData, XEvent *event, Boolean *flag) 
{
  mixmark *m = (mixmark *)clientData;
  XButtonEvent *ev = (XButtonEvent *)event;
  Window wn;
  chan_info *cp;
  snd_state *ss;
  cp = m_to_cp(m);
  ss = cp->state;
  if (!(ss->using_schemes)) XtVaSetValues(m->w[mm_name],XmNforeground,(ss->sgx)->red,NULL);
  mix_dragged = 0;
  mix_down_time = ev->time;
  XTranslateCoordinates(XtDisplay(w),XtWindow(m->w[mm_main]),DefaultRootWindow(XtDisplay(w)),0,0,&xoff,&yoff,&wn);
  last_mix_x = ev->x_root - xoff;
  XTranslateCoordinates(XtDisplay(w),XtWindow(chan_widget(m_to_cp(m),W_chn_graph)),DefaultRootWindow(XtDisplay(w)),0,0,&xoff,&yoff,&wn);
  select_channel(cp->sound,cp->chan);
}

/* does not currently extend the base file as we push off the right (or left???) edge */
/* also what about dragging in the y direction? */


void set_mixer_location(mixmark *m, int nx)
{
  m->x = nx;
  XtVaSetValues(m->w[mm_main],XmNx,(Position)nx,NULL);
}

static void move_mix(mixmark *m, int evx)
{
  snd_state *ss;
  axis_info *ap;
  chan_info *cp;
  mixdata *md;
  console_state *cs;
  int samps,nx,x,samp,kx,updated;
  Position xx;
  Dimension len;
  updated = 0;
  cp = m_to_cp(m);
  if (!cp) return;
  ap = cp->axis;
  if (!ap) return;
  md = m->owner;
  if (!md) return;
  ss = md->ss;
  /* evx here is in root window coordinates */
  x = evx - last_mix_x - xoff; /* console left edge relative to graph */
  if ((x > ap->x_axis_x1) || (x < ap->x_axis_x0)) 
    {
      if (watch_mix_proc)
	{
	  if ((x < ap->x_axis_x0) && (ap->x0 == ap->xmin)) return;
	  if ((x > ap->x_axis_x1) && (ap->x1 == ap->xmax)) return;
	}
      nx = move_axis(cp,ap,x); /* calls update_graph eventually (in snd-chn.c reset_x_display) (and moves sync'd chans) */
      updated = 1;
      if ((mix_dragged) && (!watch_mix_proc))
	watch_mix_proc = XtAppAddWorkProc(XtWidgetToApplicationContext(main_PANE(cp)),watch_mix,(XtPointer)m);
    }
  else 
    {
      nx = x;
      if (watch_mix_proc) 
	{
	  XtRemoveWorkProc(watch_mix_proc);
	  watch_mix_proc = 0;
	}
    }
  cs = md->current_cs;
  if (m->x != nx)
    {
      if (ss->mix_duration_brackets) erase_mix_duration_bracket(md,m->x,m->y);
      kx = m->x;
      m->x = nx;
      XtVaSetValues(m->w[mm_main],XmNx,(Position)(m->x),NULL);
      /* if widget refuses to move, reset notion of last_mix_x instead */
      /* this happens when we hit the graph borders with one or the other end of the title bar */
      XtVaGetValues(m->w[mm_main],XmNx,&xx,XmNwidth,&len,NULL);
      if (xx != nx) 
	{
	  /* if we've moved off the right side, check for moving the axis */
	  m->x = xx;
	  if ((int)(nx+len) >= (int)(ap->x_axis_x1))
	    {
	      nx = move_axis(cp,ap,nx+len);
	      if (!watch_mix_proc)
		watch_mix_proc = XtAppAddWorkProc(XtWidgetToApplicationContext(main_PANE(cp)),watch_mix,(XtPointer)m);
	    }
	  else if ((xx == kx) && (!updated)) return;
	}
      samp = ungrf_x(ap,m->x) * snd_SRATE(cp);
      if (samp < 0) samp = 0;
      samps = current_ed_samples(cp);
      if (samp > samps) samp = samps;
      /* now redraw the mix and reset its notion of begin time */
      /* actually should make a new state if cp->edit_ctr has changed ?? */
      cs->beg = samp - md->anchor;
      if (ss->mix_duration_brackets) draw_mix_duration_bracket(md,m->x,m->y);
      if ((!updated) && (XtAppPending(((state_context *)(((snd_state *)(cp->state))->sgx))->mainapp))) return;
      if ((cs->groups) && (cs->beg != cs->orig))
	{
	  move_associated_mixes(md,cs->beg - cs->orig,FALSE);
	  /* TODO: fixup amp env */
	  set_mix_title_beg(md,m);
	  make_temporary_graph_over_groups(md->ss,cs->groups);
	}
      else
	{
	  /* can't easily use work proc here because the erasure gets complicated */
	  make_temporary_graph(cp,md,cs);
	  set_mix_title_beg(md,m);
	  move_or_disable_sync_chain(cp->sound,md,cs->beg,nx);
	}
    }
  else
    {
      if (updated) 
	{
	  cs = md->current_cs;
	  make_temporary_graph(cp,md,cs);
	  move_or_disable_sync_chain(cp->sound,md,cs->beg,nx);
	}
    }
}

static void mix_title_button_release(Widget w, XtPointer clientData, XEvent *event, Boolean *flag) 
{
  mixmark *m = (mixmark *)clientData;
  XButtonEvent *ev;
  console_state *cs;
  mixdata *md;
  mix_context *ms;
  snd_state *ss; 
  chan_info *cp;
  int amount;
  char *buf;
  m->moving = 0;
  cp = m_to_cp(m);
  ss = cp->state;
  if (!(ss->using_schemes)) XtVaSetValues(m->w[mm_name],XmNforeground,(ss->sgx)->black,NULL);
  md = m->owner;
  cs = md->current_cs;
  if (mix_dragged)
    {
      if (watch_mix_proc) 
	{
	  XtRemoveWorkProc(watch_mix_proc);
	  watch_mix_proc = 0;
	}
      mix_dragged = 0;
      /* now finalize the current state of the mix as an edit */
      clear_sync_moves(md);
      ms = md->wg;
      ms->lastpj = 0;
      if (cs->beg == cs->orig) return;
      amount = cs->beg - cs->orig;
      if (cs->groups) 
	{
	  move_associated_mixes(md,amount,FALSE);
	  /* TODO: fixup amp env */
	  remix_file_over_groups(ss,cs->groups);
	  reset_associated_origs(md,amount);
	}
      else 
	{
	  remix_sync_chain(md);
	  remix_file(md);
	  reset_syncd_origs(md,amount);
	}
    }
  else
    {
      /* control-click in title bar => reset anchor based on current cursor (if within mix segment) */
      ev = (XButtonEvent *)event;
      if (ev->state & (snd_ControlMask | snd_MetaMask))
	{
	  if ((cp->cursor >= cs->orig) && (cp->cursor <= cs->end))
	    {
	      md->anchor = (cp->cursor - cs->orig); /* doesn't follow undo/redo... */
	      anchor_or_disable_sync_chain(cp->sound,md,md->anchor);
	      update_graph(cp,NULL);
	    }
	}
      else 
	{
	  raise_widget(m->w[mm_main]); /* bring to top of possibly stacked console tree */
	  buf = (char *)calloc(16,sizeof(char));
	  sprintf(buf,"mix %d",int_from_md(md->ss,md));
	  report_in_minibuffer(cp->sound,buf);
	  free(buf);
	}
    }
}

static void mix_console_name_callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  /* called upon button release in name widget */
  mixmark *m = (mixmark *)clientData;
  XmPushButtonCallbackStruct *cb = (XmPushButtonCallbackStruct *)callData;
  mixdata *md;
  chan_info *cp;
  md = m->owner;
  cp = md->cp;
  select_channel(cp->sound,cp->chan);
  if ((cb->reason == XmCR_ACTIVATE) && (cb->click_count == 2))
    {
      /* if we're completely closed, open the title bar, else collapse to a single char */
      if (md->state == MD_M) md->state = MD_TITLE; else md->state = MD_M;
      fixup_mixmark(md);
    }
}

static void mix_title_button_motion(Widget w, XtPointer clientData, XEvent *event, Boolean *flag) 
{
  Time mix_time;
  chan_info *cp;
  mixdata *md;
  mixmark *m = (mixmark *)clientData;
  XMotionEvent *ev = (XMotionEvent *)event;
  /* this needs to be a little slow about deciding that we are dragging, as opposed to a slow click */
  mix_time = ev->time;
  if ((mix_time - mix_down_time) < (0.5 * XtGetMultiClickTime(XtDisplay(w)))) return;
  cp = m_to_cp(m);
  if (!mix_dragged) 
    {
      md = m->owner;
      set_sync_moves(md);
      mix_save_graph(md->ss,md->wg,make_graph(cp,cp->sound,cp->state));
    }
  mix_dragged = 1;
  current_mix_x = ev->x_root;
  m->moving = 1;
  move_mix(m,ev->x_root);
  md = m->owner;
  update_group_positions(md->ss,md);
}

void move_mixmark(mixmark *m, int x, int y)
{
  Position xx,yy;
  XtVaSetValues(m->w[mm_main],XmNx,(Position)x,XmNy,(Position)y,NULL);
  XtVaGetValues(m->w[mm_main],XmNx,&xx,XmNy,&yy,NULL);
  m->x = xx;
  m->y = yy;
  if (!(m->active)) activate_mixmark_widgets(m);
}

void move_mix_x(mixmark *m, int xspot)
{
  Position xx;
  XtVaSetValues(m->w[mm_main],XmNx,(Position)xspot,NULL);
  XtVaGetValues(m->w[mm_main],XmNx,&xx,NULL);
  m->x = xx;
}


static int add_key_press(Widget w, void *ptr)
{
  if (XtIsManaged(w))
    XtAddEventHandler(w,KeyPressMask,FALSE,graph_key_press,(XtPointer)ptr);
  return(0);
}

static void mousify(mixmark *m)
{
  snd_info *sp;
  chan_info *cp;

  XtAddEventHandler(m->w[mm_main],EnterWindowMask,FALSE,mix_mouse_enter,(XtPointer)NULL);
  XtAddEventHandler(m->w[mm_main],LeaveWindowMask,FALSE,mix_mouse_leave,(XtPointer)NULL);

  XtAddEventHandler(m->w[mm_title],ButtonPressMask,FALSE,mix_title_button_press,(XtPointer)m);
  XtAddEventHandler(m->w[mm_title],ButtonMotionMask,FALSE,mix_title_button_motion,(XtPointer)m);
  XtAddEventHandler(m->w[mm_title],ButtonReleaseMask,FALSE,mix_title_button_release,(XtPointer)m);

  XtAddEventHandler(m->w[mm_name],ButtonPressMask,FALSE,mix_title_button_press,(XtPointer)m);
  XtAddEventHandler(m->w[mm_name],ButtonMotionMask,FALSE,mix_title_button_motion,(XtPointer)m);
  XtAddEventHandler(m->w[mm_name],ButtonReleaseMask,FALSE,mix_title_button_release,(XtPointer)m);

  XtAddEventHandler(m->w[mm_console],ButtonPressMask,FALSE,mix_title_button_press,(XtPointer)m);
  XtAddEventHandler(m->w[mm_console],ButtonMotionMask,FALSE,mix_title_button_motion,(XtPointer)m);
  XtAddEventHandler(m->w[mm_console],ButtonReleaseMask,FALSE,mix_title_button_release,(XtPointer)m);
  
  cp = m_to_cp(m);
  sp = cp->sound;
  /* now everyone has to respond correctly to keypress (problematic apparently because traversal is off) */
  map_over_children(m->w[mm_main],add_key_press,(void *)sp);
}


#define GROUP_BUTTON_SIZE 13

static void create_mixer(mixdata *md, int x, int y)
{
  /* make a new mixer console */
  /* this is made complicated by the fact that, to quote the XmManager man page:
   *
   *   "In general a widget can receive keyboard focus when it is a primitive, a
   *    gadget, or a manager (such as a DrawingArea with no traversable children) 
   *    that acts as a primitive."
   *
   * which severely limits what we can do in the mix console, since it is a child
   * of the channel's graphics drawingarea widget.  We need to retain that structure
   * so that mix position correlates with waveform position. But this means we
   * can't use manager widgets to handle the console (no scrolling window, which
   * is what I wanted originally), and (sigh!) no text fields (for envelope definition).
   * I had some very fancy plans...
   */
  snd_state *ss;
  mixmark *m;
  mix_context *ms;
  int n,i;
  Dimension wid,hgt;
  Arg args[32];
  XmString s1;
  Widget last_widget,amp_widget;

  ss = md->ss;
  ms = md->wg;
  m = md->mixer;

  m->w = (Widget *)calloc(mm_chans+(md->in_chans*MIX_CHAN_SIZE) + ss->ngroups,sizeof(Widget));
  m->chans_allocated = md->in_chans;
  n=0;
  if (!(ss->using_schemes)) n = background_mixer_color(args,n,ss);
  XtSetArg(args[n],XmNtraversalOn,FALSE); n++;
  XtSetArg(args[n],XmNx,x); n++;
  XtSetArg(args[n],XmNy,y); n++;
  XtSetArg(args[n],XmNshadowType,XmSHADOW_OUT); n++;
  XtSetArg(args[n],XmNshadowThickness,1); n++;
  m->w[mm_main] = XtCreateManagedWidget("mm_main",xmFrameWidgetClass,ms->graph,args,n);
  XtAddCallback(m->w[mm_main],XmNhelpCallback,mix_console_help_callback,m);
  m->x = x;
  m->y = y;

  n=0;
  XtSetArg(args[n],XmNtraversalOn,FALSE); n++;
  m->w[mm_fmain] = XtCreateManagedWidget("mm_main",xmFormWidgetClass,m->w[mm_main],args,n);
  XtAddCallback(m->w[mm_fmain],XmNhelpCallback,mix_console_help_callback,m);

  n=0;
  if (!(ss->using_schemes)) n = background_mixer_color(args,n,ss);
  XtSetArg(args[n],XmNtopAttachment,XmATTACH_FORM); n++;
  XtSetArg(args[n],XmNbottomAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNleftAttachment,XmATTACH_FORM); n++;
  XtSetArg(args[n],XmNrightAttachment,XmATTACH_FORM); n++;
  XtSetArg(args[n],XmNorientation,XmHORIZONTAL); n++;
  XtSetArg(args[n],XmNtraversalOn,FALSE); n++;
  m->w[mm_title] = XtCreateManagedWidget("mm_title",xmRowColumnWidgetClass,m->w[mm_fmain],args,n);

  n=0;
  s1=XmStringCreate(md->name,"button_font");
  if (!(ss->using_schemes)) n = background_mixer_color(args,n,ss);
  XtSetArg(args[n],XmNfontList,button_FONT(ss)); n++;
  XtSetArg(args[n],XmNlabelString,s1); n++;
  XtSetArg(args[n],XmNtraversalOn,FALSE); n++;
  XtSetArg(args[n],XmNshadowThickness,0); n++;
  XtSetArg(args[n],XmNhighlightThickness,0); n++;
  XtSetArg(args[n],XmNfillOnArm,FALSE); n++;
  XtSetArg(args[n],XmNmultiClick,XmMULTICLICK_KEEP); n++;
  m->w[mm_name] = XtCreateManagedWidget("mm_name",xmPushButtonWidgetClass,m->w[mm_title],args,n);
  XtAddCallback(m->w[mm_name],XmNactivateCallback,mix_console_name_callback,m);
  XmStringFree(s1);
  
  if (!icons_created) create_icons(m->w[mm_name],ss); /* pick up console background color for label pixmaps */

  n=0;
  if (!(ss->using_schemes)) n = background_main_color(args,n,ss);
  XtSetArg(args[n],XmNtraversalOn,FALSE); n++;
  XtSetArg(args[n],XmNfontList,button_FONT(ss)); n++;
  m->w[mm_beg] = XtCreateManagedWidget("beg",xmPushButtonWidgetClass,m->w[mm_title],args,n);
  XtAddCallback(m->w[mm_beg],XmNactivateCallback,beg_click_callback,m);

  n=0;
  if (!(ss->using_schemes)) n = background_mixer_color(args,n,ss);
  XtSetArg(args[n],XmNlabelType,XmPIXMAP); n++;
  XtSetArg(args[n],XmNlabelPixmap,speaker_r); n++;
  XtSetArg(args[n],XmNtraversalOn,FALSE); n++;
  m->w[mm_play] = XtCreateManagedWidget("play",xmPushButtonWidgetClass,m->w[mm_title],args,n);
  XtAddCallback(m->w[mm_play],XmNarmCallback,mix_console_start_play_callback,m);
  XtAddCallback(m->w[mm_play],XmNdisarmCallback,mix_console_stop_play_callback,m);

  n=0;
  if (!(ss->using_schemes)) n = background_mixer_color(args,n,ss);
  XtSetArg(args[n],XmNlabelType,XmPIXMAP); n++;
  XtSetArg(args[n],XmNlabelPixmap,cross_r); n++;
  XtSetArg(args[n],XmNtraversalOn,FALSE); n++;
  m->w[mm_close] = XtCreateManagedWidget("close",xmPushButtonWidgetClass,m->w[mm_title],args,n);
  XtAddCallback(m->w[mm_close],XmNactivateCallback,mix_console_close_callback,m);

  n=0;
  if (!(ss->using_schemes)) n = background_mixer_color(args,n,ss);
  XtSetArg(args[n],XmNlabelType,XmPIXMAP); n++;
  XtSetArg(args[n],XmNlabelPixmap,mixer_r); n++;
  XtSetArg(args[n],XmNtraversalOn,FALSE); n++;
  m->w[mm_open] = XtCreateManagedWidget("open",xmPushButtonWidgetClass,m->w[mm_title],args,n);
  XtAddCallback(m->w[mm_open],XmNactivateCallback,mix_console_open_callback,m);

  n=0;
  if (!(ss->using_schemes)) n = background_main_color(args,n,ss);
  XtSetArg(args[n],XmNtopAttachment,XmATTACH_WIDGET); n++;
  XtSetArg(args[n],XmNtopWidget,m->w[mm_title]); n++;
  XtSetArg(args[n],XmNbottomAttachment,XmATTACH_FORM); n++;
  XtSetArg(args[n],XmNleftAttachment,XmATTACH_FORM); n++;
  XtSetArg(args[n],XmNrightAttachment,XmATTACH_FORM); n++;
  XtSetArg(args[n],XmNtraversalOn,FALSE); n++;
  XtSetArg(args[n],XmNresizable,TRUE); n++;
  m->w[mm_console] = XtCreateManagedWidget("mm_console",xmFormWidgetClass,m->w[mm_fmain],args,n);

  n=0;
  if (!(ss->using_schemes)) n = background_main_color(args,n,ss);
  XtSetArg(args[n],XmNtopAttachment,XmATTACH_FORM); n++;
  XtSetArg(args[n],XmNbottomAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNleftAttachment,XmATTACH_FORM); n++;
  XtSetArg(args[n],XmNrightAttachment,XmATTACH_FORM); n++;
  XtSetArg(args[n],XmNseparatorType,XmSHADOW_ETCHED_IN); n++;
  XtSetArg(args[n],XmNtraversalOn,FALSE); n++;
  XtSetArg(args[n],XmNheight,0); n++;
  m->w[mm_title_sep] = XtCreateManagedWidget("mm-title-sep",xmSeparatorWidgetClass,m->w[mm_console],args,n);

  n=0;
  if (!(ss->using_schemes)) 
    {
      n = background_main_color(args,n,ss);
      XtSetArg(args[n],XmNarmColor,(ss->sgx)->mixpix); n++;
      XtSetArg(args[n],XmNfillOnArm,TRUE); n++;
    }
  XtSetArg(args[n],XmNtopAttachment,XmATTACH_WIDGET); n++;
  XtSetArg(args[n],XmNtopWidget,m->w[mm_title_sep]); n++;
  XtSetArg(args[n],XmNbottomAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNleftAttachment,XmATTACH_FORM); n++;
  XtSetArg(args[n],XmNrightAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNtraversalOn,FALSE); n++;
  XtSetArg(args[n],XmNfontList,button_FONT(ss)); n++;
  XtSetArg(args[n],XmNrecomputeSize,FALSE); n++;
  XtSetArg(args[n],XmNshadowThickness,0); n++;
  XtSetArg(args[n],XmNhighlightThickness,0); n++;
  XtSetArg(args[n],XmNmultiClick,XmMULTICLICK_KEEP); n++;
  m->w[mm_amp] = XtCreateManagedWidget(snd_string_amp_m,xmPushButtonWidgetClass,m->w[mm_console],args,n);
  XtAddCallback(m->w[mm_amp],XmNactivateCallback,amp_click_callback,m);
#if OVERRIDE_TOGGLE
  override_toggle_translation(m->w[mm_amp]);
#endif
  last_widget = m->w[mm_amp];

  for (i=0;i<ss->ngroups;i++)
    {
      n=0;
      if (!(ss->using_schemes)) n = background_main_color(args,n,ss);
      XtSetArg(args[n],XmNtopAttachment,XmATTACH_WIDGET); n++;
      XtSetArg(args[n],XmNtopWidget,m->w[mm_title_sep]); n++;
      XtSetArg(args[n],XmNbottomAttachment,XmATTACH_NONE); n++;
      if (i == 0)
	{XtSetArg(args[n],XmNrightAttachment,XmATTACH_FORM); n++;}
      else 
	{
	  XtSetArg(args[n],XmNrightAttachment,XmATTACH_WIDGET); n++;
	  XtSetArg(args[n],XmNrightWidget,last_widget); n++;
	}
      XtSetArg(args[n],XmNleftAttachment,XmATTACH_NONE); n++;
      XtSetArg(args[n],XmNheight,GROUP_BUTTON_SIZE); n++;
      XtSetArg(args[n],XmNindicatorSize,GROUP_BUTTON_SIZE); n++;
      XtSetArg(args[n],XmNmarginWidth,0); n++;
      XtSetArg(args[n],XmNmarginLeft,0); n++;
      if (i != 0) {XtSetArg(args[n],XmNmarginRight,0); n++;}
      XtSetArg(args[n],XmNtopOffset,2); n++;
      XtSetArg(args[n],XmNspacing,0); n++;
      XtSetArg(args[n],XmNtraversalOn,FALSE); n++;
      XtSetArg(args[n],XmNuserData,(XtPointer)(ss->ngroups - i - 1)); n++; /* pass group number of this button */
      if (!(ss->using_schemes)) {XtSetArg(args[n],XmNselectColor,(ss->sgx)->red); n++;}
      /* someday we might want the color to code the group (title, waveform, etc) */
      last_widget = XtCreateManagedWidget("",xmToggleButtonWidgetClass,m->w[mm_console],args,n);
      m->w[mm_chans + md->in_chans*MIX_CHAN_SIZE + i] = last_widget;
      XtAddCallback(last_widget,XmNvalueChangedCallback,group_button_callback,m);
    }

  last_widget = m->w[mm_amp];
  for (i=0;i<md->in_chans;i++)
    {
      n=0;
      if (!(ss->using_schemes)) n = background_main_color(args,n,ss);
      s1=XmStringCreate((i == md->out_chan) ? "1.00" : "0.00","button_font");
      XtSetArg(args[n],XmNfontList,button_FONT(ss)); n++;
      XtSetArg(args[n],XmNalignment,XmALIGNMENT_BEGINNING); n++;
      XtSetArg(args[n],XmNtopAttachment,XmATTACH_WIDGET); n++;
      XtSetArg(args[n],XmNtopWidget,last_widget); n++;
      XtSetArg(args[n],XmNbottomAttachment,XmATTACH_NONE); n++;
      XtSetArg(args[n],XmNleftAttachment,XmATTACH_NONE); n++;
      /* XtSetArg(args[n],XmNrightAttachment,XmATTACH_FORM); n++; */
      XtSetArg(args[n],XmNrightAttachment,XmATTACH_POSITION); n++;
      XtSetArg(args[n],XmNrightPosition,98); n++;
      XtSetArg(args[n],XmNlabelString,s1); n++;
      XtSetArg(args[n],XmNtraversalOn,FALSE); n++;
      /* XtSetArg(args[n],XmNrecomputeSize,FALSE); n++; */
      amp_widget = XtCreateManagedWidget ("amp-number",xmLabelWidgetClass,m->w[mm_console],args,n);
      m->w[mm_chans + mm_amp_label + i*MIX_CHAN_SIZE] = amp_widget;
      XmStringFree(s1);
      
      n=0;
      if (!(ss->using_schemes)) n = background_main_color(args,n,ss);
      XtSetArg(args[n],XmNtopAttachment,XmATTACH_WIDGET); n++;
      XtSetArg(args[n],XmNtopWidget,last_widget); n++;
      XtSetArg(args[n],XmNbottomAttachment,XmATTACH_NONE); n++;
      /* XtSetArg(args[n],XmNleftAttachment,XmATTACH_FORM); n++; */
      XtSetArg(args[n],XmNleftAttachment,XmATTACH_POSITION); n++;
      XtSetArg(args[n],XmNleftPosition,2); n++;
      XtSetArg(args[n],XmNrightAttachment,XmATTACH_WIDGET); n++;
      XtSetArg(args[n],XmNrightWidget,amp_widget); n++;
      XtSetArg(args[n],XmNorientation,XmHORIZONTAL); n++;
      XtSetArg(args[n],XmNdragCallback,make_callback_list(m_amp_drag_callback,(XtPointer)m)); n++;
      XtSetArg(args[n],XmNvalueChangedCallback,make_callback_list(m_amp_value_changed_callback,(XtPointer)m)); n++;
      /* has to be m here, not md since this is not changed once created */
      XtSetArg(args[n],XmNtraversalOn,FALSE); n++;
      XtSetArg(args[n],XmNvalue,50); n++;
      XtSetArg(args[n],XmNheight,14); n++;
      XtSetArg(args[n],XmNuserData,(XtPointer)i); n++; /* pass channel number of this scaler */
      last_widget = XtCreateManagedWidget ("mm-amp",xmScaleWidgetClass,m->w[mm_console],args,n);
      m->w[mm_chans + mm_scl + i*MIX_CHAN_SIZE] = last_widget;
    }
  
  n=0;
  if (!(ss->using_schemes)) 
    {
      n = background_main_color(args,n,ss);
      XtSetArg(args[n],XmNarmColor,(ss->sgx)->mixpix); n++;
      XtSetArg(args[n],XmNfillOnArm,TRUE); n++;
    }
  XtSetArg(args[n],XmNtopAttachment,XmATTACH_WIDGET); n++;
  XtSetArg(args[n],XmNtopWidget,last_widget); n++;
  XtSetArg(args[n],XmNbottomAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNleftAttachment,XmATTACH_FORM); n++;
  XtSetArg(args[n],XmNrightAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNtraversalOn,FALSE); n++;
  XtSetArg(args[n],XmNfontList,button_FONT(ss)); n++;
  XtSetArg(args[n],XmNrecomputeSize,FALSE); n++;
  XtSetArg(args[n],XmNshadowThickness,0); n++;
  XtSetArg(args[n],XmNhighlightThickness,0); n++;	 
  XtSetArg(args[n],XmNmultiClick,XmMULTICLICK_DISCARD); n++;
  m->w[mm_speed] = XtCreateManagedWidget(snd_string_speed_m,xmPushButtonWidgetClass,m->w[mm_console],args,n);
  XtAddCallback(m->w[mm_speed],XmNactivateCallback,speed_click_callback,m);
#if OVERRIDE_TOGGLE
  override_toggle_translation(m->w[mm_speed]);
#endif
  
  n=0;
  if (!(ss->using_schemes)) n = background_main_color(args,n,ss);
  s1=XmStringCreate("1.00","button_font");
  XtSetArg(args[n],XmNfontList,button_FONT(ss)); n++;
  XtSetArg(args[n],XmNalignment,XmALIGNMENT_BEGINNING); n++;	
  XtSetArg(args[n],XmNtopAttachment,XmATTACH_WIDGET); n++;
  XtSetArg(args[n],XmNtopWidget,m->w[mm_speed]); n++;
  XtSetArg(args[n],XmNbottomAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNleftAttachment,XmATTACH_NONE); n++;
  /* XtSetArg(args[n],XmNrightAttachment,XmATTACH_FORM); n++; */
  XtSetArg(args[n],XmNrightAttachment,XmATTACH_POSITION); n++;
  XtSetArg(args[n],XmNrightPosition,98); n++;
  XtSetArg(args[n],XmNlabelString,s1); n++;
  XtSetArg(args[n],XmNtraversalOn,FALSE); n++;
  m->w[mm_speed_label] = XtCreateManagedWidget ("spd-number",xmLabelWidgetClass,m->w[mm_console],args,n);
  XmStringFree(s1);
  
  n=0;
  if (!(ss->using_schemes)) n = background_main_color(args,n,ss);
  XtSetArg(args[n],XmNtopAttachment,XmATTACH_WIDGET); n++;
  XtSetArg(args[n],XmNtopWidget,m->w[mm_speed]); n++;
  XtSetArg(args[n],XmNbottomAttachment,XmATTACH_NONE); n++;
  /* XtSetArg(args[n],XmNleftAttachment,XmATTACH_FORM); n++; */
  XtSetArg(args[n],XmNleftAttachment,XmATTACH_POSITION); n++;
  XtSetArg(args[n],XmNleftPosition,2); n++;
  XtSetArg(args[n],XmNrightAttachment,XmATTACH_WIDGET); n++;
  XtSetArg(args[n],XmNrightWidget,m->w[mm_speed_label]); n++;
  XtSetArg(args[n],XmNorientation,XmHORIZONTAL); n++;
  XtSetArg(args[n],XmNdragCallback,make_callback_list(m_speed_drag_callback,(XtPointer)m)); n++;
  XtSetArg(args[n],XmNvalueChangedCallback,make_callback_list(m_speed_value_changed_callback,(XtPointer)m)); n++;
  /* has to be m here, not md since this is not changed once created */
  XtSetArg(args[n],XmNtraversalOn,FALSE); n++;
  XtSetArg(args[n],XmNvalue,50); n++;
  XtSetArg(args[n],XmNheight,14); n++;
  m->w[mm_spdscl] = XtCreateManagedWidget("mm-spdscl",xmScaleWidgetClass,m->w[mm_console],args,n);
  
  n=0;
  if (!(ss->using_schemes)) n = background_main_color(args,n,ss);
  XtSetArg(args[n],XmNtopAttachment,XmATTACH_WIDGET); n++;
  XtSetArg(args[n],XmNtopWidget,m->w[mm_spdscl]); n++;
  XtSetArg(args[n],XmNbottomAttachment,XmATTACH_FORM); n++;
  XtSetArg(args[n],XmNleftAttachment,XmATTACH_FORM); n++;
  XtSetArg(args[n],XmNrightAttachment,XmATTACH_FORM); n++;
  XtSetArg(args[n],XmNseparatorType,XmNO_LINE); n++;
  XtSetArg(args[n],XmNtraversalOn,FALSE); n++;
  XtSetArg(args[n],XmNheight,4); n++;
  m->w[mm_spdsep] = XtCreateManagedWidget("mm-spdsep",xmSeparatorWidgetClass,m->w[mm_console],args,n);

  mousify(m);

  XtVaGetValues(m->w[mm_main],XmNwidth,&wid,XmNheight,&hgt,NULL);
  md->width = wid;
  md->height = hgt;
  m->state = MD_CS;
  m->active = 1;

}

void use_mixmark(mixdata *md, int x, int y)
{
  mixmark *m;
  m = md->mixer;
  m->owner = md;
  if (!(m->w)) create_mixer(md,x,y);
  fixup_mixmark(md);
}

#define MID_MIX_Y 10
#define MIX_BRACKET_LENGTH 14

static void ur_draw_mix_duration_bracket(mixdata *md, int xspot, int yspot, int draw)
{
  chan_info *cp;
  axis_context *ax;
  axis_info *ap;
  int endspot;
  console_state *cs;
  chan_context *cx;
  GC curgc;
  /* go to right side of mixer title bar, draw a line to end of graph window or duration, bracket in latter case */
  /* similarly on the left if anchor != 0 */
  cp = md->cp;
  ap = cp->axis;
  cx = cp->tcgx;
  if (!cx) cx = cp->cgx;
  ax = cx->ax;
  if (draw) curgc = cx->gc; else curgc = cx->igc;
  cs = md->current_cs;
  endspot = grf_x((double)(cs->beg+cs->len)/(double)snd_SRATE(cp),ap);
  if (endspot > ap->x_axis_x1) endspot = ap->x_axis_x1;
  XDrawLine(ax->dp,ax->wn,curgc,xspot+md->width,yspot+MID_MIX_Y,endspot,yspot+MID_MIX_Y);
  if (endspot != ap->x_axis_x1)
    XDrawLine(ax->dp,ax->wn,curgc,endspot,yspot+MID_MIX_Y,endspot,yspot+MID_MIX_Y+MIX_BRACKET_LENGTH);
  if (md->anchor > 0)
    {
      endspot = grf_x((double)(cs->beg)/(double)snd_SRATE(cp),ap);
      if (endspot < ap->x_axis_x0) endspot = ap->x_axis_x0;
      XDrawLine(ax->dp,ax->wn,curgc,endspot,yspot+MID_MIX_Y,xspot,yspot+MID_MIX_Y);
      if (endspot != ap->x_axis_x0)
	XDrawLine(ax->dp,ax->wn,curgc,endspot,yspot+MID_MIX_Y,endspot,yspot+MID_MIX_Y+MIX_BRACKET_LENGTH);
    }
}

void draw_mix_duration_bracket(mixdata *md, int xspot, int yspot) {ur_draw_mix_duration_bracket(md,xspot,yspot,TRUE);}
void erase_mix_duration_bracket(mixdata *md, int xspot, int yspot) {ur_draw_mix_duration_bracket(md,xspot,yspot,FALSE);}



/* ---------------- GROUP BROWSER ---------------- */

static Widget *wb = NULL;

enum {
  g_dialog,
  g_form,
  g_row,
  g_times,g_info,
  g_title_sep,
  g_amp_label,g_env_label,
  g_speed_label,g_speed_val,g_speed_scale,g_speed_env,
  g_tempo_label,g_tempo_val,g_tempo_scale,g_tempo_env,
  g_play,g_entire,g_dur,g_movies,
  g_arrays
};

#define g_amp_file  0
#define g_amp_scale 1
#define g_amp_val   2
#define g_amp_env   3
#define g_amp_size  4


static int color_scale_inactive(Widget w, void *s)
{ /* can also use XmNtroughColor here I think */
  snd_state *ss = (snd_state *)s;
  if (XmIsScrollBar(w)) XmChangeColor(w,(Pixel)((ss->sgx)->main));
  return(0);
}
	  
static int color_scale_active(Widget w, void *s)
{ 
  snd_state *ss = (snd_state *)s;
  if (XmIsScrollBar(w)) XmChangeColor(w,(Pixel)((ss->sgx)->scale));
  return(0);
}

static void color_mixer_title(mixmark *m, Pixel pix)
{
  XtVaSetValues(m->w[mm_name],XmNbackground,pix,NULL);
  XtVaSetValues(m->w[mm_title],XmNbackground,pix,NULL);
}

static void highlight_group(snd_state *ss, grp_info *g, int high)
{
  /* sx->mixhi => mixer title highlight color (to show which mix consoles are in the current group) */
  int i;
  mixdata *md;
  mixmark *m;
  Pixel pix;
  if (g)
    {
      for (i=0;i<g->mixes;i++)
	{
	  md = g->mds[i];
	  if (md)
	    {
	      m = md->mixer;
	      if (m) 
		{
		  if (high) pix = (ss->sgx)->mixhi; else pix = (ss->sgx)->mixpix;
		  color_mixer_title(m,pix);
		}}}}
}

void reflect_group_size_in_scales(snd_state *ss, grp_info *g)
{
  if (g->mixes > 0)
    {
      highlight_group(ss,g,TRUE);
      map_over_children(wb[g_speed_scale],color_scale_active,(void *)ss);
      map_over_children(wb[g_tempo_scale],color_scale_active,(void *)ss);
    }
  else
    {
      map_over_children(wb[g_speed_scale],color_scale_inactive,(void *)ss);
      map_over_children(wb[g_tempo_scale],color_scale_inactive,(void *)ss);
    }
}

int gb_is_active(void)
{
  return((wb) && (wb[g_dialog]) && (XtIsManaged(wb[g_dialog])));
}
  
static int last_browser_label_group = -1;

static void make_group_browser_label(Widget lab, Widget tim, grp_info *g)
{
  /* label should be "group 3 (n member[s][, undo: m inactive): 0.123 : 1.321"  group 0 (empty) */
  char *str;
  XmString s1,s2;
  str = (char *)calloc(128,sizeof(char));
  if (g->mixes == 0)
    sprintf(str,"(empty)");
  else 
    {
      if ((g->beg == 0.0) && (g->end == 0.0))
	set_group_bounds(g);
      if (g->unreadies == 0)
	sprintf(str,"(%d member%s): %.3f to %.3f",g->mixes,((g->mixes == 1) ? "" : "s"),g->beg,g->end);
      else sprintf(str,"(%d member%s, %d inactive): %.3f to %.3f",g->mixes,((g->mixes == 1) ? "" : "s"),g->unreadies,g->beg,g->end);
    }
  s1=XmStringCreate(str,"button_font");
  XtVaSetValues(tim,XmNlabelString,s1,NULL);
  XmStringFree(s1);
  if (g->group != last_browser_label_group)
    {
      sprintf(str,"group %d:",g->group);
      s2=XmStringCreate(str,XmFONTLIST_DEFAULT_TAG);
      XtVaSetValues(lab,XmNlabelString,s2,NULL);
      XmStringFree(s2);
      last_browser_label_group = g->group;
    }
  free(str);
}

void remake_group_browser_label(void)
{
  make_group_browser_label(wb[g_times],wb[g_info],get_current_group());
}

void remake_group_amp_labels(snd_state *ss)
{
  char **file_names;
  int i,j;
  int chans;
  grp_info *g;
  g = get_current_group();
  file_names = (char **)calloc(GROUP_MAX_OUT_CHANS,sizeof(char *));
  set_group_out_names(g,file_names,GROUP_MAX_OUT_CHANS);
  chans = g->chans;
  if (g->active == GROUP_INITIALIZED)
    { 
      g->active = GROUP_ACTIVE;
      for (i=0;i<chans;i++)
	{
	  g->amps[i] = 1.0;
	  g->old_amps[i] = 1.0;
	}
      for (i=chans;i<GROUP_MAX_OUT_CHANS;i++)
	{
	  g->amps[i] = 0.0;
	  g->old_amps[i] = 0.0;
	}
    }
  else
    {
      if (chans > g->chans) /* an active group got new mix output channels */
	{
	  for (i=g->chans;i<chans;i++)
	    {
	      g->amps[i] = 1.0;
	      g->old_amps[i] = 1.0;
	    }
	}
    }
  g->chans = chans;
  for (i=0,j=g_arrays+ss->ngroups;i<GROUP_MAX_OUT_CHANS;i++,j+=g_amp_size)
    {
      make_button_label(wb[j+g_amp_file],file_names[i]);
      if (i<chans)
	{
	  reflect_mix_amp(wb[j+g_amp_scale],wb[j+g_amp_val],g->amps[i],g->amps[i]);
	  if (!(ss->using_schemes)) map_over_children(wb[j+g_amp_scale],color_scale_active,(void *)ss);
	}
      else
	{
	  if (!(ss->using_schemes)) map_over_children(wb[j+g_amp_scale],color_scale_inactive,(void *)ss);
	}
      /* envelopes */
    }
  for (i=0;i<chans;i++) free(file_names[i]);
  free(file_names);
  file_names = NULL;
}

void reflect_mixer_amp(mixdata *md)
{
  int i,j;
  console_state *cs;
  mixmark *m;
  cs = md->current_cs;
  m = md->mixer;
  for (i=0,j=mm_chans;i<md->in_chans;i++,j+=MIX_CHAN_SIZE) 
    {
      cs->scalers[i] = cs->scl_scalers[i] * cs->gs_amp;
      if ((m) && (m->active)) reflect_mix_amp(m->w[j+mm_scl],m->w[j+mm_amp_label],cs->scl_scalers[i],cs->scalers[i]);
    }
}

void update_group_browser(snd_state *ss)
{
  int grp,i,j;
  grp_info *g;
  g = get_current_group();
  if (!g) {set_current_group(0); g = get_current_group();}
  remake_group_amp_labels(ss);
  grp = g->group;
  make_group_browser_label(wb[g_times],wb[g_info],g);
  g->speed = reflect_mix_speed(wb[g_speed_scale],wb[g_speed_val],ss,g->scl_speed,g->old_speed);
  reflect_mix_tempo(wb[g_tempo_scale],wb[g_tempo_val],g->tempo);
  reflect_group_size_in_scales(ss,g);
  XmToggleButtonSetState(wb[g_arrays+grp],TRUE,FALSE);
  XmToggleButtonSetState(wb[g_movies],ss->movies,FALSE);
  XmToggleButtonSetState(wb[g_dur],ss->mix_duration_brackets,FALSE);
  XmToggleButtonSetState(wb[g_entire],g->env_entire,FALSE);
  for (i=0,j=g_arrays+ss->ngroups;i<GROUP_MAX_OUT_CHANS;i++,j+=g_amp_size)
    {
      XmTextSetString(wb[j+g_amp_env],g->env_strs[i]);
    } 
  XmTextSetString(wb[g_tempo_env],g->env_strs[GROUP_MAX_OUT_CHANS+1]);
  XmTextSetString(wb[g_speed_env],g->env_strs[GROUP_MAX_OUT_CHANS]);
}

static XEvent *last_env_activate_event = NULL;
/* try to keep automatically generated callbacks from clobbering the group browser */

static void Groups_Dismiss_Callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  snd_state *ss = (snd_state *)clientData;
  XmAnyCallbackStruct *cb = (XmAnyCallbackStruct *)callData;
  grp_info *g;
  if (cb->event != last_env_activate_event)  /* passed up from textfield widget after <cr> typed */
    {
      g = get_current_group();
      if (g) highlight_group(ss,g,FALSE);
      XtUnmanageChild(wb[g_dialog]);
    }
}

static void Groups_Help_Callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  XmAnyCallbackStruct *cb = (XmAnyCallbackStruct *)callData;
  if (cb->event != last_env_activate_event)  /* passed up from textfield widget after <cr> typed */
    {
      snd_help((snd_state *)clientData,snd_string_Groups,get_grouping_help());
    }
}

static void Groups_Button_Callback(Widget w,XtPointer clientData,XtPointer callData)
{
  int button;
  grp_info *g;
  XmToggleButtonCallbackStruct *cb = (XmToggleButtonCallbackStruct *)callData;
  snd_state *ss = (snd_state *)clientData;
  if ((snd_pointer_type(ss)) != SND_STATE) snd_pointer_error("yow ",SND_STATE,ss);
  if (cb->set)
    {
      XtVaGetValues(w,XmNuserData,&button,NULL);
      set_current_group(button);
      update_group_browser(ss);
    }
  else
    {
      g = get_current_group();
      if (g) highlight_group(ss,g,FALSE);
    }
}

void stop_group_play(snd_info *sp)
{
  if ((wb) && (wb[g_play]))
    XmToggleButtonSetState(wb[g_play],FALSE,FALSE);
}

static void Groups_Play_Callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  XmToggleButtonCallbackStruct *cb = (XmToggleButtonCallbackStruct *)callData;
  snd_state *ss = (snd_state *)clientData;
  grp_info *g;
  g = get_current_group();
  if (g)
    {
      if (cb->set) 
	start_playing_group(ss,g);
      else stop_playing_group(ss,g);
    }
}

static void Groups_Clear_Callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  snd_state *ss = (snd_state *)clientData;
  XmAnyCallbackStruct *cb = (XmAnyCallbackStruct *)callData;
  if (cb->event != last_env_activate_event)  /* passed up from textfield widget after <cr> typed */
    clear_groups(ss);
}

static void group_drag_amp_callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  XmScaleCallbackStruct *cb = (XmScaleCallbackStruct *)callData;
  snd_state *ss = (snd_state *)clientData;
  int row,jrow;
  grp_info *g;
  g = get_current_group();
  XtVaGetValues(w,XmNuserData,&row,NULL);
  jrow = g_arrays + ss->ngroups + row*g_amp_size;
  g->amps[row] = change_amp_label(wb[jrow+g_amp_val],amp_int_to_float(cb->value));
  refigure_amps(ss,row);
  if (g->mixes > 0) make_temporary_graph_over_group(ss,g);
}

void reflect_group_amp_row_change(snd_state *ss, grp_info *g, int row)
{
  int jrow;
  jrow = g_arrays + ss->ngroups + row*g_amp_size;
  reflect_mix_amp(wb[jrow+g_amp_scale],wb[jrow+g_amp_val],g->amps[row],g->amps[row]);
}

static void group_change_amp_callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  XmScaleCallbackStruct *cb = (XmScaleCallbackStruct *)callData;
  snd_state *ss = (snd_state *)clientData;
  int row,jrow;
  grp_info *g;
  g = get_current_group();
  XtVaGetValues(w,XmNuserData,&row,NULL);
  jrow = g_arrays + ss->ngroups + row*g_amp_size;
  g->amps[row] = change_amp_label(wb[jrow+g_amp_val],amp_int_to_float(cb->value));
  g->old_amps[row] = g->amps[row];
  refigure_amps(ss,row);
  if (g->mixes > 0) remix_file_with_group(g,row);
}

static void group_amp_env_callback(Widget w,XtPointer clientData,XtPointer callData)
{
  snd_state *ss = (snd_state *)clientData;
  XmAnyCallbackStruct *cb = (XmAnyCallbackStruct *)callData;
  int row,err;
  grp_info *g;
  g = get_current_group();
  XtVaGetValues(w,XmNuserData,&row,NULL);
  if (g->amp_envs[row]) free_env(g->amp_envs[row]);
  g->env_strs[row] = copy_string(XmTextGetString(w));
  g->amp_envs[row] = scan_envelope_and_report_error(any_selected_sound(ss),XmTextGetString(w),&err);
  last_env_activate_event = cb->event;
  refigure_amp_envs(ss,row);
  if (g->mixes > 0) remix_file_with_group(g,row);
}

static void group_drag_speed_callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  XmScaleCallbackStruct *cb = (XmScaleCallbackStruct *)callData;
  snd_state *ss = (snd_state *)clientData;
  grp_info *g;
  g = get_current_group();
  g->scl_speed = speed_int_to_float(cb->value);
  g->speed = change_speed_label(wb[g_speed_val],ss,g->scl_speed);
  update_group_speed(ss,g);
}

static void group_change_speed_callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  XmScaleCallbackStruct *cb = (XmScaleCallbackStruct *)callData;
  snd_state *ss = (snd_state *)clientData;
  grp_info *g;
  g = get_current_group();
  g->scl_speed = speed_int_to_float(cb->value);
  g->speed = change_speed_label(wb[g_speed_val],ss,g->scl_speed);
  g->old_speed = cb->value;
  remix_group_speeds(ss,g);
}

static void group_speed_env_callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  XmAnyCallbackStruct *cb = (XmAnyCallbackStruct *)callData;
  snd_state *ss = (snd_state *)clientData;
  int err;
  grp_info *g;
  g = get_current_group();
  if (g->speed_env) free_env(g->speed_env);
  g->env_strs[GROUP_MAX_OUT_CHANS] = copy_string(XmTextGetString(w));
  g->speed_env = scan_envelope_and_report_error(any_selected_sound(ss),XmTextGetString(w),&err);
  last_env_activate_event = cb->event;
  remix_group_speeds(ss,g);
}

void reflect_group_speed_change(snd_state *ss, grp_info *g, float speed)
{
  g->speed = ur_srate_changed(speed,srcbuf,ss->speed_style,ss->speed_tones); 
  if ((g == (get_current_group())) && (gb_is_active())) make_name_label(wb[g_speed_val],srcbuf);
}

void reflect_group_tempo_change(grp_info *g, float val)
{
  g->tempo = val;
  if ((g == (get_current_group())) && (gb_is_active())) change_tempo_label(wb[g_tempo_val],g->tempo);
}

static void group_drag_tempo_callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  XmScaleCallbackStruct *cb = (XmScaleCallbackStruct *)callData;
  snd_state *ss = (snd_state *)clientData;
  grp_info *g;
  g = get_current_group();
  g->tempo = tempo_int_to_float(cb->value);
  change_tempo_label(wb[g_tempo_val],g->tempo);
  mix_dragged = 1;
  update_group_tempo(ss,g);
}

static void group_change_tempo_callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  XmScaleCallbackStruct *cb = (XmScaleCallbackStruct *)callData;
  snd_state *ss = (snd_state *)clientData;
  grp_info *g;
  g = get_current_group();
  g->tempo = tempo_int_to_float(cb->value);
  g->old_tempo = g->tempo;
  change_tempo_label(wb[g_tempo_val],g->tempo);
  remix_group_tempo(ss,g);
  mix_dragged = 0;
}

static void group_tempo_env_callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  int err;
  /* should this be the time-map (integrated tempo) or the tempo-map? */
  XmAnyCallbackStruct *cb = (XmAnyCallbackStruct *)callData;
  snd_state *ss = (snd_state *)clientData;
  grp_info *g;
  g = get_current_group();
  if (g->tempo_env) free_env(g->tempo_env);
  g->env_strs[GROUP_MAX_OUT_CHANS+1] = copy_string(XmTextGetString(w));
  g->tempo_env = scan_envelope_and_report_error(any_selected_sound(ss),XmTextGetString(w),&err);
  last_env_activate_event = cb->event;
  remix_group_tempo(ss,g);
}

static void Groups_Duration_Callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  XmToggleButtonCallbackStruct *cb = (XmToggleButtonCallbackStruct *)callData;
  snd_state *ss = (snd_state *)clientData;
  ss->mix_duration_brackets = (cb->set);
  map_over_chans(ss,update_graph,NULL);
}

static void Groups_Movies_Callback(Widget w,XtPointer clientData,XtPointer callData) 
{
  XmToggleButtonCallbackStruct *cb = (XmToggleButtonCallbackStruct *)callData;
  snd_state *ss = (snd_state *)clientData;
  ss->movies = cb->set;
}

static void Groups_Amp_Click_Callback(Widget w,XtPointer clientData,XtPointer callData)
{
  XmPushButtonCallbackStruct *cb = (XmPushButtonCallbackStruct *)callData;
  snd_state *ss = (snd_state *)clientData;
  int i,j;
  XButtonEvent *ev;
  mixdata *md;
  grp_info *g;
  g = get_current_group();
  ev = (XButtonEvent *)(cb->event);
  for (i=0,j=g_arrays+ss->ngroups;i<GROUP_MAX_OUT_CHANS;i++,j+=g_amp_size)
    {
      if (cb->click_count == 1)
	{
	  if (ev->state & (snd_ControlMask | snd_MetaMask))
	    g->amps[i] = g->old_amps[i];
	  else 
	    {
	      if (i<g->chans) g->amps[i] = 1.0;
	      else g->amps[i] = 0.0;
	    }
	}
      else
	{
	  if (cb->click_count == 2) g->amps[i] = 0.0;
	}
      reflect_mix_amp(wb[j+g_amp_scale],wb[j+g_amp_val],g->amps[i],g->amps[i]);
    }
  if (g->mixes > 0) 
    {
      for (i=0;i<g->mixes;i++) 
	{
	  md = g->mds[i];
	  if (ready_mix(md)) refigure_amp(ss,md);
	}
      remix_file_over_group(g);
    }
}

static void Groups_Speed_Click_Callback(Widget w,XtPointer clientData,XtPointer callData)
{
  XmPushButtonCallbackStruct *cb = (XmPushButtonCallbackStruct *)callData;
  XButtonEvent *ev;
  snd_state *ss = (snd_state *)clientData;
  int i,val;
  mixdata *md;
  grp_info *g;
  g = get_current_group();
  ev = (XButtonEvent *)(cb->event);

  if (ev->state & (snd_ControlMask | snd_MetaMask))
    {
      g->scl_speed = speed_int_to_float(g->old_speed);
      val = g->old_speed;
    }
  else
    {
      g->scl_speed = 1.0;
      val = 50;
    }
  g->speed = reflect_mix_speed(wb[g_speed_scale],wb[g_speed_val],ss,g->scl_speed,val);
  if (g->mixes > 0) 
    {
      for (i=0;i<g->mixes;i++) 
	{
	  md = g->mds[i];
	  if (ready_mix(md)) refigure_speed(ss,md);
	}
      remix_file_over_group(g);
    }
} 

static void Groups_Entire_Callback(Widget w,XtPointer clientData,XtPointer callData)
{
  XmToggleButtonCallbackStruct *cb = (XmToggleButtonCallbackStruct *)callData;
  grp_info *g;
  g = get_current_group();
  g->env_entire = cb->set;
}

static void Groups_Tempo_Click_Callback(Widget w,XtPointer clientData,XtPointer callData)
{
  XmPushButtonCallbackStruct *cb = (XmPushButtonCallbackStruct *)callData;
  XButtonEvent *ev;
  snd_state *ss = (snd_state *)clientData;
  grp_info *g;
  g = get_current_group();
  ev = (XButtonEvent *)(cb->event);
  if (ev->state & (snd_ControlMask | snd_MetaMask))
    g->tempo = g->old_tempo;
  else g->tempo = 1.0;
  reflect_mix_tempo(wb[g_tempo_scale],wb[g_tempo_val],g->tempo);
  if (g->mixes > 1)
    {
      refigure_tempo(ss,g);
      remix_file_over_group(g);
      set_group_bounds(g);
      remake_group_browser_label();
    }
} 

#define SCALE_COLUMN 4
#define VALUE_COLUMN 45
#define TEXT_COLUMN 55
#define RIGHT_COLUMN 96

static Widget group_basic_row(snd_state *ss, Widget *wb, Widget last_widget, 
			      int scale, int val, int text, int amp_label, 
			      int row, 
			      XtCallbackProc drag_callback, XtCallbackProc value_changed_callback, XtCallbackProc env_callback)
{
  int n;
  Arg args[32];
  
  if (amp_label)
    {
      n=0;
      XtSetArg(args[n],XmNtopAttachment,XmATTACH_WIDGET); n++;
      XtSetArg(args[n],XmNtopWidget,last_widget); n++;
      XtSetArg(args[n],XmNbottomAttachment,XmATTACH_NONE); n++;
      XtSetArg(args[n],XmNleftAttachment,XmATTACH_POSITION); n++;
      XtSetArg(args[n],XmNleftPosition,2); n++;
      XtSetArg(args[n],XmNrightAttachment,XmATTACH_POSITION); n++;
      XtSetArg(args[n],XmNrightPosition,SCALE_COLUMN+4); n++;
      XtSetArg(args[n],XmNfontList,button_FONT(ss)); n++;
      XtSetArg(args[n],XmNalignment,XmALIGNMENT_END); n++;	
      wb[amp_label] = XtCreateManagedWidget("",xmLabelWidgetClass,wb[g_form],args,n);
    }

  n=0;
  XtSetArg(args[n],XmNtopAttachment,XmATTACH_WIDGET); n++;
  XtSetArg(args[n],XmNtopWidget,last_widget); n++;
  XtSetArg(args[n],XmNbottomAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNleftAttachment,XmATTACH_POSITION); n++;
  XtSetArg(args[n],XmNuserData,(XtPointer)row); n++;
  XtSetArg(args[n],XmNdragCallback,make_callback_list(drag_callback,(XtPointer)ss)); n++;
  XtSetArg(args[n],XmNvalueChangedCallback,make_callback_list(value_changed_callback,(XtPointer)ss)); n++;
  if (amp_label)
    {
      XtSetArg(args[n],XmNleftPosition,SCALE_COLUMN+4); n++;
    }
  else
    {
      XtSetArg(args[n],XmNleftPosition,SCALE_COLUMN); n++;
    }
  XtSetArg(args[n],XmNrightAttachment,XmATTACH_POSITION); n++;
  XtSetArg(args[n],XmNrightPosition,VALUE_COLUMN); n++; 
  XtSetArg(args[n],XmNorientation,XmHORIZONTAL); n++;
  wb[scale] = XtCreateManagedWidget("",xmScaleWidgetClass,wb[g_form],args,n);

  n=0;
  XtSetArg(args[n],XmNtopAttachment,XmATTACH_OPPOSITE_WIDGET); n++;
  XtSetArg(args[n],XmNtopWidget,wb[scale]); n++;
  XtSetArg(args[n],XmNbottomAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNleftAttachment,XmATTACH_POSITION); n++;
  XtSetArg(args[n],XmNleftPosition,VALUE_COLUMN); n++;
  XtSetArg(args[n],XmNrightAttachment,XmATTACH_POSITION); n++;
  XtSetArg(args[n],XmNrightPosition,TEXT_COLUMN); n++;
  XtSetArg(args[n],XmNfontList,button_FONT(ss)); n++;
  XtSetArg(args[n],XmNalignment,XmALIGNMENT_BEGINNING); n++;
  XtSetArg(args[n],XmNresizable,FALSE); n++;	
  wb[val] = XtCreateManagedWidget("0.00",xmLabelWidgetClass,wb[g_form],args,n);
  
  n=0;
  XtSetArg(args[n],XmNtopAttachment,XmATTACH_OPPOSITE_WIDGET); n++;
  XtSetArg(args[n],XmNtopWidget,wb[val]); n++;
  XtSetArg(args[n],XmNbottomAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNleftAttachment,XmATTACH_POSITION); n++;
  XtSetArg(args[n],XmNleftPosition,TEXT_COLUMN); n++;
  XtSetArg(args[n],XmNrightAttachment,XmATTACH_POSITION); n++;
  XtSetArg(args[n],XmNrightPosition,RIGHT_COLUMN); n++;
  XtSetArg(args[n],XmNfontList,bold_button_FONT(ss)); n++;
  XtSetArg(args[n],XmNmarginHeight,1); n++;
  XtSetArg(args[n],XmNuserData,(XtPointer)row); n++;
  wb[text] = sndCreateTextFieldWidget(ss,"",wb[g_form],args,n);
  XtAddCallback(wb[text],XmNactivateCallback,env_callback,(XtPointer)ss);

  return(wb[text]);
}

static void create_group_browser(snd_state *ss)
{
  int n,i,j;
  Arg args[20];
  XmString helpstr,clearstr,dismissstr,titlestr;
  Widget last_widget;
  /* create overall browser */
  wb = (Widget *)calloc(g_arrays+GROUP_MAX_OUT_CHANS*g_amp_size+ss->ngroups,sizeof(Widget));

  n=0;
  if (!(ss->using_schemes)) n = background_main_color(args,n,ss);
  dismissstr=XmStringCreateLocalized(snd_string_Dismiss);
  XtSetArg(args[n],XmNokLabelString,dismissstr); n++;
  helpstr = XmStringCreateLocalized(snd_string_Help);
  XtSetArg(args[n],XmNhelpLabelString,helpstr); n++;
  clearstr = XmStringCreateLocalized(snd_string_Clear);
  XtSetArg(args[n],XmNcancelLabelString,clearstr); n++;
  titlestr = XmStringCreateLocalized(snd_string_Group_Editor);
  XtSetArg(args[n],XmNdialogTitle,titlestr); n++;
#if RESIZE_DIALOG
  XtSetArg(args[n],XmNresizePolicy,XmRESIZE_GROW); n++;
  XtSetArg(args[n],XmNnoResize,FALSE); n++;
#endif
#if UNSET_TRANSIENT
  XtSetArg(args[n],XmNtransient,FALSE); n++;
#endif
  XtSetArg(args[n],XmNautoUnmanage,FALSE); n++;
  wb[g_dialog] = XmCreateMessageDialog(main_PANE(ss),snd_string_Groups,args,n);
#if UNSET_TRANSIENT
  add_dialog(ss,wb[g_dialog]);
#endif

  XmStringFree(clearstr);
  XmStringFree(helpstr);
  XmStringFree(dismissstr);
  XmStringFree(titlestr);

  XtUnmanageChild(XmMessageBoxGetChild(wb[g_dialog],XmDIALOG_SYMBOL_LABEL));
  XtUnmanageChild(XmMessageBoxGetChild(wb[g_dialog],XmDIALOG_MESSAGE_LABEL));

  XtAddCallback(wb[g_dialog],XmNhelpCallback,Groups_Help_Callback,ss);
  XtAddCallback(wb[g_dialog],XmNcancelCallback,Groups_Clear_Callback,ss);
  XtAddCallback(wb[g_dialog],XmNokCallback,Groups_Dismiss_Callback,ss);

  n=0;
  wb[g_form] = XtCreateManagedWidget("gform",xmFormWidgetClass,wb[g_dialog],args,n);
  /* since we want to change the number of amp outs on the fly, if we use a form
   *   widget here, we have to have unused amp controls lying around which looks
   *   ugly (unmanaging them leaves an empty space!).  We also want various other
   *   things like an overall group waveform (for easy playing, etc), so...
   */

  n=0;
  XtSetArg(args[n],XmNtopAttachment,XmATTACH_FORM); n++;
  XtSetArg(args[n],XmNbottomAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNleftAttachment,XmATTACH_FORM); n++;
  XtSetArg(args[n],XmNrightAttachment,XmATTACH_NONE); n++;
  wb[g_times] = XtCreateManagedWidget("group",xmLabelWidgetClass,wb[g_form],args,n);
  
  n=0;
  XtSetArg(args[n],XmNtopAttachment,XmATTACH_FORM); n++;
  XtSetArg(args[n],XmNbottomAttachment,XmATTACH_OPPOSITE_WIDGET); n++;
  XtSetArg(args[n],XmNbottomWidget,wb[g_times]); n++;
  XtSetArg(args[n],XmNleftAttachment,XmATTACH_WIDGET); n++;
  XtSetArg(args[n],XmNleftWidget,wb[g_times]); n++;
  XtSetArg(args[n],XmNrightAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNfontList,button_FONT(ss)); n++;
  XtSetArg(args[n],XmNalignment,XmALIGNMENT_BEGINNING); n++;	
  wb[g_info] = XtCreateManagedWidget("",xmLabelWidgetClass,wb[g_form],args,n);
  
  n=0;
  XtSetArg(args[n],XmNorientation,XmHORIZONTAL); n++;
  XtSetArg(args[n],XmNtopAttachment,XmATTACH_FORM); n++;
  XtSetArg(args[n],XmNbottomAttachment,XmATTACH_OPPOSITE_WIDGET); n++;
  XtSetArg(args[n],XmNbottomWidget,wb[g_times]); n++;
  XtSetArg(args[n],XmNleftAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNrightAttachment,XmATTACH_FORM); n++;
  wb[g_row] = XmCreateRadioBox(wb[g_form],"grow",args,n);
  
  for (i=0;i<ss->ngroups;i++)
    {
      n=0;
#if NEED_TOGGLE_SIZE
      XtSetArg(args[n],XmNindicatorSize,TOGGLE_SIZE); n++;
#endif
      XtSetArg(args[n],XmNuserData,(XtPointer)i); n++;
      wb[g_arrays+i] = XtCreateManagedWidget("",xmToggleButtonWidgetClass,wb[g_row],args,n);
      XtAddCallback(wb[g_arrays+i],XmNvalueChangedCallback,Groups_Button_Callback,ss);
    }
  XtManageChild(wb[g_row]);
  
  n=0;
  XtSetArg(args[n],XmNtopAttachment,XmATTACH_WIDGET); n++;
  XtSetArg(args[n],XmNtopWidget,wb[g_times]); n++;
  XtSetArg(args[n],XmNbottomAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNleftAttachment,XmATTACH_FORM); n++;
  XtSetArg(args[n],XmNrightAttachment,XmATTACH_FORM); n++;
  XtSetArg(args[n],XmNorientation,XmHORIZONTAL); n++;
  XtSetArg(args[n],XmNseparatorType,XmDOUBLE_LINE); n++;
  wb[g_title_sep] = XtCreateManagedWidget("gtitlesep",xmSeparatorWidgetClass,wb[g_form],args,n);
  
  /* now for amps, speed, tempo each with label, scalers, text fields
     then various buttons,
     then amp and speed scalers for mix consoles
     */
  
  n=0;
  XtSetArg(args[n],XmNtopAttachment,XmATTACH_WIDGET); n++;
  XtSetArg(args[n],XmNtopWidget,wb[g_title_sep]); n++;
  XtSetArg(args[n],XmNbottomAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNleftAttachment,XmATTACH_FORM); n++;
  XtSetArg(args[n],XmNrightAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNshadowThickness,0); n++;
  XtSetArg(args[n],XmNhighlightThickness,0); n++;	 
  XtSetArg(args[n],XmNmultiClick,XmMULTICLICK_KEEP); n++;
  wb[g_amp_label] = XtCreateManagedWidget(snd_string_amp_p,xmPushButtonWidgetClass,wb[g_form],args,n);
  XtAddCallback(wb[g_amp_label],XmNactivateCallback,Groups_Amp_Click_Callback,ss);
#if OVERRIDE_TOGGLE
  override_toggle_translation(wb[g_amp_label]);
#endif
  
  n=0;
  XtSetArg(args[n],XmNtopAttachment,XmATTACH_WIDGET); n++;
  XtSetArg(args[n],XmNtopWidget,wb[g_title_sep]); n++;
  XtSetArg(args[n],XmNbottomAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNleftAttachment,XmATTACH_POSITION); n++;
  XtSetArg(args[n],XmNleftPosition,TEXT_COLUMN - 4); n++;
  XtSetArg(args[n],XmNrightAttachment,XmATTACH_NONE); n++;
  wb[g_env_label] = XtCreateManagedWidget(snd_string_env_p,xmLabelWidgetClass,wb[g_form],args,n);
  
  last_widget = wb[g_amp_label];
  for (i=0,j=g_arrays+ss->ngroups;i<GROUP_MAX_OUT_CHANS;i++,j+=g_amp_size)
    {
      last_widget = group_basic_row(ss,wb,last_widget,
				    j+g_amp_scale,j+g_amp_val,j+g_amp_env,j+g_amp_file,
				    i,group_drag_amp_callback,group_change_amp_callback,group_amp_env_callback);
    }
  
  n=0;
  XtSetArg(args[n],XmNtopAttachment,XmATTACH_WIDGET); n++;
  XtSetArg(args[n],XmNtopWidget,last_widget); n++;
  XtSetArg(args[n],XmNbottomAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNleftAttachment,XmATTACH_FORM); n++;
  XtSetArg(args[n],XmNrightAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNshadowThickness,0); n++;
  XtSetArg(args[n],XmNhighlightThickness,0); n++;	 
  XtSetArg(args[n],XmNmultiClick,XmMULTICLICK_DISCARD); n++;
  wb[g_speed_label] = XtCreateManagedWidget(snd_string_speed,xmPushButtonWidgetClass,wb[g_form],args,n);
  XtAddCallback(wb[g_speed_label],XmNactivateCallback,Groups_Speed_Click_Callback,ss);
#if OVERRIDE_TOGGLE
  override_toggle_translation(wb[g_speed_label]);
#endif
  
  last_widget = group_basic_row(ss,wb,wb[g_speed_label],g_speed_scale,g_speed_val,g_speed_env,0,
				    0,group_drag_speed_callback,group_change_speed_callback,group_speed_env_callback);

  n=0;
  XtSetArg(args[n],XmNtopAttachment,XmATTACH_WIDGET); n++;
  XtSetArg(args[n],XmNtopWidget,wb[g_speed_scale]); n++;
  XtSetArg(args[n],XmNbottomAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNleftAttachment,XmATTACH_FORM); n++;
  XtSetArg(args[n],XmNrightAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNshadowThickness,0); n++;
  XtSetArg(args[n],XmNhighlightThickness,0); n++;	 
  XtSetArg(args[n],XmNmultiClick,XmMULTICLICK_DISCARD); n++;
  wb[g_tempo_label] = XtCreateManagedWidget(snd_string_tempo,xmPushButtonWidgetClass,wb[g_form],args,n);
  XtAddCallback(wb[g_tempo_label],XmNactivateCallback,Groups_Tempo_Click_Callback,ss);
#if OVERRIDE_TOGGLE
  override_toggle_translation(wb[g_tempo_label]);
#endif
  
  last_widget = group_basic_row(ss,wb,wb[g_tempo_label],g_tempo_scale,g_tempo_val,g_tempo_env,0,
				    0,group_drag_tempo_callback,group_change_tempo_callback,group_tempo_env_callback);
  
  n=0;
  XtSetArg(args[n],XmNtopAttachment,XmATTACH_WIDGET); n++;
  XtSetArg(args[n],XmNtopWidget,last_widget); n++;
  XtSetArg(args[n],XmNbottomAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNleftAttachment,XmATTACH_POSITION); n++;
  XtSetArg(args[n],XmNleftPosition,SCALE_COLUMN); n++;
  XtSetArg(args[n],XmNrightAttachment,XmATTACH_NONE); n++;
  wb[g_play] = XtCreateManagedWidget(snd_string_play_this_group,xmToggleButtonWidgetClass,wb[g_form],args,n);
  XtAddCallback(wb[g_play],XmNvalueChangedCallback,Groups_Play_Callback,ss);
  
  n=0;
  XtSetArg(args[n],XmNtopAttachment,XmATTACH_OPPOSITE_WIDGET); n++;
  XtSetArg(args[n],XmNtopWidget,wb[g_play]); n++;
  XtSetArg(args[n],XmNbottomAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNleftAttachment,XmATTACH_POSITION); n++;
  XtSetArg(args[n],XmNleftPosition,TEXT_COLUMN); n++;
  XtSetArg(args[n],XmNrightAttachment,XmATTACH_NONE); n++;
  wb[g_entire] = XtCreateManagedWidget(snd_string_envelope_over_output,xmToggleButtonWidgetClass,wb[g_form],args,n);
  XtAddCallback(wb[g_entire],XmNvalueChangedCallback,Groups_Entire_Callback,ss);
  
  n=0;
  XtSetArg(args[n],XmNtopAttachment,XmATTACH_WIDGET); n++;
  XtSetArg(args[n],XmNtopWidget,wb[g_play]); n++;
  XtSetArg(args[n],XmNbottomAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNleftAttachment,XmATTACH_POSITION); n++;
  XtSetArg(args[n],XmNleftPosition,SCALE_COLUMN); n++;
  XtSetArg(args[n],XmNrightAttachment,XmATTACH_NONE); n++;
  wb[g_dur] = XtCreateManagedWidget(snd_string_show_mix_duration,xmToggleButtonWidgetClass,wb[g_form],args,n);
  XtAddCallback(wb[g_dur],XmNvalueChangedCallback,Groups_Duration_Callback,ss);
  
  n=0;
  XtSetArg(args[n],XmNtopAttachment,XmATTACH_WIDGET); n++;
  XtSetArg(args[n],XmNtopWidget,wb[g_dur]); n++;
  XtSetArg(args[n],XmNbottomAttachment,XmATTACH_NONE); n++;
  XtSetArg(args[n],XmNleftAttachment,XmATTACH_POSITION); n++;
  XtSetArg(args[n],XmNleftPosition,SCALE_COLUMN); n++;
  XtSetArg(args[n],XmNrightAttachment,XmATTACH_NONE); n++;
  wb[g_movies] = XtCreateManagedWidget(snd_string_show_waveform_changes_as_mix_changes,xmToggleButtonWidgetClass,wb[g_form],args,n);
  XtAddCallback(wb[g_movies],XmNvalueChangedCallback,Groups_Movies_Callback,ss);
  
#if MANAGE_DIALOG
  XtManageChild(wb[g_dialog]);
#endif
  
  if (!(ss->using_schemes))
    {
      map_over_children(wb[g_dialog],set_main_color_of_widget,(void *)ss);
      for (i=0;i<ss->ngroups;i++) {XtVaSetValues(wb[g_arrays+i],XmNselectColor,(ss->sgx)->red,NULL);}
      XtVaSetValues(wb[g_title_sep],XmNbackground,(ss->sgx)->white,NULL);
      XtVaSetValues(wb[g_times],XmNbackground,(ss->sgx)->high,NULL);
      XtVaSetValues(wb[g_play],XmNselectColor,(ss->sgx)->text,NULL);
      XtVaSetValues(wb[g_entire],XmNselectColor,(ss->sgx)->text,NULL);
      XtVaSetValues(wb[g_dur],XmNselectColor,(ss->sgx)->text,NULL);
      XtVaSetValues(wb[g_movies],XmNselectColor,(ss->sgx)->text,NULL);
      XtVaSetValues(XmMessageBoxGetChild(wb[g_dialog],XmDIALOG_OK_BUTTON),XmNarmColor,(ss->sgx)->text,NULL);
      XtVaSetValues(XmMessageBoxGetChild(wb[g_dialog],XmDIALOG_CANCEL_BUTTON),XmNarmColor,(ss->sgx)->text,NULL);
      XtVaSetValues(XmMessageBoxGetChild(wb[g_dialog],XmDIALOG_HELP_BUTTON),XmNarmColor,(ss->sgx)->text,NULL);
    }
}

void fire_up_group_browser(snd_state *ss)
{
  check_and_allocate_groups(ss);
  if (!wb) create_group_browser(ss);
  else raise_dialog(wb[g_dialog]);
  if (!(XtIsManaged(wb[g_dialog]))) XtManageChild(wb[g_dialog]);
  update_group_browser(ss);
}
