#include "snd.h"


/* -------- Keyboard Macros -------- */

#define MIN_KEY_CODE 0
#define MAX_KEY_CODE 65535
#define MIN_KEY_STATE 0
#define MAX_KEY_STATE 15

static bool defining_macro = false;
static int macro_cmd_size = 0;
static int macro_size = 0;
typedef struct {int keysym; int state;} macro_cmd;
static macro_cmd **macro_cmds = NULL;


static void allocate_macro_cmds(void)
{
  int old_size;
  old_size = macro_cmd_size;
  macro_cmd_size += 16;
  if (!macro_cmds)
    macro_cmds = (macro_cmd **)calloc(macro_cmd_size, sizeof(macro_cmd *));
  else 
    {
      int i;
      macro_cmds = (macro_cmd **)realloc(macro_cmds, macro_cmd_size * sizeof(macro_cmd *));
      for (i = old_size; i < macro_cmd_size; i++) macro_cmds[i] = NULL;
    }
}


static void start_defining_macro(void)
{
  macro_size = 0;
  defining_macro = true;
  if ((!macro_cmds) || (macro_size == macro_cmd_size)) allocate_macro_cmds();
}


static void stop_defining_macro(void)
{
  /* the last C-x ) went into the macro before we noticed it should not have */
  macro_size -= 2;
  defining_macro = false;
}


static void execute_last_macro(chan_info *cp, int count)
{
  int i, j;
  if ((macro_cmds) && (macro_size > 0))
    for (j = 0; j < count; j++)
      for (i = 0; i < macro_size; i++) 
	keyboard_command(cp, 
			 macro_cmds[i]->keysym, 
			 macro_cmds[i]->state);
}


static void continue_macro(int keysym, int state)
{
  if (!(macro_cmds[macro_size])) macro_cmds[macro_size] = (macro_cmd *)calloc(1, sizeof(macro_cmd));
  macro_cmds[macro_size]->keysym = keysym;
  macro_cmds[macro_size]->state = state;
  macro_size++;
  if (macro_size == macro_cmd_size) 
    allocate_macro_cmds();
}




/* ---------------- keys ---------------- */

typedef struct {
  int key; 
  int state; 
  int args; 
  Xen func; 
  bool cx_extended; /* Sun/Forte C defines "extended" somewhere */
  const char *origin, *prefs_info;
  int gc_loc;
} key_entry; 

static key_entry *keymap = NULL;
static int keymap_size = 0;
static int keymap_top = 0;


int in_keymap(int key, int state, bool cx_extended)
{
  int i;
  if (keymap_top == 0) return(-1);
  for (i = 0; i < keymap_top; i++)
    if ((keymap[i].key == key) && 
	(keymap[i].state == state) &&
	(keymap[i].cx_extended == cx_extended) && 
	(Xen_is_bound(keymap[i].func)))
      return(i);
  return(-1);
}


#if HAVE_SCHEME || HAVE_FORTH
  #define kbd_false 0
#else
  #define kbd_false Xen_false
#endif

#define NUM_BUILT_IN_KEYS 81

static key_entry built_in_keys[NUM_BUILT_IN_KEYS] = {
  {snd_K_Down,       0, 0, kbd_false, false, "zoom out",                                                 NULL, -1},
  {snd_K_Up,         0, 0, kbd_false, false, "zoom in",                                                  NULL, -1},
  {snd_K_Left,       0, 0, kbd_false, false, "move window left",                                         NULL, -1},
  {snd_K_Right,      0, 0, kbd_false, false, "move window right",                                        NULL, -1},
  {snd_keypad_Down,  0, 0, kbd_false, false, "zoom fft out",                                             NULL, -1},
  {snd_keypad_Up,    0, 0, kbd_false, false, "zoom fft in",                                              NULL, -1},
  {snd_keypad_Left,  0, 0, kbd_false, false, "move fft left",                                            NULL, -1},
  {snd_keypad_Right, 0, 0, kbd_false, false, "move fft right",                                           NULL, -1},
  {snd_K_less,       0, 0, kbd_false, false, "move cursor to sample 0",                                  NULL, -1},
  {snd_K_greater,    0, 0, kbd_false, false, "move cursor to last sample",                               NULL, -1},
  {snd_K_space,      0, 0, kbd_false, false, "play from cursor or stop playing",                         NULL, -1},

  {snd_K_less,       ControlMask, 0, kbd_false, false, "move cursor to sample 0",                    NULL, -1},
  {snd_K_greater,    ControlMask, 0, kbd_false, false, "move cursor to last sample",                 NULL, -1},
  {snd_K_a,          ControlMask, 0, kbd_false, false, "move cursor to window start",                NULL, -1},
  {snd_K_b,          ControlMask, 0, kbd_false, false, "move cursor back one pixel",                 NULL, -1},
  {snd_K_d,          ControlMask, 0, kbd_false, false, "delete sample at cursor",                    NULL, -1},
  {snd_K_e,          ControlMask, 0, kbd_false, false, "move cursor to window end",                  NULL, -1},
  {snd_K_f,          ControlMask, 0, kbd_false, false, "move cursor ahead one pixel",                NULL, -1},
  {snd_K_g,          ControlMask, 0, kbd_false, false, "abort current command",                      NULL, -1},
  {snd_K_h,          ControlMask, 0, kbd_false, false, "delete previous sample",                     NULL, -1},
  {snd_K_i,          ControlMask, 0, kbd_false, false, "display cursor info",                        NULL, -1},
  {snd_K_j,          ControlMask, 0, kbd_false, false, "goto mark",                                  NULL, -1},
  {snd_K_k,          ControlMask, 0, kbd_false, false, "delete one line's worth of samples",         NULL, -1},
  {snd_K_l,          ControlMask, 0, kbd_false, false, "position window so cursor is in the middle", NULL, -1},
  {snd_K_m,          ControlMask, 0, kbd_false, false, "place (or remove) mark at cursor location",  NULL, -1},
  {snd_K_n,          ControlMask, 0, kbd_false, false, "move cursor ahead one 'line'",               NULL, -1},
  {snd_K_o,          ControlMask, 0, kbd_false, false, "insert one zero sample at cursor",           NULL, -1},
  {snd_K_p,          ControlMask, 0, kbd_false, false, "move cursor back one 'line'",                NULL, -1},
  {snd_K_q,          ControlMask, 0, kbd_false, false, "play current channel starting at cursor",    "play-channel-from-cursor", -1},
  {snd_K_s,          ControlMask, 0, kbd_false, false, "search",                                     NULL, -1},
  {snd_K_t,          ControlMask, 0, kbd_false, false, "stop playing",                               NULL, -1},
  {snd_K_u,          ControlMask, 0, kbd_false, false, "start arg count definition.",                NULL, -1},
  {snd_K_v,          ControlMask, 0, kbd_false, false, "move cursor to mid-window",                  NULL, -1},
  {snd_K_w,          ControlMask, 0, kbd_false, false, "delete selection",                           "delete-selection", -1},
  {snd_K_y,          ControlMask, 0, kbd_false, false, "insert selection.",                          "insert-selection", -1},
  {snd_K_z,          ControlMask, 0, kbd_false, false, "set sample at cursor to 0.0",                NULL, -1},
  {snd_K_underscore, ControlMask, 0, kbd_false, false, "undo",                                       NULL, -1},
  {snd_K_space,      ControlMask, 0, kbd_false, false, "start selection definition",                 NULL, -1},
  {snd_K_g,          ControlMask | MetaMask, 0, kbd_false, false, "clear listener",              NULL, -1},
  {snd_K_less,       MetaMask,    0, kbd_false, false, "move cursor to sample 0",                    NULL, -1},
  {snd_K_greater,    MetaMask,    0, kbd_false, false, "move cursor to last sample",                 NULL, -1},

  {snd_K_b,          0, 0, kbd_false, true, "position window so cursor is on left margin",               NULL, -1},
  {snd_K_c,          0, 0, kbd_false, true, "define selection from cursor to nth mark",                  NULL, -1},
  {snd_K_e,          0, 0, kbd_false, true, "execute keyboard macro",                                    NULL, -1},
  {snd_K_f,          0, 0, kbd_false, true, "position window so cursor is on right margin",              NULL, -1},
  {snd_K_k,          0, 0, kbd_false, true, "close file",                                                NULL, -1},
  {snd_K_l,          0, 0, kbd_false, true, "position selection in mid-view",                            NULL, -1},
  {snd_K_o,          0, 0, kbd_false, true, "move to next or previous graph",                            NULL, -1},
  {snd_K_p,          0, 0, kbd_false, true, "play selection or region n",                                "play-selection", -1},
  {snd_K_q,          0, 0, kbd_false, true, "mix in selection",                                          "mix-selection", -1},
  {snd_K_r,          0, 0, kbd_false, true, "redo",                                                      NULL, -1},
  {snd_K_u,          0, 0, kbd_false, true, "undo",                                                      NULL, -1},
  {snd_K_v,          0, 0, kbd_false, true, "position window over selection",                            NULL, -1},
  {snd_K_z,          0, 0, kbd_false, true, "smooth selection",                                          NULL, -1},
  {snd_K_openparen,  0, 0, kbd_false, true, "begin keyboard macro definition",                           NULL, -1},
  {snd_K_closeparen, 0, 0, kbd_false, true, "end keyboard macro definition",                             NULL, -1},

  {snd_K_b, ControlMask, 0, kbd_false, true, "set x window bounds (preceded by 1 arg)",              NULL, -1},
  {snd_K_c, ControlMask, 0, kbd_false, true, "hide control panel",                                   NULL, -1},
  {snd_K_f, ControlMask, 0, kbd_false, true, "open file",                                            NULL, -1},
  {snd_K_g, ControlMask, 0, kbd_false, true, "abort command",                                        NULL, -1},
  {snd_K_o, ControlMask, 0, kbd_false, true, "show control panel",                                   NULL, -1},
  {snd_K_p, ControlMask, 0, kbd_false, true, "set window size (preceded by 1 arg)",                  NULL, -1},
  {snd_K_q, ControlMask, 0, kbd_false, true, "mix in file",                                          NULL, -1},
  {snd_K_r, ControlMask, 0, kbd_false, true, "redo",                                                 "save-sound", -1},
  {snd_K_s, ControlMask, 0, kbd_false, true, "save file",                                            NULL, -1},
  {snd_K_u, ControlMask, 0, kbd_false, true, "undo",                                                 NULL, -1},
  {snd_K_v, ControlMask, 0, kbd_false, true, "set window size as percentage of total",               NULL, -1},
  {snd_K_w, ControlMask, 0, kbd_false, true, "save current channel in file",                         NULL, -1},
  {snd_K_z, ControlMask, 0, kbd_false, true, "smooth using cosine",                                  NULL, -1},
};


void map_over_keys(bool (*func)(int key, int state, bool cx, Xen xf))
{
  int i;
  for (i = 0; i < keymap_top; i++)
    if ((Xen_is_bound(keymap[i].func)) &&
	((*func)(keymap[i].key, 
		 keymap[i].state, 
		 keymap[i].cx_extended, 
		 keymap[i].func)))
      return;
}


static key_info *make_key_info(key_entry k)
{
  key_info *ki;
  ki = (key_info *)calloc(1, sizeof(key_info));
#if USE_MOTIF
  ki->key = XKeysymToString(k.key); /* no free! */
#endif
  ki->c = k.state & ControlMask;
  ki->m = k.state & MetaMask;
  ki->x = k.cx_extended;
  return(ki);
}


key_info *find_prefs_key(const char *prefs_name)
{
  int i;
  key_info *ki;
  for (i = 0; i < keymap_top; i++)
    if ((Xen_is_bound(keymap[i].func)) &&
	(mus_strcmp(keymap[i].prefs_info, prefs_name)))
      return(make_key_info(keymap[i]));

  for (i = 0; i < NUM_BUILT_IN_KEYS; i++)
    if (mus_strcmp(built_in_keys[i].prefs_info, prefs_name))
      return(make_key_info(built_in_keys[i]));

  ki = (key_info *)calloc(1, sizeof(key_info));
  ki->key = NULL;
  ki->c = false;
  ki->m = false;
  ki->x = false;
  return(ki);
}


char *key_description(int key, int state, bool cx_extended)
{
  int pos;
  if ((key < MIN_KEY_CODE) || (key > MAX_KEY_CODE) ||
      (state < MIN_KEY_STATE) || (state > MAX_KEY_STATE))
    return(NULL);
  pos = in_keymap(key, state, cx_extended);
  if (pos < 0) pos = in_keymap(key, state, cx_extended);
  if (pos >= 0)
    {
      if (keymap[pos].origin)
	return(mus_strdup(keymap[pos].origin));
      return(mus_strdup("something indescribable")); /* NULL would mean "no binding" */
    }
  for (pos = 0; pos < NUM_BUILT_IN_KEYS; pos++)
    if ((built_in_keys[pos].key == key) && 
	(built_in_keys[pos].state == state) && 
	(built_in_keys[pos].cx_extended == cx_extended))
      return(mus_strdup(built_in_keys[pos].origin));
  return(NULL);
}


void set_keymap_entry(int key, int state, int args, Xen func, bool cx_extended, const char *origin, const char *prefs_info)
{
  int i;
  i = in_keymap(key, state, cx_extended);
  if (i == -1)
    {
      if (keymap_size == keymap_top)
	{
	  keymap_size += 16;
	  if (keymap_top == 0)
	    {
	      keymap = (key_entry *)calloc(keymap_size, sizeof(key_entry));
	      for (i = 0; i < keymap_size; i++) 
		keymap[i].func = Xen_undefined;
	    }
	  else 
	    {
	      keymap = (key_entry *)realloc(keymap, keymap_size * sizeof(key_entry));
	      for (i = keymap_top; i < keymap_size; i++) 
		{
		  keymap[i].key = 0; 
		  keymap[i].state = 0; 
		  keymap[i].func = Xen_undefined;
		  keymap[i].cx_extended = false;
		  keymap[i].origin = NULL;
		  keymap[i].prefs_info = NULL;
		  keymap[i].gc_loc = NOT_A_GC_LOC;
		}
	    }
	}
      keymap[keymap_top].key = key;
      keymap[keymap_top].state = state;
      keymap[keymap_top].cx_extended = cx_extended;
      check_menu_labels(key, state, cx_extended);
      i = keymap_top;
      keymap_top++;
    }
  else
    {
      if ((Xen_is_procedure(keymap[i].func)) &&
	  (keymap[i].gc_loc != NOT_A_GC_LOC))
	{
	  snd_unprotect_at(keymap[i].gc_loc);
	  keymap[i].gc_loc = NOT_A_GC_LOC;
	}
      if (keymap[i].origin)
	{
	  /* this is silly... */
	  char *tmp;
	  tmp = (char *)keymap[i].origin;
	  keymap[i].origin = NULL;
	  free(tmp);
	}
      if (keymap[i].prefs_info)
	{
	  char *tmp;
	  tmp = (char *)keymap[i].prefs_info;
	  keymap[i].prefs_info = NULL;
	  free(tmp);
	}
    }
  keymap[i].origin = mus_strdup(origin);
  keymap[i].prefs_info = mus_strdup(prefs_info);
  keymap[i].args = args;
  keymap[i].func = func;
  if (Xen_is_procedure(func)) 
    keymap[i].gc_loc = snd_protect(func);
}


static void call_keymap(int hashedsym, int count)
{
  kbd_cursor_t res = KEYBOARD_NO_ACTION;

  if (Xen_is_bound(keymap[hashedsym].func))
    {
      /* not _NO_CATCH here because the code is not protected at any higher level
       * if key-bind function returns some impossible cursor-action, return keyboard-no-action
       */
      Xen result;
      if (keymap[hashedsym].args == 0)
	result = Xen_call_with_no_args(keymap[hashedsym].func, keymap[hashedsym].origin);
      else result = Xen_call_with_1_arg(keymap[hashedsym].func, C_int_to_Xen_integer(count), keymap[hashedsym].origin);
      if (Xen_is_integer(result))
	{
	  int r;
	  r = (int)Xen_integer_to_C_int(result);
	  if ((r <= KEYBOARD_NO_ACTION) &&
	      (r >= CURSOR_IN_VIEW))
	    res = (kbd_cursor_t)r;
	}
    }
  handle_cursor(selected_channel(), res);
}




/* ---------------- other kbd built-in commands ---------------- */

static void cursor_moveto_end(chan_info *cp)
{
  cursor_moveto(cp, current_samples(cp) - 1);
}


static void set_window_bounds(chan_info *cp, int count) 
{
  /* count = sample number to start at */
  axis_info *ap;
  ap = cp->axis;
  if (ap->x_ambit != 0.0)
    {
      double sx;
      sx = (((double)count / (double)snd_srate(cp->sound)) - ap->xmin) / ap->x_ambit;
      reset_x_display(cp, sx, ap->zx);
    }
}


static void set_window_size(chan_info *cp, int count) 
{
  /* set samples within window */
  axis_info *ap;
  ap = cp->axis;
  if (ap->x_ambit != 0.0)
    {
      double zx;
      zx = ((double)count / (((double)snd_srate(cp->sound)) * ap->x_ambit));
      reset_x_display(cp, ap->sx, zx);
    }
}


static void set_window_percentage(chan_info *cp, int count) 
{
  /* set percentage of file within window */
  axis_info *ap;
  double zx;
  ap = cp->axis;
  zx = (double)count / (double)snd_srate(cp->sound);
  reset_x_display(cp, ap->sx, zx);
}


static void window_framples_selection(chan_info *cp)
{
  double x0, x1;
  int i;
  x0 = (((double)(selection_beg(cp))) / ((double)snd_srate(cp->sound)));
  x1 = x0 + ((double)(selection_len())) / ((double)(snd_srate(cp->sound)));
  set_x_axis_x0x1(cp, x0, x1);
  for (i = 0; i < ss->max_sounds; i++)
    {
      snd_info *sp;
      sp = ss->sounds[i];
      if ((sp) && 
	  (sp->inuse == SOUND_NORMAL) && 
	  (cp->sound != sp) && 
	  (selection_is_active_in_channel(sp->chans[0])) && 
	  (sp->sync != (cp->sound->sync)))
	set_x_axis_x0x1(sp->chans[0], x0, x1);
    }
}


static chan_info *goto_next_graph(chan_info *cp, int count);

static chan_info *goto_previous_graph(chan_info *cp, int count)
{
  snd_info *sp;
  chan_info *ncp, *vcp;
  int i, k, j, chan;
  if (count == 0) return(cp);
  sp = cp->sound;
  if (sp->inuse != SOUND_NORMAL) return(cp);
  vcp = virtual_selected_channel(cp);
  chan = vcp->chan;
  ncp = NULL;
  if (count < 0) 
    k = -count; 
  else return(goto_next_graph(cp, count)); 
  if (chan > 0)
    {
      /* goto previous channel in current sound */
      k -= chan;
      if (k <= 0)
	ncp = sp->chans[chan + count];
    }
  while (k > 0)
    {
      /* look for previous sound, (wrap around) */
      /* goto channel n */
      for (i = (sp->index - 1); i >= 0; i--)
	if ((snd_ok(ss->sounds[i])) && (ss->sounds[i]->inuse == SOUND_NORMAL))
	  {
	    sp = (snd_info *)(ss->sounds[i]);
	    j = k;
	    k -= sp->nchans;
	    if (k <= 0)
	      ncp = sp->chans[sp->nchans - j];
	    break;
	  }
      if (k > 0)
	for (i = ss->max_sounds - 1; i >= sp->index; i--)
	  if ((snd_ok(ss->sounds[i])) && (ss->sounds[i]->inuse == SOUND_NORMAL))
	    {
	      sp = (snd_info *)(ss->sounds[i]);
	      j = k;
	      k -= sp->nchans;
	      if (k <= 0)
		ncp = sp->chans[sp->nchans - j];
	      break;
	    }
    }
  if (ncp == vcp) return(ncp);
  if (!ncp) snd_error_without_format("goto previous graph failed!");
  select_channel(ncp->sound, ncp->chan);
  return(ncp);
}


static chan_info *goto_next_graph(chan_info *cp, int count)
{
  snd_info *sp;
  chan_info *ncp, *vcp;
  int i, k, j, chan;
  if (count == 0) return(cp);
  sp = cp->sound;
  if (sp->inuse != SOUND_NORMAL) return(cp);
  vcp = virtual_selected_channel(cp);
  chan = vcp->chan;
  ncp = NULL;
  if (count < 0) 
    return(goto_previous_graph(cp, count)); 
  k = count;
  if (chan < ((int)(sp->nchans) - 1))
    {
      /* goto next channel in current sound */
      k -= (sp->nchans-chan - 1);
      if (k <= 0)
	ncp = sp->chans[chan + count];
    }
  while (k > 0)
    {
      /* look for next sound, (wrap around) */
      /* goto channel 0 */
      for (i = (sp->index + 1); i < ss->max_sounds; i++)
	if ((snd_ok(ss->sounds[i])) && (ss->sounds[i]->inuse == SOUND_NORMAL))
	  {
	    sp = (snd_info *)(ss->sounds[i]);
	    j = k;
	    k -= sp->nchans;
	    if (k <= 0)
	      ncp = sp->chans[j - 1];
	    break;
	  }
      if (k > 0)
	for (i = 0; i <= sp->index; i++)
	  if ((snd_ok(ss->sounds[i])) && (ss->sounds[i]->inuse == SOUND_NORMAL))
	    {
	      sp = (snd_info *)(ss->sounds[i]);
	      j = k;
	      k -= sp->nchans;
	      if (k <= 0)
		ncp = sp->chans[j - 1];
	      break;
	    }
    }
  if (ncp == vcp) return(ncp);
  if (!ncp) snd_error_without_format("goto next graph failed!");
  select_channel(ncp->sound, ncp->chan);
  return(ncp);
}


void save_edits_from_kbd(snd_info *sp)
{
  /* this used to prompt for confirmation, but we now use a dialog
   */
  redirect_everything_to(printout_to_status_area, (void *)sp);
#if (!USE_NO_GUI)
  {
    io_error_t err;
    err = save_edits(sp);
    if (err == IO_NEED_WRITE_CONFIRMATION)
      changed_file_dialog(sp);
  }
#else
  save_edits_without_asking(sp);
#endif

  redirect_everything_to(NULL, NULL);
}


/* ---------------- key response ---------------- */

static mus_long_t get_count_1(char *number_buffer, int number_ctr, bool dot_seen, chan_info *cp)
{
  /* allow floats here = secs */
  int i;
  if (number_ctr == 0) return(1); /* c-u followed by nothing = 1 */
  number_buffer[number_ctr] = '\0';
  if (number_ctr == 1)
    { /* handle special cases of just - or + */
      if (number_buffer[0] == '-') return(-1);
      if (number_buffer[0] == '+') return(1);
      if (number_buffer[0] == '.') return(0);  /* -. and +. -> 0 from sscanf */
    }
  if (dot_seen)
    {
      float f;
      if (!(sscanf(number_buffer, "%12f", &f))) /* 12 is number_buffer size */
	{
	  /* this doesn't happen for most bogus cases -- the C spec says "it is not possible to determine
	   *    directly whether matches of literal character in the control string succeed or fail."
	   *    but really bad stuff like C-u ... C-f does get flagged.
	   */
	  snd_error("invalid number: %s", number_buffer);
	  return(0);
	}
      return((mus_long_t)(f * snd_srate(cp->sound)));
    }
  if (!(sscanf(number_buffer, "%12d", &i)))
    {
      snd_error("invalid number: %s", number_buffer);
      return(0);
    }
  return(i);
}


static mus_long_t get_count(char *number_buffer, int number_ctr, bool dot_seen, chan_info *cp, bool mark_wise)
{
  mus_long_t val, old_cursor;
  val = get_count_1(number_buffer, number_ctr, dot_seen, cp);
  if (!mark_wise) return(val);
  old_cursor = cursor_sample(cp);
  if (!(goto_mark(cp, val)))
    set_status(cp->sound, "no such mark", false);
  val = cursor_sample(cp) - old_cursor; /* will be 0 if no relevant marks */
  cursor_sample(cp) = old_cursor;
  return(val);
}


#define NUMBER_BUFFER_SIZE 12

static mus_float_t state_amount(int state)
{
  mus_float_t amount;
  amount = 1.0;
  if (state & ControlMask) amount *= 0.5;
  if (state & MetaMask) amount *= 0.5;
  if (state & ShiftMask) amount *= 0.5;
  return(amount);
}


static void zoom_fft(mus_float_t amount)
{
  mus_float_t zx, mx;
  zx = spectrum_end(ss) - spectrum_start(ss);
  mx = (spectrum_end(ss) + spectrum_start(ss)) * 0.5;
  zx *= amount;
  mx -= (zx * 0.5);
  if (mx < 0.0) mx = 0.0;
  set_spectrum_start(mx);
  if ((mx + zx) <= 1.0)
    set_spectrum_end(mx + zx);
  else set_spectrum_end(1.0);
}


static void move_fft(mus_float_t amount)
{
  mus_float_t mx, zx;
  zx = spectrum_end(ss) - spectrum_start(ss);
  mx = spectrum_start(ss) + zx * amount;
  if (mx < 0.0) mx = 0.0;
  if (mx >= 1.0) mx = 1.0 - zx;
  set_spectrum_start(mx);
  if ((mx + zx) <= 1.0)
    set_spectrum_end(mx + zx);
  else set_spectrum_end(1.0);
}


static bool stop_selecting(int keysym, int state)
{
  return(((state & ControlMask) == 0) ||
	 (keysym == snd_K_D) || (keysym == snd_K_d) ||
	 (keysym == snd_K_H) || (keysym == snd_K_h) ||
	 (keysym == snd_K_Y) || (keysym == snd_K_y));
}


static const char *key_to_name(int keysym) 
{
  if (keysym) 
    {
      const char *str;
      str = KEY_TO_NAME(keysym);
      if (str) return(str);
    }
  return("NUL");
}


static int number_ctr = 0;
static bool dot_seen = false;
static bool counting = false;
static bool extended_mode = false;

void control_g(snd_info *sp)
{
  ss->C_g_typed = true;
  number_ctr = 0; 
  counting = false; 
  dot_seen = false; 
  extended_mode = false;
  /* if (selection_is_active()) deactivate_selection(); */
  defining_macro = false;
  clear_stdin();
  redirect_everything_to(NULL, NULL);
  if (sp) clear_status_area(sp); /* do this before stop_playing! */

  if ((ss->checking_explicitly) || 
      (play_in_progress())) 
    ss->stopped_explicitly = true; 
  /* this tries to break out of long filter/src computations (and perhaps others) */
  /*   but, as in other such cases, it leaves this flag set so all subsequent uses of it need to clear it first */

  stop_playing_all_sounds(PLAY_C_G); /* several scm files assume hooks called upon C-g -- could be region play, etc */
  if (sp)
    {
      if (sp->applying) stop_applying(sp);
      for_each_sound_chan(sp, stop_fft_in_progress);
    }

  if (selection_creation_in_progress())
    deactivate_selection();
}

#ifndef SND_KEYMASK
  #define SND_KEYMASK (ControlMask | MetaMask)
#endif

#if HAVE_EXTENSION_LANGUAGE && (!USE_NO_GUI)
  static chan_info *last_searcher = NULL;
#endif


void keyboard_command(chan_info *cp, int keysym, int unmasked_state)
{
  /* we can't use the meta bit in some cases because this is trapped at a higher level for the Menu mnemonics */
  /* state here is the kbd bucky-bit state -- it might have bogus junk like NumLock */
  /* keysym has Shift taken into account already (see snd-xchn.c XKeycodeToKeysym, and same snd-xsnd.c) */

  static bool u_count = false;
  static char number_buffer[NUMBER_BUFFER_SIZE];
  static mus_long_t count = 1;
  static bool got_count = false;
  static bool m = false;
  int shift = 0;
  int hashloc, i, state;
  static mus_long_t ext_count = 1;
  static bool got_ext_count = false;
  snd_info *sp;
  axis_info *ap;

  /* fprintf(stderr, "%d (%s %d%s) ", keysym, KEY_TO_NAME(keysym), unmasked_state, (extended_mode) ? " (c-x)" : ""); */

  if ((!cp) || (!(cp->sound)) || (cp->active < CHANNEL_HAS_EDIT_LIST)) return;
  sp = cp->sound;
  if ((!sp) || (sp->inuse != SOUND_NORMAL)) return;

  ap = cp->axis;

  if (keysym >= snd_K_Shift_L) return;
  /* this happens when the user presses Control or Shift etc prior to hitting the actual (modified) key */
  shift = unmasked_state & ShiftMask;
  state = unmasked_state & SND_KEYMASK; /* mask off stuff we don't care about */

  if (defining_macro) continue_macro(keysym, state);
  if (!m) count = 1; else m = false;
  
  if ((selection_creation_in_progress()) && (!counting) &&
      ((extended_mode) || (stop_selecting(keysym, state))))
    finish_selection_creation();

  if ((counting) && (((keysym < snd_K_0) || (keysym > snd_K_9)) && 
		     ((keysym < snd_keypad_0) || (keysym > snd_keypad_9)) && /* these are in order in both Motif (X11) and gdk */
		     (keysym != snd_K_minus) && 
		     (keysym != snd_K_period) && 
		     (keysym != snd_keypad_Decimal) &&
		     (keysym != snd_K_plus)))
    {
      m = ((u_count) && 
	   ((keysym == snd_K_M) || (keysym == snd_K_m)));
      count = get_count(number_buffer, number_ctr, dot_seen, cp, m);
      got_count = true;
      number_ctr = 0;
      counting = false;
      dot_seen = false;
      if (m) return;
    }

  u_count = false;

  if ((keysym != snd_K_X) && (keysym != snd_K_x) &&
      (keysym != snd_K_B) && (keysym != snd_K_b) &&
      (keysym != snd_K_F) && (keysym != snd_K_f))
    {
      got_count = false;
      if (count == 0) return;
    }

#if HAVE_EXTENSION_LANGUAGE
  hashloc = in_keymap(keysym, state, extended_mode);
  if (hashloc != -1)                       /* found user-defined key */
    {
      extended_mode = false;
      call_keymap(hashloc, count);
      return;
    }
#endif

  if (state & ControlMask)
    {
      if (!extended_mode)
	{
	  mus_long_t loc;
	  /* -------------------------------- C-key -------------------------------- */
	  switch (keysym)
	    {
	    case snd_K_A: case snd_K_a: 
	      cp->cursor_on = true; 
	      loc = (mus_long_t)(ap->x0 * snd_srate(sp)); 
	      if ((loc + 1) == ap->losamp) loc = ap->losamp; /* handle dumb rounding problem */
	      cursor_moveto(cp, loc); 
	      break;

	    case snd_K_B: case snd_K_b: 
	      /* this was by samples which is nuts if we're looking at a large window -- should be by pixel, I think */
	      {
		int samples_per_pixel = 1;
		if ((!got_count) &&                              /* C-u 2.1 C-b moves back 2.1 seconds */
		    (cp->axis->x_axis_x1 > cp->axis->x_axis_x0)) /* avoid divide by zero if window tiny */
		  {
		    samples_per_pixel = (cp->axis->hisamp - cp->axis->losamp) / (cp->axis->x_axis_x1 - cp->axis->x_axis_x0);
		    if (samples_per_pixel < 1) samples_per_pixel = 1;
		  }
		cp->cursor_on = true; 
		cursor_move(cp, -count * samples_per_pixel);
		got_count = false;
	      }
	      break;

	    case snd_K_D: case snd_K_d: 
	      cp->cursor_on = true; 
	      cursor_delete(cp, count); 
	      break;

	    case snd_K_E: case snd_K_e:
	      cp->cursor_on = true; 
	      loc = (mus_long_t)(ap->x1 * (double)snd_srate(sp));
	      if ((loc + 1) == ap->hisamp) loc = ap->hisamp;
	      cursor_moveto(cp, loc); 
	      break;

	    case snd_K_F: case snd_K_f:
	      {
		int samples_per_pixel = 1;
		if ((!got_count) &&
		    (cp->axis->x_axis_x1 > cp->axis->x_axis_x0))
		  {
		    samples_per_pixel = (cp->axis->hisamp - cp->axis->losamp) / (cp->axis->x_axis_x1 - cp->axis->x_axis_x0);
		    if (samples_per_pixel < 1) samples_per_pixel = 1;
		  }
		cp->cursor_on = true; 
		cursor_move(cp, count * samples_per_pixel); 
		got_count = false;
	      }
	      break;

	    case snd_K_G: case snd_K_g: 
	      if (state & MetaMask)
		clear_listener();
	      else control_g(sp);
	      break;

	    case snd_K_H: case snd_K_h: 
	      cp->cursor_on = true; 
	      cursor_delete(cp, -count); 
	      break; 

	    case snd_K_I: case snd_K_i: 
	      show_cursor_info(cp); 
	      break;

	    case snd_K_J: case snd_K_j: 
	      cp->cursor_on = true; 
	      if (!(goto_mark(cp, count)))
		set_status(cp->sound, "no such mark", false);
	      break;

	    case snd_K_K: case snd_K_k: 
	      cp->cursor_on = true; 
	      cursor_delete(cp, count * 128);
	      break;

	    case snd_K_L: case snd_K_l: 
	      cp->cursor_on = true; 
	      handle_cursor_with_sync(cp, CURSOR_IN_MIDDLE);
	      break;

	    case snd_K_M: case snd_K_m:
	      {
		mark *mk = NULL;
		if (count > 0) 
		  {
		    cp->cursor_on = true;
		    set_show_marks(true);
		    mk = add_mark(cursor_sample(cp), NULL, cp);
		    display_channel_marks(cp);
		  }
		else 
		  {
		    if (!(delete_mark_samp(cursor_sample(cp), cp)))
		      status_report(cp->sound, "no mark at sample %" print_mus_long, cursor_sample(cp));
		  }
		if ((keysym == snd_K_M) && 
		    (cp->sound->sync != 0))
		  {
		    sync_info *si;
		    int sync_num;
		    sync_num = mark_sync_max() + 1; 
		    if (mk) set_mark_sync(mk, sync_num);
		    si = snd_sync(cp->sound->sync);
		    for (i = 0; i < si->chans; i++) 
		      if (cp != si->cps[i])
			{
			  if (count > 0)
			    {
			      mk = add_mark(cursor_sample(cp), NULL, si->cps[i]);
			      if (mk)
				{
				  set_mark_sync(mk, sync_num);
				  display_channel_marks(si->cps[i]);
				}
			    }
			  else 
			    {
			      if (!(delete_mark_samp(cursor_sample(cp), si->cps[i])))
				status_report(cp->sound, "no mark at sample %" print_mus_long, cursor_sample(cp));
			    }
			}
		    si = free_sync_info(si);
		  }
	      }
	      break;

	    case snd_K_N: case snd_K_n: 
	      cp->cursor_on = true; 
	      cursor_move(cp, count * 128); 
	      break;

	    case snd_K_O: case snd_K_o: 
	      cp->cursor_on = true; 
	      cursor_insert(cp, cursor_sample(cp), count); 
	      break;

	    case snd_K_P: case snd_K_p: 
	      cp->cursor_on = true; 
	      cursor_move(cp, -count * 128); 
	      break;

	    case snd_K_Q: case snd_K_q: 
	      play_channel(cp, cursor_sample(cp), NO_END_SPECIFIED);
	      break;

#if HAVE_EXTENSION_LANGUAGE && (!USE_NO_GUI)
	    case snd_K_S: case snd_K_s: 
	      if ((cp == last_searcher) &&
		  (find_dialog_is_active()))
		find_dialog_find(NULL, READ_FORWARD, cp);
	      last_searcher = cp;
	      find_dialog(cp);
	      break;

	    case snd_K_R: case snd_K_r: 
	      if ((cp == last_searcher) &&
		  (find_dialog_is_active()))
		find_dialog_find(NULL, READ_BACKWARD, cp);
	      last_searcher = cp;
	      find_dialog(cp);
	      break;
#endif
	    case snd_K_T: case snd_K_t: 
	      stop_playing_sound(sp, PLAY_C_T); 
	      set_play_button(sp, false);
	      break;

	    case snd_K_U: case snd_K_u: 
	      counting = true; 
	      u_count = true;
	      number_ctr = 0; 
	      dot_seen = false; 
	      break;

	    case snd_K_V: case snd_K_v:
	      cp->cursor_on = true;
	      /* in emacs this is move ahead one window, but for some reason in Snd it's center cursor?? */
	      cursor_moveto(cp, (mus_long_t)((ap->losamp + ap->hisamp) / 2));
	      break;

	    case snd_K_W: case snd_K_w: 
	      delete_selection(UPDATE_DISPLAY); 
	      break;

	    case snd_K_X: case snd_K_x: 
	      extended_mode = true; 
	      if (got_count) 
		{
		  ext_count = count; 
		  got_ext_count = got_count;
		  got_count = false;
		}
	      break;

	    case snd_K_Y: case snd_K_y: 
	      paste_region(region_list_position_to_id(0), cp);
	      break;

	    case snd_K_Z: case snd_K_z: 
	      cp->cursor_on = true; 
	      cursor_zeros(cp, count, OVER_SOUND); 
	      break;

	    case snd_K_Right: 
	      sx_incremented(cp, state_amount(state | shift));
	      break;

	    case snd_K_Left: 
	      sx_incremented(cp, -state_amount(state | shift)); 
	      break;

	    case snd_K_Up: 
	      zx_incremented(cp, 1.0 + state_amount(state | shift)); 
	      break;

	    case snd_K_Down: 
	      zx_incremented(cp, 1.0 / (1.0 + state_amount(state | shift))); 
	      break;

	    case snd_K_0: case snd_K_1: case snd_K_2: case snd_K_3: case snd_K_4:
	    case snd_K_5: case snd_K_6: case snd_K_7: case snd_K_8: case snd_K_9: 
	      counting = true;
	      number_buffer[number_ctr] = (char)('0' + keysym - snd_K_0); 
	      if (number_ctr < (NUMBER_BUFFER_SIZE - 2)) 
		number_ctr++; 
	      /* there is also the bare-number case below */
	      break;

	    case snd_keypad_0: case snd_keypad_1: case snd_keypad_2: case snd_keypad_3: case snd_keypad_4:
	    case snd_keypad_5: case snd_keypad_6: case snd_keypad_7: case snd_keypad_8: case snd_keypad_9: 
	      counting = true;
	      number_buffer[number_ctr] = (char)('0' + keysym - snd_keypad_0); 
	      if (number_ctr < (NUMBER_BUFFER_SIZE - 2)) 
		number_ctr++; 
	      /* there is also the bare-number case below */
	      break;

	    case snd_K_space: 
	      if (count > 0)
		{
		  start_selection_creation(cp, cursor_sample(cp));
		  status_report(sp, "selection starts at %" print_mus_long, cursor_sample(cp));
		}
	      break;

	    case snd_K_period:
	    case snd_keypad_Decimal:
	      counting = true; 
	      number_buffer[number_ctr] = '.'; 
	      number_ctr++; 
	      dot_seen = true; 
	      break;

	    case snd_K_greater: 
	      cp->cursor_on = true; 
	      cursor_moveto_end(cp); 
	      break;

	    case snd_K_less: 
	      cp->cursor_on = true; 
	      cursor_moveto(cp ,0); 
	      break;

	    case snd_K_minus: 
	      counting = true; 
	      number_ctr = 1; 
	      number_buffer[0] = '-'; 
	      break;

	    case snd_K_underscore: 
	      undo_edit_with_sync(cp, count); 
	      break;

	    default:
	      status_report(sp, "C-%s undefined", key_to_name(keysym));
	      break;
	    }
	}
      else /* extended mode with ctrl down */
	{
	  /* -------------------------------- C-x C-key -------------------------------- */
	  if (!got_ext_count) ext_count = 1;
	  extended_mode = false;
	  switch (keysym)
	    {
	    case snd_K_B: case snd_K_b: 
	      set_window_bounds(cp, ext_count); 
	      break;

	    case snd_K_C: case snd_K_c: 
	      hide_controls(sp); 
	      break;

	    case snd_K_F: case snd_K_f: 
	      make_open_file_dialog(FILE_READ_WRITE, true);
	      break;

	    case snd_K_G: case snd_K_g: 
	      control_g(sp);
	      break;

	    case snd_K_J: case snd_K_j:
	      cp->cursor_on = true; 
	      goto_mix(cp, ext_count); 
	      break;

	    case snd_K_O: case snd_K_o: 
	      /* this doesn't change the View:Controls menu label because (sigh...) it's specific to the currently selected sound */
	      show_controls(sp); 
	      break;

	    case snd_K_P: case snd_K_p: 
	      set_window_size(cp, ext_count); 
	      break;

	    case snd_K_Q: case snd_K_q: 
	      make_mix_file_dialog(true);
	      break;

	    case snd_K_R: case snd_K_r: 
	      redo_edit_with_sync(cp, ext_count); 
	      break;

	    case snd_K_S: case snd_K_s: 
	      save_edits_from_kbd(sp);
	      break;

	    case snd_K_T: case snd_K_t: 
	      stop_playing_sound(sp, PLAY_C_T); 
	      break;

	    case snd_K_U: case snd_K_u: 
	      undo_edit_with_sync(cp, ext_count); 
	      break;

	    case snd_K_V: case snd_K_v:
	      set_window_percentage(cp, ext_count);
	      break;

#if (!USE_NO_GUI)
	    case snd_K_W: case snd_K_w: 
	      {
		chan_info *cp;
		cp = any_selected_channel(any_selected_sound());
		make_channel_extract_dialog(cp->chan);
	      }
	      break;
#endif

	    case snd_K_Z: case snd_K_z: 
	      cp->cursor_on = true; 
	      cos_smooth(cp, cursor_sample(cp), ext_count, OVER_SOUND); 
	      break;

	    case snd_K_Right: 
	      sx_incremented(cp, state_amount(state | shift));
	      break;

	    case snd_K_Left:  
	      sx_incremented(cp, -state_amount(state | shift)); 
	      break;

	    case snd_K_Up: 
	      zx_incremented(cp, 1.0 + state_amount(state | shift)); 
	      break;

	    case snd_K_Down: 
	      zx_incremented(cp, 1.0 / (1.0 + state_amount(state | shift))); 
	      break;

	    default:
	      status_report(sp, "C-x C-%s undefined", key_to_name(keysym));
	      break;
	    }
	}
    }
  else
    {
      if (!extended_mode)
	/* -------------------------------- key (or M-key) -------------------------------- */
	{ /* no control (but possibly meta), not extended mode -- bare alpha chars sent to listener if possible */
	  /*   (we already checked for possible user key bindings above) */
	  switch (keysym)
	    {
	    case snd_K_0: case snd_K_1: case snd_K_2: case snd_K_3: case snd_K_4:
	    case snd_K_5: case snd_K_6: case snd_K_7: case snd_K_8: case snd_K_9: 
	      counting = true;
	      number_buffer[number_ctr] = (char)('0' + keysym-snd_K_0); 
	      if (number_ctr < (NUMBER_BUFFER_SIZE - 2)) 
		number_ctr++; 
	      break;

	    case snd_keypad_0: case snd_keypad_1: case snd_keypad_2: case snd_keypad_3: case snd_keypad_4:
	    case snd_keypad_5: case snd_keypad_6: case snd_keypad_7: case snd_keypad_8: case snd_keypad_9: 
	      counting = true;
	      number_buffer[number_ctr] = (char)('0' + keysym - snd_keypad_0); 
	      if (number_ctr < (NUMBER_BUFFER_SIZE - 2)) 
		number_ctr++; 
	      break;

	    case snd_K_period: 
	    case snd_keypad_Decimal:
	      counting = true; 
	      number_buffer[number_ctr] = '.'; 
	      number_ctr++; 
	      dot_seen = true; 
	      break;

	    case snd_K_greater: 
	      cp->cursor_on = true; 
	      cursor_moveto_end(cp); 
	      break;

	    case snd_K_less: 
	      cp->cursor_on = true; 
	      cursor_moveto(cp, 0); 
	      break;

	    case snd_K_minus: 
	      counting = true; 
	      number_buffer[0] = '-'; 
	      number_ctr = 1; 
	      break;

	    case snd_K_Right: 
	      sx_incremented(cp, state_amount(state | shift)); 
	      break;

	    case snd_K_Left:  
	      sx_incremented(cp, -state_amount(state | shift)); 
	      break;

	    case snd_K_Up:    
	      zx_incremented(cp, 1.0 + state_amount(state | shift)); 
	      break;

	    case snd_K_Down: 
	      zx_incremented(cp, 1.0 / (1.0 + state_amount(state | shift))); 
	      break;

	    case snd_K_Home: 
	      redirect_everything_to(printout_to_status_area, (void *)sp);
	      sp = snd_update(sp); 
	      redirect_everything_to(NULL, NULL);
	      break;

	    case snd_K_space: 
	      if (play_in_progress())
		toggle_dac_pausing(); 
	      else play_sound(sp, cursor_sample(cp), NO_END_SPECIFIED); /* was deactivate_selection */
	      break;

	    case snd_keypad_Add:
	      if (time_graph_type(ss) == GRAPH_AS_WAVOGRAM) 
		set_wavo_trace(wavo_trace(ss) + 1); 
	      else set_spectro_hop(spectro_hop(ss) + 1);
	      reflect_spectro(); 
	      break;

	    case snd_keypad_Subtract: 
	      if (time_graph_type(ss) == GRAPH_AS_WAVOGRAM) 
		{
		  if (wavo_trace(ss) > 1) 
		    set_wavo_trace(wavo_trace(ss) - 1);
		} 
	      else 
		{
		  if (spectro_hop(ss) > 1) 
		    set_spectro_hop(spectro_hop(ss) - 1);
		}
	      reflect_spectro(); 
	      break;

	    case snd_keypad_Multiply: 
	      set_transform_size(transform_size(ss) * 2); 
	      break;

	    case snd_keypad_Divide: 
	      if (transform_size(ss) > 4) 
		set_transform_size(transform_size(ss) / 2); 
	      break;

	    case snd_keypad_Delete:
	      set_dot_size(dot_size(ss) + 1); 
	      break;

	    case snd_keypad_Insert:
	      if (dot_size(ss) > 1) 
		set_dot_size(dot_size(ss) - 1); 
	      break;

	    case snd_keypad_Enter: 
	      reset_spectro(); 
	      reflect_spectro(); 
	      break;

	    case snd_keypad_Up:
	      zoom_fft(1.0 + state_amount(state | shift));
	      break;

	    case snd_keypad_Down:
	      zoom_fft(1.0 / (1.0 + state_amount(state | shift))); 
	      break;

	    case snd_keypad_Left:
	      move_fft(-state_amount(state | shift)); 
	      break;

	    case snd_keypad_Right:
	      move_fft(state_amount(state | shift)); 
	      break;

	    default:
	      /* try to send unbuckified random keyboard input to the lisp listener */

#if USE_MOTIF
	      /* if meta key, assume for now that it's intended as a menu accelerator (don't send it to the listener) */
	      /*    in some motif's we don't even see the accelerator, but in Linux we do */
	      if ((state & MetaMask) &&
		  ((keysym == snd_K_f) || (keysym == snd_K_e) || (keysym == snd_K_v) || (keysym == snd_K_o) || (keysym == snd_K_h)))
		/* here the accelerators are f e v o h (not upper case for some reason -- it's given as 'F' in snd-xmenu.c) */
		return;
	      /* it might be better to remove the menu accelerators -- they are a dubious feature to begin with */
#endif
#if HAVE_EXTENSION_LANGUAGE
	      {
		char buf[2];
		buf[0] = keysym; 
		buf[1] = 0;
		if (listener_is_visible())
		  {
		    goto_listener();
		    listener_append(buf);
		  }
	      }
#else
	      status_report(sp, "key %s%s undefined", (state & MetaMask) ? "M-" : "", key_to_name(keysym));
#endif
	      break;
	    }
	}
      else 
	/* extended mode with ctrl up -- where not an emacs analogy, related to the current selection */
	/*   the active selection now follows the edit list, so there may be no relation between
	 *   region 0 and the active selection -- in ambiguous cases, we need to look explicitly
	 *   for the selection first.
	 */
	{
	  /* -------------------------------- C-x key -------------------------------- */
	  extended_mode = false;
	  if (!(state & MetaMask))
	    {
	      switch (keysym)
		{
		case snd_K_B: case snd_K_b: 
		  cp->cursor_on = true; 
		  handle_cursor(cp, CURSOR_ON_LEFT);
		  break;

		case snd_K_C: case snd_K_c: 
		  if (!(mark_define_region(cp, (!got_ext_count) ? 1 : ext_count)))
		    set_status(cp->sound, "no such mark", false);
		  break;

		case snd_K_E: case snd_K_e: 
		  if (defining_macro) 
		    {
		      set_status(sp, "can't call macro while it's being defined", false);
		      defining_macro = false;
		      macro_size = 0; /* so subsequent M-x e doesn't get something silly */
		    }
		  else
		    {
		      execute_last_macro(cp, (!got_ext_count) ? 1 : ext_count);
		      if ((cp) && (cp->sound) && (cp->active >= CHANNEL_HAS_EDIT_LIST)) handle_cursor(cp, cursor_decision(cp)); else return;
		    }
		  break;

		case snd_K_F: case snd_K_f: 
		  cp->cursor_on = true; 
		  handle_cursor(cp, CURSOR_ON_RIGHT); 
		  break;

		case snd_K_K: case snd_K_k: 
		  snd_close_file(sp); 
		  break;

		case snd_K_L: case snd_K_l: 
		  cp->cursor_on = true;
		  if (selection_is_active_in_channel(cp))
		    cursor_moveto(cp, (mus_long_t)(selection_beg(cp) + 0.5 * selection_len()));
		  else set_status(sp, "no active selection", false);
		  handle_cursor_with_sync(cp, CURSOR_IN_MIDDLE);
		  break;

		case snd_K_O: case snd_K_o: 
		  if (ext_count > 0) 
		    goto_next_graph(cp, ext_count); 
		  else goto_previous_graph(cp, ext_count); 
		  break;

		case snd_K_P: case snd_K_p: 
		  if (!got_ext_count)
		    play_selection(IN_BACKGROUND);
		  else play_region(ext_count, IN_BACKGROUND);
		  break;

		case snd_K_Q: case snd_K_q: 
		  add_selection_or_region((!got_ext_count) ? 0 : ext_count, cp); 
		  break;

		case snd_K_R: case snd_K_r: 
		  redo_edit_with_sync(cp, (!got_ext_count) ? 1 : ext_count); 
		  break;

		case snd_K_U: case snd_K_u: 
		  undo_edit_with_sync(cp, (!got_ext_count) ? 1 : ext_count); 
		  break;

		case snd_K_V: case snd_K_v: 
		  if (selection_is_active_in_channel(cp))
		    window_framples_selection(cp); 
		  else 
		    {
		      bool complain = true;
		      if (cp->sound->channel_style != CHANNELS_SEPARATE)
			{
			  int i;
			  for (i = 0; i < (int)sp->nchans; i++)
			    if ((i != cp->chan) &&
				(selection_is_active_in_channel(sp->chans[i])))
			      {
				window_framples_selection(sp->chans[i]);
				complain = false;
				break;
			      }
			}
		      if (complain)
			set_status(sp, "no active selection", false);
		    }
		  break;

		case snd_K_Z: case snd_K_z: 
		  if (selection_is_active_in_channel(cp))
		    cos_smooth(cp, cursor_sample(cp), (!got_ext_count) ? 1 : ext_count, OVER_SELECTION); 
		  else set_status(sp, "no active selection", false);
		  break;

		case snd_K_Right:   
		  sx_incremented(cp, state_amount(state | shift));
		  break;

		case snd_K_Left:
		  sx_incremented(cp, -state_amount(state | shift));
		  break;

		case snd_K_Up:
		  zx_incremented(cp, 1.0 + state_amount(state | shift));
		  break;

		case snd_K_Down:
		  zx_incremented(cp, 1.0 / (1.0 + state_amount(state | shift)));
		  break;

		case snd_K_less:
		  cp->cursor_on = true; 
		  cursor_moveto(cp, 0); 
		  break;

		case snd_K_greater: 
		  cp->cursor_on = true; 
		  cursor_moveto_end(cp);
		  break;

		case snd_K_openparen:
		  if (defining_macro) 
		    set_status(sp, "macro definition already in progress", false);
		  else
		    {
		      start_defining_macro(); 
		      set_status(sp, "defining macro...", false); 
		    }
		  break;

		case snd_K_closeparen: 
		  if (defining_macro)
		    {
		      stop_defining_macro(); 
		      clear_status_area(sp); 
		    }
		  break;

		default:
		  status_report(sp, "C-x %s undefined", key_to_name(keysym));
		  break;
		}
	    }
	  else
	    {
	      status_report(sp, "C-x M-%s undefined", key_to_name(keysym));
	    }
	}
    }
  if (!extended_mode) {got_ext_count = false; ext_count = 1;}
}



char *make_key_name(char *buf, int buf_size, int key, int state, bool extended)
{
  snprintf(buf, buf_size, "%s%s%s",
	       (extended) ? "C-x " : "",
	       (state & ControlMask) ? ((state & MetaMask) ? "CM-" : "C-") : ((state & MetaMask) ? "M-" : ""),
	       (key == snd_K_less) ? "<" : 
	       ((key == snd_K_greater) ? ">" : 
		((key == snd_K_openparen) ? "(" :
		 ((key == snd_K_closeparen) ? ")" :
		  ((key == snd_K_slash) ? "/" :
		   KEY_TO_NAME(key))))));
  return(buf);
}


static int key_name_to_key(Xen key, const char *caller)
{
  /* Ruby thinks chars are strings */
  if (Xen_is_integer(key))
    return(Xen_integer_to_C_int(key)); /* includes 0xffc0 style keys, and in Ruby things like ?a */

#if (!HAVE_RUBY)
  if (Xen_is_char(key))
    return((int)(Xen_char_to_C_char(key)));
#endif

#if USE_MOTIF
  return((int)XStringToKeysym(Xen_string_to_C_string(key)));  /* these are the X names: not "+" but "plus" etc */
#endif
  return(0);
}


static Xen check_for_key_error(int k, int s, const char *caller)
{
  if ((k < MIN_KEY_CODE) || (k > MAX_KEY_CODE) ||
      (s < MIN_KEY_STATE) || (s > MAX_KEY_STATE))
    Xen_error(Xen_make_error_type("no-such-key"),
	      Xen_list_4(C_string_to_Xen_string("~A: no such key: ~A, state: ~A"),
			 C_string_to_Xen_string(caller),
			 C_int_to_Xen_integer(k),
			 C_int_to_Xen_integer(s)));
  return(Xen_false);
}


static Xen g_key_binding(Xen key, Xen state, Xen cx_extended)
{
  #define H_key_binding "(" S_key_binding " key :optional (state 0) extended): function bound to this key and associated \
modifiers.  As in " S_bind_key ", state is the logical 'or' of ctrl=4, meta=8, and 'extended' is " PROC_TRUE " if the key is \
prefixed with C-x. 'key' can be a character, a key name such as 'Home', or an integer."
  int i, k, s;

  Xen_check_type(Xen_is_integer(key) || Xen_is_char(key) || Xen_is_string(key), key, 1, S_key_binding, "an integer, character, or string");
  Xen_check_type(Xen_is_integer_or_unbound(state), state, 2, S_key_binding, "an integer");
  Xen_check_type(Xen_is_boolean_or_unbound(cx_extended), cx_extended, 3, S_key_binding, "a boolean");

  k = key_name_to_key(key, S_key_binding);
  s = ((Xen_is_integer(state)) ? Xen_integer_to_C_int(state) : 0) & 0xfffe; /* no shift bit */
  check_for_key_error(k, s, S_key_binding);
  i = in_keymap(k, s, Xen_is_true(cx_extended));
  if (i >= 0) 
    return(keymap[i].func);

  return(Xen_undefined);
}


static Xen g_bind_key_1(Xen key, Xen state, Xen code, Xen cx_extended, Xen origin, Xen prefs_info, const char *caller)
{
  int k, s;
  bool e;

  Xen_check_type(Xen_is_integer(key) || Xen_is_string(key) || Xen_is_char(key), key, 1, caller, "an integer, char, or string");
  Xen_check_type(Xen_is_integer(state), state, 2, caller, "an integer");
  Xen_check_type((Xen_is_false(code) || Xen_is_procedure(code)), code, 3, caller, PROC_FALSE " or a procedure");
  Xen_check_type(Xen_is_boolean_or_unbound(cx_extended), cx_extended, 4, caller, "a boolean");
  Xen_check_type(Xen_is_string_or_unbound(origin), origin, 5, caller, "a string");
  Xen_check_type(Xen_is_string_or_unbound(prefs_info), prefs_info, 6, caller, "a string");

  k = key_name_to_key(key, caller);
  s = Xen_integer_to_C_int(state) & 0xfffe; /* get rid of shift bit */
  check_for_key_error(k, s, caller);
  e = (Xen_is_true(cx_extended));

  if (Xen_is_false(code))
    set_keymap_entry(k, s, 0, Xen_undefined, e, NULL, NULL);
  else 
    {
      int args;
      char buf[256];
      const char *comment = NULL, *prefs = NULL;
      args = Xen_required_args(code);
      if (args > 1)
	{
	  Xen errmsg;
	  char *errstr;
	  errstr = mus_format(S_bind_key " function arg should take either zero or one args, not %d", args);
	  errmsg = C_string_to_Xen_string(errstr);

	  free(errstr);
	  return(snd_bad_arity_error(caller, errmsg, code));
	}
      if (Xen_is_string(origin))
	comment = Xen_string_to_C_string(origin); 
      else comment = make_key_name(buf, 256, k, s, e);
      if (Xen_is_string(prefs_info)) prefs = Xen_string_to_C_string(prefs_info);
      set_keymap_entry(k, s, args, code, e, comment, prefs);
    }
  return(code);
}


static Xen g_bind_key(Xen key, Xen state, Xen code, Xen cx_extended, Xen origin, Xen prefs_info)
{
  #define H_bind_key "(" S_bind_key " key modifiers func :optional extended origin prefs-info): \
causes 'key' (an integer, character, or string) \
when typed with 'modifiers' (0:none, 4:control, 8:meta) (and C-x if extended) to invoke 'func', a function of \
zero or one arguments. If the function takes one argument, it is passed the preceding C-u number, if any. \
The function should return one of the cursor choices (e.g. " S_keyboard_no_action ").  'origin' is \
the name reported if an error occurs. The 'key' argument can be the X name of the key (e.g. \"plus\" for \"+\" or \"Home\"), \
the character on the key (#\\a), or the integer corresponding to that character: (\"(char->integer #\\a)\" in Scheme, \
\"?a\" in Ruby, \
or \"<char> a\" in Forth)."
  
  return(g_bind_key_1(key, state, code, cx_extended, origin, prefs_info, S_bind_key));
}


static Xen g_unbind_key(Xen key, Xen state, Xen cx_extended)
{
  #define H_unbind_key "(" S_unbind_key " key state :optional extended): undo the effect of a prior " S_bind_key " call."
  return(g_bind_key_1(key, state, Xen_false, cx_extended, Xen_undefined, Xen_undefined, S_unbind_key));
}


static Xen g_key(Xen kbd, Xen buckybits, Xen snd, Xen chn)
{
  #define H_key "(" S_key " key modifiers :optional snd chn): simulate typing 'key' with 'modifiers' in snd's channel chn"
  chan_info *cp;
  int k, s;

  Xen_check_type(Xen_is_integer(kbd) || Xen_is_char(kbd) || Xen_is_string(kbd), kbd, 1, S_key, "an integer, character, or string");
  Xen_check_type(Xen_is_integer(buckybits), buckybits, 2, S_key, "an integer");
  Snd_assert_channel(S_key, snd, chn, 3);

  cp = get_cp(snd, chn, S_key);
  if (!cp) return(Xen_false);

  k = key_name_to_key(kbd, S_key);
  s = Xen_integer_to_C_int(buckybits);
  check_for_key_error(k, s, S_key);
  keyboard_command(cp, k, s);

  return(kbd);
}


Xen_wrap_3_optional_args(g_key_binding_w, g_key_binding)
Xen_wrap_6_optional_args(g_bind_key_w, g_bind_key)
Xen_wrap_3_optional_args(g_unbind_key_w, g_unbind_key)
Xen_wrap_4_optional_args(g_key_w, g_key)

void g_init_kbd(void)
{
  int i;
#if HAVE_SCHEME
  s7_pointer n, t, b, s;
  t = s7_t(s7);
  n = s7_make_symbol(s7, "integer?");
  b = s7_make_symbol(s7, "boolean?");
  s = s7_make_symbol(s7, "string?");
#endif

  #define H_cursor_in_view     "The value for a " S_bind_key " function that moves the window so that the cursor is in the view"
  #define H_cursor_on_left     "The value for a " S_bind_key " function that moves the window so that the cursor is at the left edge"
  #define H_cursor_on_right    "The value for a " S_bind_key " function that moves the window so that the cursor is at the right edge"
  #define H_cursor_in_middle   "The value for a " S_bind_key " function that moves the window so that the cursor is in the middle"
  #define H_keyboard_no_action "The value for a " S_bind_key " function that does nothing upon return"

  Xen_define_constant(S_cursor_in_view,     CURSOR_IN_VIEW,     H_cursor_in_view);
  Xen_define_constant(S_cursor_on_left,     CURSOR_ON_LEFT,     H_cursor_on_left);
  Xen_define_constant(S_cursor_on_right,    CURSOR_ON_RIGHT,    H_cursor_on_right);
  Xen_define_constant(S_cursor_in_middle,   CURSOR_IN_MIDDLE,   H_cursor_in_middle);
  Xen_define_constant(S_keyboard_no_action, KEYBOARD_NO_ACTION, H_keyboard_no_action);

  Xen_define_typed_procedure(S_key_binding, g_key_binding_w, 1, 2, 0, H_key_binding, s7_make_signature(s7, 4, t, t, n, b));
  Xen_define_typed_procedure(S_bind_key,    g_bind_key_w,    3, 3, 0, H_bind_key,    s7_make_signature(s7, 7, t, t, n, t, b, s, s)); 
  Xen_define_typed_procedure(S_unbind_key,  g_unbind_key_w,  2, 1, 0, H_unbind_key,  s7_make_signature(s7, 4, t, t, n, b));
  Xen_define_unsafe_typed_procedure(S_key,  g_key_w,         2, 2, 0, H_key,         s7_make_signature(s7, 5, t, n, n, t, t));

  for (i = 0; i < NUM_BUILT_IN_KEYS; i++)
    built_in_keys[i].func = Xen_false;
}
