Blob Blame History Raw
/*
  Copyright (C) 2005, 2006, 2008, 2009, 2010, 2011, 2012, 2017
  Rocky Bernstein <rocky@gnu.org>

  Adapted from Gerd Knorr's player.c program  <kraxel@bytesex.org>
  Copyright (C) 1997, 1998

  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#ifdef HAVE_CONFIG_H
# include "config.h"
# define __CDIO_CONFIG_H__ 1
#endif

#ifdef HAVE_STDIO_H
#include <stdio.h>
#endif

#ifdef HAVE_STDLIB_H
#include <stdlib.h>
#endif

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#ifdef HAVE_STRING_H
#include <string.h>
#endif

#ifdef HAVE_SYS_TIME_H
#include <sys/time.h>
#endif

#ifdef HAVE_CDDB
#include <cddb/cddb.h>
#endif

#include <signal.h>

#ifdef HAVE_ERRNO_H
#include <errno.h>
#endif

#ifdef HAVE_CURSES_H
# include <curses.h>
#else
# ifdef HAVE_NCURSES_H
#include <ncurses.h>
# else
#   ifdef HAVE_NCURSES_NCURSES_H
#     include <ncurses/ncurses.h>
#   else
#     error "You need <curses.h> or <ncurses.h to build cdda-player"
#   endif
# endif
#endif

#include <cdio/cdio.h>
#include <cdio/mmc.h>
#include <cdio/util.h>
#include <cdio/cd_types.h>
#include <cdio/logging.h>

#include "cddb.h"
#include "getopt.h"

static void action(const char *psz_action);
static void display_cdinfo(CdIo_t *p_cdio, track_t i_tracks,
                           track_t i_first_track);
static void display_tracks(void);
static void get_cddb_track_info(track_t i_track);
static void get_cdtext_track_info(track_t i_track);
static void get_track_info(track_t i_track);
static bool play_track(track_t t1, track_t t2);

static CdIo_t             *p_cdio_global;        /* libcdio handle */
static driver_id_t        driver_id = DRIVER_DEVICE;
static int b_sig = false;                        /* set on some signals */

/* cdrom data */
static track_t            i_first_track_global;
static track_t            i_last_track;
static track_t            i_first_audio_track;
static track_t            i_last_audio_track;
static track_t            i_last_display_track = CDIO_INVALID_TRACK;
static track_t            i_tracks_global;
static msf_t              toc[CDIO_CDROM_LEADOUT_TRACK+1];
static cdio_subchannel_t  sub;      /* subchannel last time read */
static int                i_data;     /* # of data tracks present ? */
static int                start_track = 0;
static int                stop_track = 0;
static int                one_track = 0;
static int                i_vol_port   = 5; /* If 5, retrieve volume port.
                                         Otherwise the port number 0..3
                                         of a working volume port and
                                         4 for no working port.
                                       */

/* settings which can be set from the command or interactively. */
static bool               b_cd            = false;
static bool               auto_mode       = false;
static bool               b_verbose       = false;
static bool               debug           = false;
static bool               b_interactive   = true;
static bool               b_prefer_cdtext = true;
#ifdef CDDB_ADDED
static bool               b_cddb          = false; /* CDDB database present */
#endif
static bool               b_db            = false; /* we have a database at all */
static bool               b_record        = false; /* we have a record for
static                                         the inserted CD */
static bool               b_all_tracks = false; /* True if we display all tracks*/
static int8_t             i_volume_level = -1;   /* Valid range is 0..100 */


static char *psz_device_global=NULL;
static char *psz_program;

/* Info about songs and titles. The 0 entry will contain the disc info.
 */
typedef struct
{
  char title[80];
  char artist[80];
  char length[8];
  char ext_data[80];
  bool b_cdtext;  /* true if from CD-Text, false if from CDDB */
} cd_track_info_rec_t;

static cd_track_info_rec_t cd_info[CDIO_CD_MAX_TRACKS+2];

static char title[80];
static char artist[80];
static char genre[40];
static char category[40];
static char year[5];

static bool b_cdtext_title;     /* true if from CD-Text, false if from CDDB */
static bool b_cdtext_artist;    /* true if from CD-Text, false if from CDDB */
static bool b_cdtext_genre;     /* true if from CD-Text, false if from CDDB */
#ifdef CDTEXT_CATEGORY_ADDED
static bool b_cdtext_category;  /* true if from CD-Text, false if from CDDB */
#endif
static bool b_cdtext_year;  /* true if from CD-Text, false if from CDDB */

static cdio_audio_volume_t audio_volume;

#ifdef HAVE_CDDB
static cddb_conn_t *p_conn = NULL;
static cddb_disc_t *p_cddb_disc = NULL;
static int i_cddb_matches = 0;
#endif

#define MAX_KEY_STR 50
static const char key_bindings[][MAX_KEY_STR] = {
  "    right     play / next track",
  "    left      previous track",
  "    up/down   10 sec forward / back",
  "    1-9       jump to track 1-9",
  "    0         jump to track 10",
  "    F1-F20    jump to track 11-30",
  " ",
  "    k, h, ?   show this key help",
  "    l,        toggle listing all tracks",
  "    e         eject",
  "    c         close tray",
  "    p, space  pause / resume",
  "    s         stop",
  "    q, ^C     quit",
  "    x         quit and continue playing",
  "    a         toggle auto-mode",
  "    -         decrease volume level",
  "    +         increase volume level",
};

static const unsigned int i_key_bindings = sizeof(key_bindings) / MAX_KEY_STR;

/* ---------------------------------------------------------------------- */
/* tty stuff                                                              */

typedef enum {
  LINE_STATUS       =  0,
  LINE_CDINFO       =  1,

  LINE_ARTIST       =  3,
  LINE_CDNAME       =  4,
  LINE_GENRE        =  5,
  LINE_YEAR         =  6,

  LINE_TRACK_PREV   =  8,
  LINE_TRACK_TITLE  =  9,
  LINE_TRACK_ARTIST = 10,
  LINE_TRACK_NEXT   = 11,

} track_line_t;

static unsigned int  LINE_ACTION = 25;
static unsigned int  COLS_LAST;
static char psz_action_line[300] = "";

static int rounded_div(unsigned int i_a, unsigned int i_b)
{
  const unsigned int i_b_half=i_b/2;
  return ((i_a)+i_b_half)/i_b;
}

/** Curses window initialization. */
static void
tty_raw(void)
{
  if (!b_interactive) return;

  initscr();
  cbreak();
  clear();
  noecho();
#ifdef HAVE_KEYPAD
  keypad(stdscr,1);
#endif
  getmaxyx(stdscr, LINE_ACTION, COLS_LAST);
  LINE_ACTION--;
  refresh();
}

/** Curses window finalization. */
static void
tty_restore(void)
{
  if (!b_interactive) return;
  endwin();
}

#define UNUSED(x) (void)(x)
/* Called when window is resized. */
static void
sigwinch(int dummy)
{
  UNUSED(dummy);
  tty_restore();
  tty_raw();
  action(NULL);
}

/* Signal handler - Ctrl-C and others. */
static void
ctrlc(int signal_num)
{
  b_sig = true;
}

/* Timed wait on an event. */
static int
select_wait(int sec)
{
  struct timeval  tv;
  fd_set          se;

  FD_ZERO(&se);
  tv.tv_sec = sec;
  tv.tv_usec = 0;
  return select(1,&se,NULL,NULL,&tv);
}

/* ---------------------------------------------------------------------- */

/* Display the action line. */
static void
action(const char *psz_action)
{
  if (!b_interactive) {
    if (b_verbose && psz_action)
      fprintf(stderr,"action: %s\n", psz_action);
    return;
  }

  if (!psz_action)
    ;
  else if (psz_action && strlen(psz_action))
    snprintf(psz_action_line, sizeof(psz_action_line), "action : %s",
             psz_action);
  else
    snprintf(psz_action_line, sizeof(psz_action_line), "%s", "" );
  mvprintw(LINE_ACTION, 0, psz_action_line);
  clrtoeol();
  refresh();
}


/* Display an error message.. */
static void
xperror(const char *psz_msg)
{
  char line[80];

  if (!b_interactive) {
    if (b_verbose) {
      fprintf(stderr, "error: ");
      perror(psz_msg);
    }
    return;
  }

  if (b_verbose) {
    snprintf(line, sizeof(line), "%s: %s", psz_msg, strerror(errno));
    attron(A_STANDOUT);
    mvprintw(LINE_ACTION, 0, (char *) "error  : %s", line);
    attroff(A_STANDOUT);
    clrtoeol();
    refresh();
    select_wait(3);
    action("");
  }
}

static void
finish(const char *psz_msg, int rc)
{
  if (b_interactive) {
    attron(A_STANDOUT);
    mvprintw(LINE_ACTION, 0, (char *) "%s, exiting...\n", psz_msg);
    attroff(A_STANDOUT);
    clrtoeol();
    refresh();
  }
  tty_restore();
#ifdef HAVE_CDDB
  if (p_conn) cddb_destroy(p_conn);
  cddb_disc_destroy(p_cddb_disc);
  libcddb_shutdown();
#endif /*HAVE_CDDB*/
  cdio_destroy (p_cdio_global);
  free (psz_device_global);
  exit (rc);
}

/* ---------------------------------------------------------------------- */

/* Set all audio channels to level. level is assumed to be in the range
   0..100.
*/
static bool
set_volume_level(CdIo_t *p_cdio, uint8_t i_level)
{
  const unsigned int i_new_level= rounded_div(i_level*256, 100);
  unsigned int i;
  driver_return_code_t rc;
  for (i=0; i<=3; i++) {
    audio_volume.level[i] = i_new_level;
  }

  rc = cdio_audio_set_volume(p_cdio, &audio_volume);
  if ( DRIVER_OP_SUCCESS != rc ) {
    /* If we can't do a get volume, audio_volume.level is used as
       a second-best guess. But if this set failed restore it to
       an invalid value so we don't get confused and think that this
       was set. */
    for (i=0; i<=3; i++) {
      audio_volume.level[i] = 0;
    }
  } else {
    /* Set i_vol_port so volume levels set above will get used. */
    i_vol_port=0;
  }
  return rc;

}

/* Subtract one from the volume level. If we are at the minimum value,
 * nothing is done.

   We used to wrap at the boundaries but this is probably wrong because
   is assumes someone:
     * looks at the display while listening,
     * knows that 99 is the maximum value.

   See issue #33333

   If the volume level is undefined, then this means we could not get
   the current value and we'll' set it to 50 the midway point.

   Return the status of setting the volume level.  */
static bool
decrease_volume_level(CdIo_t *p_cdio)
{
  if (i_volume_level == -1) i_volume_level = 51;
  if (i_volume_level <= 0)  i_volume_level = 1;
  return set_volume_level(p_cdio, --i_volume_level);
}

/* Add 1 to the volume level. If we are at the maximum value, nothing
   is done.

   We used to wrap at the boundaries but this is probably wrong because
   is assumes someone:
     * looks at the display while listening,
     * knows that 99 is the maximum value.

   See issue #33333

   If volume level is undefined, then this means we could not get the
   current value and we'll' set it to 50 the midway point.

   Return the status of setting the volume level.  */
static bool
increase_volume_level(CdIo_t *p_cdio)
{
  if (i_volume_level == -1) i_volume_level = 49;
  if (i_volume_level <= 0) i_volume_level = 0;
  if (i_volume_level > 98) i_volume_level = 98;
  return set_volume_level(p_cdio, ++i_volume_level);
}

/** Stop playing audio CD */
static bool
cd_stop(CdIo_t *p_cdio)
{
  bool b_ok = true;
  if (b_cd && p_cdio) {
    action("stop...");
    i_last_audio_track = CDIO_INVALID_TRACK;
    b_ok = DRIVER_OP_SUCCESS == cdio_audio_stop(p_cdio);
    if ( !b_ok )
      xperror("stop");
    if (b_all_tracks) display_tracks();
  }
  return b_ok;
}

/** Eject CD */
static bool
cd_eject(void)
{
  bool b_ok = true;
  if (p_cdio_global) {
    cd_stop(p_cdio_global);
    action("eject...");
    b_ok = DRIVER_OP_SUCCESS == cdio_eject_media(&p_cdio_global);
    if (!b_ok)
      xperror("eject");
    b_cd = false;
    cdio_destroy (p_cdio_global);
    p_cdio_global = NULL;
  }
  return b_ok;
}

/** Close CD tray */
static bool
cd_close(const char *psz_device)
{
  bool b_ok = true;
  if (!b_cd) {
    action("close...");
    b_ok = DRIVER_OP_SUCCESS == cdio_close_tray(psz_device, &driver_id);
    if (!b_ok)
      xperror("close");
  }
  return b_ok;
}

/** Pause playing audio CD */
static bool
cd_pause(CdIo_t *p_cdio)
{
  bool b_ok = true;
  if (sub.audio_status == CDIO_MMC_READ_SUB_ST_PLAY) {
    b_ok = DRIVER_OP_SUCCESS == cdio_audio_pause(p_cdio);
    if (!b_ok)
      xperror("pause");
  }
  return b_ok;
}

/** Get status/track/position info of an audio CD */
static bool
read_subchannel(CdIo_t *p_cdio)
{
  bool b_ok = true;
  if (!p_cdio) return false;

  b_ok = DRIVER_OP_SUCCESS == cdio_audio_read_subchannel(p_cdio, &sub);
  if (!b_ok) {
    xperror("read subchannel");
    b_cd = 0;
  }
  if (auto_mode && sub.audio_status == CDIO_MMC_READ_SUB_ST_COMPLETED)
    cd_eject();
  return b_ok;
}

#ifdef HAVE_CDDB
/** This routine is called by vcd routines on error.
   Setup is done by init_input_plugin.
*/
static void
cddb_log_handler (cddb_log_level_t level, const char message[])
{
  switch (level) {
  case CDDB_LOG_DEBUG:
  case CDDB_LOG_INFO:
    if (!b_verbose)
      return;
    /* Fall through if to warn case */
  case CDDB_LOG_WARN:
  case CDDB_LOG_ERROR:
  case CDDB_LOG_CRITICAL:
  default:
    xperror(message);
    break;
  }

  /* gl_default_cdio_log_handler (level, message); */
}
#endif /* HAVE_CDDB  */

static void
get_cddb_disc_info(CdIo_t *p_cdio)
{
#ifdef HAVE_CDDB
  b_db = init_cddb(p_cdio, &p_conn, &p_cddb_disc, xperror, i_first_track_global,
                   i_tracks_global, &i_cddb_matches);
  if (b_db) {
    int i_year;
    i_year = atoi(year);
    cddb_disc_set_artist(p_cddb_disc, artist);
    cddb_disc_set_title(p_cddb_disc, title);
    cddb_disc_set_genre(p_cddb_disc, genre);
    cddb_disc_set_year(p_cddb_disc, i_year);
  }
#endif /* HAVE_CDDB */
  return;
}

#define add_cdtext_disc_info(format_str, info_field, FIELD)     \
  if (cdtext_get_const(p_cdtext, FIELD, 0) && !strlen(info_field)) { \
    snprintf(info_field, sizeof(info_field), format_str,        \
             cdtext_get_const(p_cdtext, FIELD, 0));                  \
    b_cdtext_ ## info_field = true;                             \
  }

static void
get_cdtext_disc_info(CdIo_t *p_cdio)
{
  cdtext_t *p_cdtext = cdio_get_cdtext(p_cdio);

  if (p_cdtext) {
    add_cdtext_disc_info("%s", title, CDTEXT_FIELD_TITLE);
    add_cdtext_disc_info("%s", artist, CDTEXT_FIELD_PERFORMER);
    add_cdtext_disc_info("%s", genre, CDTEXT_FIELD_GENRE);
  }
}

static void
get_disc_info(CdIo_t *p_cdio)
{
  b_db = false;
  if (b_prefer_cdtext) {
    get_cdtext_disc_info(p_cdio);
    get_cddb_disc_info(p_cdio);
  } else {
    get_cddb_disc_info(p_cdio);
    get_cdtext_disc_info(p_cdio);
  }
}

/** Read CD TOC and set CD information. */
static void
read_toc(CdIo_t *p_cdio)
{
  track_t i;

  action("read toc...");

  memset(cd_info, 0, sizeof(cd_info));
  title[0] = artist[0] = genre[0] = category[0] = year[0] = '\0';

  i_first_track_global = cdio_get_first_track_num(p_cdio);
  i_last_track         = cdio_get_last_track_num(p_cdio);
  i_tracks_global      = cdio_get_num_tracks(p_cdio);
  i_first_audio_track  = i_first_track_global;
  i_last_audio_track   = i_last_track;


  cdio_audio_get_volume(p_cdio, &audio_volume);
  for (i_vol_port=0; i_vol_port<4; i_vol_port++) {
    if (audio_volume.level[i_vol_port] > 0) break;
  }

  if ( CDIO_INVALID_TRACK == i_first_track_global ||
       CDIO_INVALID_TRACK == i_last_track ) {
    xperror("read toc header");
    b_cd = false;
    b_record = false;
    i_last_display_track = CDIO_INVALID_TRACK;
  } else {
    b_cd = true;
    i_data = 0;
    get_disc_info(p_cdio);
    for (i = i_first_track_global; i <= i_last_track+1; i++) {
      int s;
      if ( !cdio_get_track_msf(p_cdio, i, &(toc[i])) )
      {
        xperror("read toc entry");
        b_cd = false;
        return;
      }
      if ( TRACK_FORMAT_AUDIO == cdio_get_track_format(p_cdio, i) ) {

        if (i != i_first_track_global)
          {
            s = cdio_audio_get_msf_seconds(&toc[i])
              - cdio_audio_get_msf_seconds(&toc[i-1]);
            snprintf(cd_info[i-1].length, sizeof(cd_info[0].length),
                     "%02d:%02d",
                     (uint8_t) (s / CDIO_CD_SECS_PER_MIN),
                     (uint8_t) (s % CDIO_CD_SECS_PER_MIN));
          }
      } else {
        if ((i != i_last_track+1) ) {
          i_data++;
          if (i == i_first_track_global) {
            if (i == i_last_track)
              i_first_audio_track = CDIO_CDROM_LEADOUT_TRACK;
            else
              i_first_audio_track++;
          }
        }
      }
      get_track_info(i);
    }
    b_record = true;
    read_subchannel(p_cdio);
    if (auto_mode && sub.audio_status != CDIO_MMC_READ_SUB_ST_PLAY)
      play_track(1, CDIO_CDROM_LEADOUT_TRACK);
  }
  action("");
  if (!b_all_tracks)
    display_cdinfo(p_cdio, i_tracks_global, i_first_track_global);
}

/** Play an audio track. */
static bool
play_track(track_t i_start_track, track_t i_end_track)
{
  bool b_ok = true;
  char line[80];

  if (!b_cd) {
    read_toc(p_cdio_global);
  }

  read_subchannel(p_cdio_global);
  if (!b_cd || i_first_track_global == CDIO_CDROM_LEADOUT_TRACK)
    return false;

  if (debug)
    fprintf(stderr,"play tracks: %d-%d => ", i_start_track, i_end_track-1);
  if (i_start_track < i_first_track_global)  i_start_track = i_first_track_global;
  if (i_start_track > i_last_audio_track)    i_start_track = i_last_audio_track;
  if (i_end_track < i_first_track_global)    i_end_track   = i_first_track_global;
  if (i_end_track > i_last_audio_track)      i_end_track   = i_last_audio_track;
  i_end_track++;
  if (debug)
    fprintf(stderr,"%d-%d\n",i_start_track, i_end_track-1);

  cd_pause(p_cdio_global);
  snprintf(line, sizeof(line), "play track %d to track %d.",
	   i_start_track, i_end_track-1);
  action(line);
  b_ok = (DRIVER_OP_SUCCESS == cdio_audio_play_msf(p_cdio_global,
                                                   &(toc[i_start_track]),
                                                   &(toc[i_end_track])) );
  if (!b_ok) xperror("play");
  return b_ok;
}

static void
skip(int diff)
{
  msf_t start_msf;
  int   sec;

  read_subchannel(p_cdio_global);
  if (!b_cd ||  i_first_track_global == CDIO_CDROM_LEADOUT_TRACK)
    return;

  sec  = cdio_audio_get_msf_seconds(&sub.abs_addr);
  sec += diff;
  if (sec < 0) sec = 0;

  start_msf.m = cdio_to_bcd8(sec / CDIO_CD_SECS_PER_MIN);
  start_msf.s = cdio_to_bcd8(sec % CDIO_CD_SECS_PER_MIN);
  start_msf.f = 0;

  cd_pause(p_cdio_global);
  if ( DRIVER_OP_SUCCESS != cdio_audio_play_msf(p_cdio_global, &start_msf,
                                                &(toc[i_last_audio_track])) )
    xperror("play");
}

static bool
toggle_pause(void)
{
  bool b_ok = true;
  if (!b_cd) return false;

  if (CDIO_MMC_READ_SUB_ST_PAUSED == sub.audio_status) {
    b_ok = DRIVER_OP_SUCCESS != cdio_audio_resume(p_cdio_global);
    if (!b_ok)
      xperror("resume");
  } else {
    b_ok = DRIVER_OP_SUCCESS != cdio_audio_pause(p_cdio_global);
    if (!b_ok)
      xperror("pause");
  }
  return b_ok;
}

/** Update windows with status and possibly track info. This used in
  interactive playing not batch mode.
 */
static void
display_status(bool b_status_only)
{
  char line[80];

  if (!b_interactive) return;

  if (!b_cd) {
    snprintf(line, sizeof(line), "no CD in drive (%s)", psz_device_global);

  } else if (i_first_track_global == CDIO_CDROM_LEADOUT_TRACK) {
    snprintf(line, sizeof(line), "CD has only data tracks");

  } else if (sub.audio_status == CDIO_MMC_READ_SUB_ST_PAUSED ||
             sub.audio_status == CDIO_MMC_READ_SUB_ST_PLAY) {
    cdio_audio_get_volume(p_cdio_global, &audio_volume);
    if (i_vol_port < 4) {
        i_volume_level = rounded_div(audio_volume.level[i_vol_port]*100, 256);
        snprintf(line, sizeof(line),
                "track %2d - %02x:%02x of %s (%02x:%02x abs) %s volume: %d",
                sub.track, sub.rel_addr.m, sub.rel_addr.s,
                cd_info[sub.track].length,
                sub.abs_addr.m, sub.abs_addr.s,
                mmc_audio_state2str(sub.audio_status),
                i_volume_level);
      } else
        snprintf(line, sizeof(line),
		 "track %2d - %02x:%02x of %s (%02x:%02x abs) %s",
		 sub.track, sub.rel_addr.m, sub.rel_addr.s,
		 cd_info[sub.track].length, sub.abs_addr.m, sub.abs_addr.s,
		 mmc_audio_state2str(sub.audio_status));
  } else {
    snprintf(line, sizeof(line), "%s", mmc_audio_state2str(sub.audio_status));

  }

  action(NULL);
  mvprintw(LINE_STATUS, 0, (char *) "status%s: %s",
           auto_mode ? "*" : " ", line);
  clrtoeol();

  if ( !b_status_only && b_db && i_last_display_track != sub.track &&
       (sub.audio_status == CDIO_MMC_READ_SUB_ST_PAUSED ||
        sub.audio_status == CDIO_MMC_READ_SUB_ST_PLAY)  &&
        b_cd) {

    if (b_all_tracks)
      display_tracks();
    else {
      const cd_track_info_rec_t *p_cd_info = &cd_info[sub.track];
      i_last_display_track = sub.track;
      if (i_first_audio_track != sub.track &&
          strlen(cd_info[sub.track-1].title)) {
        const cd_track_info_rec_t *p_cd_info_track = &cd_info[sub.track-1];
        mvprintw(LINE_TRACK_PREV, 0, (char *) " track %2d title : %s [%s]",
                 sub.track-1, p_cd_info->title,
                 p_cd_info_track->b_cdtext ? "CD-Text" : "CDDB");
        clrtoeol();
      } else {
        mvprintw(LINE_TRACK_PREV, 0, (char *) "%s","");
        clrtoeol();
      }
      if (strlen(p_cd_info->title)) {
        mvprintw(LINE_TRACK_TITLE,  0, (char *) ">track %2d title : %s [%s]",
                 sub.track, p_cd_info->title,
                 (char *) (p_cd_info->b_cdtext ? "CD-Text" : "CDDB"));
        clrtoeol();
      }
      if (strlen(p_cd_info->artist)) {
        mvprintw(LINE_TRACK_ARTIST, 0, (char *) ">track %2d artist: %s [%s]",
                 sub.track, p_cd_info->artist,
                 p_cd_info->b_cdtext ? "CD-Text" : "CDDB");
        clrtoeol();
      }
      if (i_last_audio_track != sub.track &&
          strlen(cd_info[sub.track+1].title)) {
        const cd_track_info_rec_t *p_cd_info_track = &cd_info[sub.track+1];
        mvprintw(LINE_TRACK_NEXT, 0, (char *) " track %2d title : %s [%s]",
                 sub.track+1, p_cd_info_track->title,
                 p_cd_info->b_cdtext ? "CD-Text" : "CDDB");
        clrtoeol();
      } else {
        mvprintw(LINE_TRACK_NEXT, 0, (char *) "%s","");
        clrtoeol();
      }
      clrtobot();
    }
  }

  action(NULL); /* Redisplay action line. */

}

static void
get_cddb_track_info(track_t i_track)
{
#ifdef HAVE_CDDB
  cddb_track_t *t = cddb_disc_get_track(p_cddb_disc,
                                        i_track - i_first_track_global);
  if (t) {
    cddb_track_set_title(t, title);
    cddb_track_set_artist(t, artist);
  }

#else
    ;
#endif
}

#define add_cdtext_track_info(format_str, info_field, FIELD) \
  if (cdtext_get_const(p_cdtext, FIELD, i_track)) {                        \
    snprintf(cd_info[i_track].info_field,                             \
             sizeof(cd_info[i_track].info_field),                     \
             format_str, cdtext_get_const(p_cdtext, FIELD, i_track));      \
    cd_info[i_track].b_cdtext = true; \
  }


static void
get_cdtext_track_info(track_t i_track)
{

  cdtext_t *p_cdtext = cdio_get_cdtext(p_cdio_global);

  if (NULL != p_cdtext) {
    add_cdtext_track_info("%s", title, CDTEXT_FIELD_TITLE);
    add_cdtext_track_info("%s", artist, CDTEXT_FIELD_PERFORMER);
  }
}

static void
get_track_info(track_t i_track)
{

  if (b_prefer_cdtext) {
    get_cdtext_track_info(i_track);
    get_cddb_track_info(i_track);
  } else {
    get_cddb_track_info(i_track);
    get_cdtext_track_info(i_track);
  }
}

#define display_line(LINE_NO, COL_NO, format_str, field)   \
  if (field != NULL && field[0])  {                                \
    mvprintw(LINE_NO, COL_NO, (char *) format_str " [%s]", \
             field,                                        \
             b_cdtext_ ## field ? "CD-Text": "CDDB");      \
    clrtoeol();                                            \
  }

static void
display_cdinfo(CdIo_t *p_cdio, track_t i_tracks, track_t i_first_track)
{
  int len;
  char line[80];

  if (!b_interactive) return;

  if (!b_cd) snprintf(line, sizeof(line), "-");
  else {
    len = snprintf(line, sizeof(line), "%2u tracks  (%02x:%02x min)",
                  (unsigned int) i_last_track,
                  toc[i_last_track+1].m, toc[i_last_track+1].s);
    if (i_data && i_first_track != CDIO_CDROM_LEADOUT_TRACK)
      snprintf(line+len, sizeof(line)-len, ", audio=%u-%u",
	      (unsigned int) i_first_audio_track,
              (unsigned int) i_last_audio_track);

    display_line(LINE_ARTIST, 0, "CD Artist       : %s", artist);
    display_line(LINE_CDNAME, 0, "CD Title        : %s", title);
    display_line(LINE_GENRE,  0, "CD Genre        : %s", genre);
    display_line(LINE_YEAR,   0, "CD Year         : %s", year);
  }

  mvprintw(LINE_CDINFO, 0, (char *) "CD info: %s", line);
  clrtoeol();
  action(NULL);
}

/* ---------------------------------------------------------------------- */

static void
usage(char *prog)
{
    fprintf(stderr,
            "%s is a simple curses CD player.  It can pick up artist,\n"
            "CD name and song title from CD-Text info on the CD or\n"
            "via CDDB.\n"
            "\n"
            "usage: %s [options] [device]\n"
            "\n"
            "default for to search for a CD-ROM device with a CD-DA loaded\n"
            "\n"
            "These command line options available:\n"
            "  -h      print this help\n"
            "  -k      print key mapping\n"
            "  -a      start up in auto-mode\n"
            "  -v      verbose\n"
            "\n"
            "for non-interactive use (only one) of these:\n"
            "  -l      list tracks\n"
            "  -c      print cover (PostScript to stdout)\n"
            "  -C      close CD-ROM tray. If you use this option,\n"
            "          a CD-ROM device name must be specified.\n"
            "  -p      play the whole CD\n"
            "  -t n    play track >n<\n"
            "  -t a-b  play all tracks between a and b (inclusive)\n"
            "  -L      set volume level\n"
            "  -s      stop playing\n"
            "  -S      list audio subchannel information\n"
            "  -e      eject cdrom\n"
            "\n"
            "That's all. Oh, maybe a few words more about the auto-mode. This\n"
            "is the 'dont-touch-any-key' feature. You load a CD, player starts\n"
            "to play it, and when it is done it ejects the CD. Start it that\n"
            "way on a spare console and forget about it...\n"
            "\n"
            "(c) 1997-98 Gerd Knorr <kraxel@goldbach.in-berlin.de>\n"
            "(c) 2005-2006, 2017 Rocky Bernstein <rocky@gnu.org>\n"
            , prog, prog);
}

static void
print_keys(void)
{
  unsigned int i;
  for (i=0; i < i_key_bindings; i++)
    fprintf(stderr, "%s\n", key_bindings[i]);
}

static void
keypress_wait(CdIo_t *p_cdio)
  {
    action("press any key to continue");
    while (1 != select_wait(b_cd ? 1 : 5)) {
      read_subchannel(p_cdio);
      display_status(true);
    }
    (void) getch();
    clrtobot();
    action(NULL);
    if (!b_all_tracks)
      display_cdinfo(p_cdio, i_tracks_global, i_first_track_global);
    i_last_display_track = CDIO_INVALID_TRACK;
  }

static void
list_keys(void)
{
  unsigned int i;
  for (i=0; i < i_key_bindings; i++) {
    mvprintw(LINE_TRACK_PREV+i, 0, (char *) "%s", key_bindings[i]);
    clrtoeol();
  }
  keypress_wait(p_cdio_global);
}

static void
display_tracks(void)
{
  track_t i;
  int i_line=0;
  int s;

  if (b_record) {
    i_line=LINE_TRACK_PREV - 1;
    for (i = i_first_track_global; i <= i_last_track; i++) {
      char line[200]="";
      s = cdio_audio_get_msf_seconds(&toc[i+1])
        - cdio_audio_get_msf_seconds(&toc[i]);
      read_subchannel(p_cdio_global);
      snprintf(line, sizeof(line), "%2d  %02d:%02d  %s ", i,
              s / CDIO_CD_SECS_PER_MIN,  s % CDIO_CD_SECS_PER_MIN,
              ( ( sub.audio_status == CDIO_MMC_READ_SUB_ST_PLAY ||
                  sub.audio_status == CDIO_MMC_READ_SUB_ST_PAUSED ) &&
                sub.track == i ) ? "->" : " |");
      if (b_record) {
        if ( strlen(cd_info[i].title) )
          strcat(line, cd_info[i].title);
        if ( strlen(cd_info[i].artist) > 0 ) {
          if (strlen(cd_info[i].title))
            strcat(line, " / ");
          strcat(line, cd_info[i].artist);
        }
      }
      if (sub.track == i) {
        attron(A_STANDOUT);
        mvprintw(i_line++, 0, line);
        attroff(A_STANDOUT);
      } else
        mvprintw(i_line++, 0, line);
      clrtoeol();
    }
  }
}

/*
 * PostScript 8bit latin1 handling
 * stolen from mpage output -- please don't ask me how this works...
 */
#define ENCODING_TRICKS \
        "/reencsmalldict 12 dict def\n"                         \
        "/ReEncodeSmall { reencsmalldict begin\n"               \
        "/newcodesandnames exch def /newfontname exch def\n"    \
        "/basefontname exch def\n"                              \
        "/basefontdict basefontname findfont def\n"             \
        "/newfont basefontdict maxlength dict def\n"            \
        "basefontdict { exch dup /FID ne { dup /Encoding eq\n"  \
        "{ exch dup length array copy newfont 3 1 roll put }\n" \
        "{ exch newfont 3 1 roll put }\n"                       \
        "ifelse }\n"                                            \
        "{ pop pop }\n"                                         \
        "ifelse } forall\n"                                     \
        "newfont /FontName newfontname put\n"                   \
        "newcodesandnames aload pop newcodesandnames length 2 idiv\n"   \
        "{ newfont /Encoding get 3 1 roll put } repeat\n"       \
        "newfontname newfont definefont pop end } def\n"        \
        "/charvec [\n"          \
        "026 /Scaron\n"         \
        "027 /Ydieresis\n"      \
        "028 /Zcaron\n"         \
        "029 /scaron\n"         \
        "030 /trademark\n"      \
        "031 /zcaron\n"         \
        "032 /space\n"          \
        "033 /exclam\n"         \
        "034 /quotedbl\n"       \
        "035 /numbersign\n"     \
        "036 /dollar\n"         \
        "037 /percent\n"        \
        "038 /ampersand\n"      \
        "039 /quoteright\n"     \
        "040 /parenleft\n"      \
        "041 /parenright\n"     \
        "042 /asterisk\n"       \
        "043 /plus\n"           \
        "044 /comma\n"          \
        "045 /minus\n"          \
        "046 /period\n"         \
        "047 /slash\n"          \
        "048 /zero\n"           \
        "049 /one\n"            \
        "050 /two\n"            \
        "051 /three\n"          \
        "052 /four\n"           \
        "053 /five\n"           \
        "054 /six\n"            \
        "055 /seven\n"          \
        "056 /eight\n"          \
        "057 /nine\n"           \
        "058 /colon\n"          \
        "059 /semicolon\n"      \
        "060 /less\n"           \
        "061 /equal\n"          \
        "062 /greater\n"        \
        "063 /question\n"       \
        "064 /at\n"             \
        "065 /A\n"              \
        "066 /B\n"              \
        "067 /C\n"              \
        "068 /D\n"              \
        "069 /E\n"              \
        "070 /F\n"              \
        "071 /G\n"              \
        "072 /H\n"              \
        "073 /I\n"              \
        "074 /J\n"              \
        "075 /K\n"              \
        "076 /L\n"              \
        "077 /M\n"              \
        "078 /N\n"              \
        "079 /O\n"              \
        "080 /P\n"              \
        "081 /Q\n"              \
        "082 /R\n"              \
        "083 /S\n"              \
        "084 /T\n"              \
        "085 /U\n"              \
        "086 /V\n"              \
        "087 /W\n"              \
        "088 /X\n"              \
        "089 /Y\n"              \
        "090 /Z\n"              \
        "091 /bracketleft\n"    \
        "092 /backslash\n"      \
        "093 /bracketright\n"   \
        "094 /asciicircum\n"    \
        "095 /underscore\n"     \
        "096 /quoteleft\n"      \
        "097 /a\n"              \
        "098 /b\n"              \
        "099 /c\n"              \
        "100 /d\n"              \
        "101 /e\n"              \
        "102 /f\n"              \
        "103 /g\n"              \
        "104 /h\n"              \
        "105 /i\n"              \
        "106 /j\n"              \
        "107 /k\n"              \
        "108 /l\n"              \
        "109 /m\n"              \
        "110 /n\n"              \
        "111 /o\n"              \
        "112 /p\n"              \
        "113 /q\n"              \
        "114 /r\n"              \
        "115 /s\n"              \
        "116 /t\n"              \
        "117 /u\n"              \
        "118 /v\n"              \
        "119 /w\n"              \
        "120 /x\n"              \
        "121 /y\n"              \
        "122 /z\n"              \
        "123 /braceleft\n"      \
        "124 /bar\n"            \
        "125 /braceright\n"     \
        "126 /asciitilde\n"     \
        "127 /.notdef\n"        \
        "128 /fraction\n"       \
        "129 /florin\n"         \
        "130 /quotesingle\n"    \
        "131 /quotedblleft\n"   \
        "132 /guilsinglleft\n"  \
        "133 /guilsinglright\n" \
        "134 /fi\n"             \
        "135 /fl\n"             \
        "136 /endash\n"         \
        "137 /dagger\n"         \
        "138 /daggerdbl\n"      \
        "139 /bullet\n"         \
        "140 /quotesinglbase\n" \
        "141 /quotedblbase\n"   \
        "142 /quotedblright\n"  \
        "143 /ellipsis\n"       \
        "144 /dotlessi\n"       \
        "145 /grave\n"          \
        "146 /acute\n"          \
        "147 /circumflex\n"     \
        "148 /tilde\n"          \
        "149 /oe\n"             \
        "150 /breve\n"          \
        "151 /dotaccent\n"      \
        "152 /perthousand\n"    \
        "153 /emdash\n"         \
        "154 /ring\n"           \
        "155 /Lslash\n"         \
        "156 /OE\n"             \
        "157 /hungarumlaut\n"   \
        "158 /ogonek\n"         \
        "159 /caron\n"          \
        "160 /lslash\n"         \
        "161 /exclamdown\n"     \
        "162 /cent\n"           \
        "163 /sterling\n"       \
        "164 /currency\n"       \
        "165 /yen\n"            \
        "166 /brokenbar\n"      \
        "167 /section\n"        \
        "168 /dieresis\n"       \
        "169 /copyright\n"      \
        "170 /ordfeminine\n"    \
        "171 /guillemotleft\n"  \
        "172 /logicalnot\n"     \
        "173 /hyphen\n"         \
        "174 /registered\n"     \
        "175 /macron\n"         \
        "176 /degree\n"         \
        "177 /plusminus\n"      \
        "178 /twosuperior\n"    \
        "179 /threesuperior\n"  \
        "180 /acute\n"          \
        "181 /mu\n"             \
        "182 /paragraph\n"      \
        "183 /periodcentered\n" \
        "184 /cedilla\n"        \
        "185 /onesuperior\n"    \
        "186 /ordmasculine\n"   \
        "187 /guillemotright\n" \
        "188 /onequarter\n"     \
        "189 /onehalf\n"        \
        "190 /threequarters\n"  \
        "191 /questiondown\n"   \
        "192 /Agrave\n"         \
        "193 /Aacute\n"         \
        "194 /Acircumflex\n"    \
        "195 /Atilde\n"         \
        "196 /Adieresis\n"      \
        "197 /Aring\n"          \
        "198 /AE\n"             \
        "199 /Ccedilla\n"       \
        "200 /Egrave\n"         \
        "201 /Eacute\n"         \
        "202 /Ecircumflex\n"    \
        "203 /Edieresis\n"      \
        "204 /Igrave\n"         \
        "205 /Iacute\n"         \
        "206 /Icircumflex\n"    \
        "207 /Idieresis\n"      \
        "208 /Eth\n"            \
        "209 /Ntilde\n"         \
        "210 /Ograve\n"         \
        "211 /Oacute\n"         \
        "212 /Ocircumflex\n"    \
        "213 /Otilde\n"         \
        "214 /Odieresis\n"      \
        "215 /multiply\n"       \
        "216 /Oslash\n"         \
        "217 /Ugrave\n"         \
        "218 /Uacute\n"         \
        "219 /Ucircumflex\n"    \
        "220 /Udieresis\n"      \
        "221 /Yacute\n"         \
        "222 /Thorn\n"          \
        "223 /germandbls\n"     \
        "224 /agrave\n"         \
        "225 /aacute\n"         \
        "226 /acircumflex\n"    \
        "227 /atilde\n"         \
        "228 /adieresis\n"      \
        "229 /aring\n"          \
        "230 /ae\n"             \
        "231 /ccedilla\n"       \
        "232 /egrave\n"         \
        "233 /eacute\n"         \
        "234 /ecircumflex\n"    \
        "235 /edieresis\n"      \
        "236 /igrave\n"         \
        "237 /iacute\n"         \
        "238 /icircumflex\n"    \
        "239 /idieresis\n"      \
        "240 /eth\n"            \
        "241 /ntilde\n"         \
        "242 /ograve\n"         \
        "243 /oacute\n"         \
        "244 /ocircumflex\n"    \
        "245 /otilde\n"         \
        "246 /odieresis\n"      \
        "247 /divide\n"         \
        "248 /oslash\n"         \
        "249 /ugrave\n"         \
        "250 /uacute\n"         \
        "251 /ucircumflex\n"    \
        "252 /udieresis\n"      \
        "253 /yacute\n"         \
        "254 /thorn\n"          \
        "255 /ydieresis\n"      \
        "] def"


static void
ps_list_tracks(void)
{
  int i,s,y,sy;

  if (!b_record) return;

  printf("%%!PS-Adobe-2.0\n");

  /* encoding tricks */
  puts(ENCODING_TRICKS);
  printf("/Times /TimesLatin1 charvec ReEncodeSmall\n");
  printf("/Helvetica /HelveticaLatin1 charvec ReEncodeSmall\n");

  /* Spaces */
  printf("0 setlinewidth\n");
  printf(" 100 100 moveto\n");
  printf(" 390   0 rlineto\n");
  printf("   0 330 rlineto\n");
  printf("-390   0 rlineto\n");
  printf("closepath stroke\n");

  printf(" 100 100 moveto\n");
  printf("-16    0 rlineto\n");
  printf("  0  330 rlineto\n");
  printf("422    0 rlineto\n");
  printf("  0 -330 rlineto\n");
  printf("closepath stroke\n");

  /* Title */
  printf("/TimesLatin1 findfont 24 scalefont setfont\n");
  printf("120 400 moveto (%s) show\n", title);
  printf("/TimesLatin1 findfont 18 scalefont setfont\n");
  printf("120 375 moveto (%s) show\n", artist);

  /* List */
  sy = 250 / i_last_track;
  if (sy > 14) sy = 14;
  printf("/labelfont /TimesLatin1 findfont %d scalefont def\n",sy-2);
  printf("/timefont /Courier findfont %d scalefont def\n",sy-2);
  y = 350;
  for (i = i_first_track_global; i <= i_last_track; i++, y -= sy) {
    s = cdio_audio_get_msf_seconds(&toc[i+1])
      - cdio_audio_get_msf_seconds(&toc[i]);

    printf("labelfont setfont\n");
    printf("120 %d moveto (%d) show\n", y, i);
    {
      char line[200]="";
      if ( strlen(cd_info[i].title) )
        strcat(line, cd_info[i].title);
      if ( strlen(cd_info[i].artist) > 0 ) {
        if (strlen(cd_info[i].title))
          strcat(line, " / ");
        strcat(line, cd_info[i].artist);
      }
      printf("150 %d moveto (%s) show\n", y, line);
    }
    printf("timefont setfont\n");
    printf("420 %d moveto (%2d:%02d) show\n", y,
           s / CDIO_CD_SECS_PER_MIN, s % CDIO_CD_SECS_PER_MIN);
  }

  /* Seitenbanner */
  printf("/HelveticaLatin1 findfont 12 scalefont setfont\n");
  printf(" 97 105 moveto (%s: %s) 90 rotate show -90 rotate\n",
         artist, title);
  printf("493 425 moveto (%s: %s) -90 rotate show 90 rotate\n",
         artist, title);
  printf("showpage\n");
}

static void
list_tracks(void)
{
  int i,s;

  if (!b_record) return;

  printf("Title : %s\n", title);
  printf("Artist: %s\n", artist);

  for (i = i_first_track_global; i <= i_last_track; i++) {
    s = cdio_audio_get_msf_seconds(&toc[i+1])
      - cdio_audio_get_msf_seconds(&toc[i]);
    printf("%2d: %s [%d seconds]\n", i, cd_info[i].title, s);
  }
}

typedef enum {
  NO_OP=0,
  PLAY_CD=1,
  PLAY_TRACK=2,
  STOP_PLAYING=3,
  EJECT_CD=4,
  CLOSE_CD=5,
  SET_VOLUME=6,
  LIST_SUBCHANNEL=7,
  LIST_KEYS=8,
  LIST_TRACKS=9,
  PS_LIST_TRACKS=10,
  TOGGLE_PAUSE=11,
  EXIT_PROGRAM=12
} cd_operation_t;

int
main(int argc, char *argv[])
{
  int  c, nostop=0;
  char *h;
  int  i_rc = 0;
  cd_operation_t cd_op = NO_OP; /* operation to do in non-interactive mode */


  psz_program = strrchr(argv[0],'/');
  psz_program = psz_program ? psz_program+1 : argv[0];

  memset(&cddb_opts, 0, sizeof(cddb_opts));

  cdio_loglevel_default = CDIO_LOG_WARN;
  /* parse options */
  while ( 1 ) {
    if (-1 == (c = getopt(argc, argv, "acCdehkplL:sSt:vx")))
      break;
    switch (c) {
    case 'v':
      b_verbose = true;
      if (cdio_loglevel_default > CDIO_LOG_INFO)
        cdio_loglevel_default = CDIO_LOG_INFO;
      break;
    case 'd':
      debug = 1;
      if (cdio_loglevel_default > CDIO_LOG_DEBUG)
      cdio_loglevel_default = CDIO_LOG_DEBUG;
      break;
    case 'a':
      auto_mode = 1;
      break;

    case 'L':
      i_volume_level = atoi(optarg);
      cd_op = SET_VOLUME;
      b_interactive = false;
      break;

    case 't':
      if (NULL != (h = strchr(optarg,'-'))) {
        *h = 0;
        start_track = atoi(optarg);
        stop_track = atoi(h+1)+1;
        if (0 == start_track) start_track = 1;
        if (1 == stop_track)  stop_track  = CDIO_CDROM_LEADOUT_TRACK;
      } else {
        start_track = atoi(optarg);
        stop_track = start_track+1;
        one_track = 1;
      }
      b_interactive = false;
      cd_op = PLAY_TRACK;
      break;
    case 'p':
      b_interactive = false;
      cd_op = PLAY_CD;
      break;
    case 'l':
      b_interactive = false;
      cd_op = LIST_TRACKS;
      break;
    case 'C':
      b_interactive = false;
      cd_op = CLOSE_CD;
      break;
    case 'c':
      b_interactive = false;
      cd_op = PS_LIST_TRACKS;
      break;
    case 's':
      b_interactive = false;
      cd_op = STOP_PLAYING;
      break;
    case 'S':
      b_interactive = false;
      cd_op = LIST_SUBCHANNEL;
      break;
    case 'e':
      b_interactive = false;
      cd_op = EJECT_CD;
      break;
    case 'k':
      print_keys();
      exit(1);
    case 'h':
      usage(psz_program);
      exit(1);
    default:
      usage(psz_program);
      exit(1);
    }
  }

  if (argc > optind) {
    psz_device_global = strdup(argv[optind]);
  } else {
    char **ppsz_cdda_drives=NULL;
    char **ppsz_all_cd_drives = cdio_get_devices_ret(&driver_id);

    if (!ppsz_all_cd_drives) {
      fprintf(stderr, "Can't find a CD-ROM drive\n");
      exit(2);
    }
    ppsz_cdda_drives = cdio_get_devices_with_cap(ppsz_all_cd_drives,
                                                 CDIO_FS_AUDIO, false);
    if (!ppsz_cdda_drives || !ppsz_cdda_drives[0]) {
      fprintf(stderr, "Can't find a CD-ROM drive with a CD-DA in it\n");
      cdio_free_device_list(ppsz_all_cd_drives);
      exit(3);
    }
    psz_device_global = strdup(ppsz_cdda_drives[0]);
    cdio_free_device_list(ppsz_all_cd_drives);
    cdio_free_device_list(ppsz_cdda_drives);
  }

  if (!b_interactive) {
    b_sig = true;
    nostop=1;
  }

  tty_raw();
  signal(SIGINT,ctrlc);
  signal(SIGQUIT,ctrlc);
  signal(SIGTERM,ctrlc);
  signal(SIGHUP,ctrlc);
  signal(SIGWINCH, sigwinch);

  if (CLOSE_CD != cd_op) {
    /* open device */
    if (b_verbose)
      fprintf(stderr, "open %s... ", psz_device_global);
    p_cdio_global = cdio_open (psz_device_global, driver_id);
    if (!p_cdio_global && cd_op != EJECT_CD) {
      cd_close(psz_device_global);
      p_cdio_global = cdio_open (psz_device_global, driver_id);
    }

    if (p_cdio_global && b_verbose)
      fprintf(stderr,"ok\n");
  }

  if (b_interactive) {
#ifdef HAVE_CDDB
    cddb_log_set_handler (cddb_log_handler);
#else
    ;
#endif
  }  else {
    b_sig = true;
    nostop=1;
    if (EJECT_CD == cd_op) {
      i_rc = cd_eject() ? 0 : 1;
    } else {
      switch (cd_op) {
      case PS_LIST_TRACKS:
      case LIST_TRACKS:
      case PLAY_TRACK:
        read_toc(p_cdio_global);
      default:
        break;
      }
      if (p_cdio_global)
        switch (cd_op) {
        case STOP_PLAYING:
          b_cd = true;
          i_rc = cd_stop(p_cdio_global) ? 0 : 1;
          break;
        case EJECT_CD:
          /* Should have been handled above. */
          cd_eject();
          break;
        case LIST_TRACKS:
          list_tracks();
          break;
        case PS_LIST_TRACKS:
          ps_list_tracks();
          break;

        case PLAY_TRACK:
          /* play just this one track */
          if (b_record) {
            printf("%s / %s\n", artist, title);
            if (one_track)
              printf("%s\n", cd_info[start_track].title);
          }
          i_rc = play_track(start_track, stop_track) ? 0 : 1;
          break;

        case PLAY_CD:
          if (b_record)
            printf("%s / %s\n", artist, title);
          play_track(1,CDIO_CDROM_LEADOUT_TRACK);
          break;

        case SET_VOLUME:
          i_rc = set_volume_level(p_cdio_global, i_volume_level);
          break;

        case LIST_SUBCHANNEL:
          if (read_subchannel(p_cdio_global)) {
            if (sub.audio_status == CDIO_MMC_READ_SUB_ST_PAUSED ||
                sub.audio_status == CDIO_MMC_READ_SUB_ST_PLAY) {
              {
                printf("track %2d - %02x:%02x (%02x:%02x abs) ",
                       sub.track, sub.rel_addr.m, sub.rel_addr.s,
                       sub.abs_addr.m, sub.abs_addr.s);
              }
            }
            printf("drive state: %s\n",
                   mmc_audio_state2str(sub.audio_status));
          } else {
            i_rc = 1;
          }
          break;
        case CLOSE_CD: /* Handled below */
        case LIST_KEYS:
        case TOGGLE_PAUSE:
        case EXIT_PROGRAM:
        case NO_OP:
          break;
        }
      else if (CLOSE_CD == cd_op) {
        i_rc = (DRIVER_OP_SUCCESS == cdio_close_tray(psz_device_global, NULL))
                ? 0 : 1;
      } else {
        fprintf(stderr,"no CD in drive (%s)\n", psz_device_global);
      }
    }
  }

  /* Play all tracks *unless* we have a play or paused status
     already. */

  read_subchannel(p_cdio_global);
  if (sub.audio_status != CDIO_MMC_READ_SUB_ST_PAUSED &&
      sub.audio_status != CDIO_MMC_READ_SUB_ST_PLAY)
    play_track(1, CDIO_CDROM_LEADOUT_TRACK);

  while ( !b_sig ) {
    int key;
    if (!b_cd) read_toc(p_cdio_global);
    read_subchannel(p_cdio_global);
    display_status(false);

    if (1 == select_wait(b_cd ? 1 : 5)) {
      switch (key = getch()) {
      case '-':
        decrease_volume_level(p_cdio_global);
        break;
      case '+':
        increase_volume_level(p_cdio_global);
        break;
      case 'A':
      case 'a':
        auto_mode = !auto_mode;
        break;
      case 'X':
      case 'x':
        nostop=1;
        /* fall through */
      case 'Q':
      case 'q':
        b_sig = true;
        break;
      case 'E':
      case 'e':
        cd_eject();
        break;
      case 's':
        cd_stop(p_cdio_global);
        break;
      case 'C':
      case 'c':
        cd_close(psz_device_global);
        break;
      case 'L':
      case 'l':
        b_all_tracks = !b_all_tracks;
        if (b_all_tracks)
          display_tracks();
        else {
          i_last_display_track = CDIO_INVALID_TRACK;
          display_cdinfo(p_cdio_global, i_tracks_global, i_first_track_global);
        }

        break;
      case 'K':
      case 'k':
      case 'h':
      case 'H':
      case '?':
        list_keys();
        break;
      case ' ':
      case 'P':
      case 'p':
        toggle_pause();
        break;
      case KEY_RIGHT:
        if (b_cd &&
            (sub.audio_status == CDIO_MMC_READ_SUB_ST_PAUSED ||
             sub.audio_status == CDIO_MMC_READ_SUB_ST_PLAY))
          play_track(sub.track+1, CDIO_CDROM_LEADOUT_TRACK);
        else
          play_track(1,CDIO_CDROM_LEADOUT_TRACK);
        break;
      case KEY_LEFT:
        if (b_cd &&
            (sub.audio_status == CDIO_MMC_READ_SUB_ST_PAUSED ||
             sub.audio_status == CDIO_MMC_READ_SUB_ST_PLAY))
          play_track(sub.track-1,CDIO_CDROM_LEADOUT_TRACK);
        break;
      case KEY_UP:
        if (b_cd && sub.audio_status == CDIO_MMC_READ_SUB_ST_PLAY)
          skip(10);
        break;
      case KEY_DOWN:
        if (b_cd && sub.audio_status == CDIO_MMC_READ_SUB_ST_PLAY)
          skip(-10);
        break;
      case '1':
      case '2':
      case '3':
      case '4':
      case '5':
      case '6':
      case '7':
      case '8':
      case '9':
        play_track(key - '0', CDIO_CDROM_LEADOUT_TRACK);
        break;
      case '0':
        play_track(10, CDIO_CDROM_LEADOUT_TRACK);
        break;
      case KEY_F(1):
      case KEY_F(2):
      case KEY_F(3):
      case KEY_F(4):
      case KEY_F(5):
      case KEY_F(6):
      case KEY_F(7):
      case KEY_F(8):
      case KEY_F(9):
      case KEY_F(10):
      case KEY_F(11):
      case KEY_F(12):
      case KEY_F(13):
      case KEY_F(14):
      case KEY_F(15):
      case KEY_F(16):
      case KEY_F(17):
      case KEY_F(18):
      case KEY_F(19):
      case KEY_F(20):
        play_track(key - KEY_F(1) + 11, CDIO_CDROM_LEADOUT_TRACK);
        break;
      }
    }
  }
  if (!nostop) cd_stop(p_cdio_global);
  tty_restore();
  finish("bye", i_rc);

  return 0; /* keep compiler happy */
}