Stephen Williams (stephenw32768) wrote,
Stephen Williams
stephenw32768

Musical coding

So, I've got a load of chiptunes from various old games, and a plugin for Audacious to play them, but no command-line player. There's a library that can decode them, but no-one seems to have written a command-line app that uses it.

Better do it myself, then...

It's for Linux, but should be portable to any other Unix-like OS that provides the Open Sound System audio API (which is most of them, I think). As well as playing the music, it can dump the music as linear PCM in AU format, suitable for burning to CD, MP3ifying, etc.

/*
  nysagme.c
  Command-line music player using GME

  Copyright (c) 2007 Stephen Williams (http://nysa.cx/)

  Permission is hereby granted, free of charge, to any person
  obtaining a copy of this software and associated documentation files
  (the "Software"), to deal in the Software without restriction,
  including without limitation the rights to use, copy, modify, merge,
  publish, distribute, sublicense, and/or sell copies of the Software,
  and to permit persons to whom the Software is furnished to do so,
  subject to the following conditions:

  The above copyright notice and this permission notice shall be
  included in all copies or substantial portions of the Software.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
  BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
  ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  SOFTWARE.

  The above distribution terms apply _only_ to this application,
  nysagme, and _not_ to the Game Music Emu library against which it
  must be linked.  Game Music Emu is distributed under the terms of
  the GNU Lesser General Public License.  See the Game Music Emu
  distribution at http://slack.net/~ant/libs/audio.html for details.
 */

static char nysagme_id[] =
"@(#) $Id: nysagme.c,v 1.6 2007/09/02 17:10:57 stephen Exp $";

#define _BSD_SOURCE   /* needed for usleep() */
#define _XOPEN_SOURCE /* needed for swab() */
#include <arpa/inet.h>
#include <errno.h>
#include <getopt.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/fcntl.h>
#include <sys/ioctl.h>
#include <sys/soundcard.h>
#include <sys/types.h>
#include <unistd.h>

#include <gme.h>


/*** Definitions ***/

#define NR_SAMPLES    (2048)        /* number of samples to buffer */
#define RATE_DEFAULT  (48000)       /* default sample rate */
#define RATE_MIN      (8000)        /* minimum sample rate */
#define RATE_MAX      (48000)       /* maximum sample rate */
#define DISPLAY_TIMER (verbose >= 2)/* when to display the timer */

/* Storage for options */
typedef struct {
  char *audio_device; /* OSS output device, or "-" for stdout */
  int desired_length; /* Desired playback length before fade in seconds */
  int pause;          /* Length of time to pause between tracks in millisecs */
  int rate;           /* Desired sample rate */
  int track;          /* Track number to play */
  int write_header;   /* Whether to write AU sample header */
} nysagme_options;

/* AU file format header, from http://en.wikipedia.org/wiki/Au_file_format */
typedef struct {
  uint32_t magic;
  uint32_t header_size;
  uint32_t data_size;
  uint32_t encoding;
  uint32_t rate;
  uint32_t channels;
} au_header;


/*** Globals ***/

static Music_Emu *emu;    /* GME instance */
static int audio_fd = -1; /* Audio device file descriptor */
static int verbose;       /* How much output to produce */
static char help_text[] =
"nysagme -- command line music player using the Game Music Emu library\n"
"Copyright (c) 2007 Stephen Williams (http://nysa.cx/)\n"
"\n"
"The Game Music Emu library used by nysagme is distributed under\n"
"the terms of the GNU Lesser General Public License.  See the Game\n"
"Music Emu distribution at http://slack.net/~ant/libs/audio.html\n"
"for details.\n"
"\n"
"Usage: nysagme [options] filename [...filename...]\n"
"\n"
"Options:\n"
"-h : Display this help text.\n"
"-l : Specify the length of the playback before fading out, in seconds.\n"
"     Has no effect if the track doesn't loop and is shorter than the\n"
"     specified time.\n"
"-o : Specify the output file.  Assumed to be an Open Sound System audio\n"
"     device, defaults to /dev/dsp.  Alternatively, set to \"-\" to write\n"
"     an AU format sample to standard output.\n"
"-p : Specify a pause between tracks, in milliseconds.  Default is no pause.\n"
"-r : Specify the sample rate in hertz.  Minimum %d, maximum %d.\n"
"     Defaults to %d.\n"
"-t : For files with more than one track, specify the track number.\n"
"     Default is 1.\n"
"-v : Verbose output.  Display information about the track being played.\n"
"-vv: Verboser output.  Display track information and a timer.\n"
"-x : When writing a sample to standard output, omit the AU format header.\n"
"     Output will be raw signed, big-endian, stereo, 16-bit linear PCM.\n"
"\n";



/*** Utility functions ***/

/* Fail with an error message */
#ifdef __GNUC__
static void die(const char *, ...) __attribute__ ((noreturn));
#endif
static void die(const char *fmt, ...)
{
  va_list ap;

  va_start(ap, fmt);
  vfprintf(stderr, fmt, ap);
  fprintf(stderr, "\n");
  va_end(ap);
  exit(1);
}


/* Displays the program's options. */
static void display_help(void)
{
  fprintf(stderr, help_text, RATE_MIN, RATE_MAX, RATE_DEFAULT);
}


/* Prints a track info field, with caption, if the field is set */
static void print_info_field(const char *caption, const char *field)
{
  if(field && *field)
    fprintf(stderr, "%-10s: %s\n", caption, field);
}


/* Prints formatted elapsed and total time */
static void print_time(const long millis_elapsed,
                       const long total_millis)
{
  int hours_elapsed = millis_elapsed / 3600000;
  int mins_elapsed = (millis_elapsed / 60000) % 60;
  int secs_elapsed = (millis_elapsed / 1000) % 60;
  int tenths_elapsed = (millis_elapsed / 100) % 10;
  int total_hours = total_millis / 3600000;
  int total_mins = (total_millis / 60000) % 60;
  int total_secs = (total_millis / 1000) % 60;
  int total_tenths = (total_millis / 100) % 10;

  /*
  fprintf(stderr, " %02d:%02d:%02d.%d of %02d:%02d:%02d.%d\r",
          hours_elapsed, mins_elapsed, secs_elapsed, tenths_elapsed,
          total_hours, total_mins, total_secs, total_tenths);
  */
  fprintf(stderr, "Time      : %02d:%02d:%02d.%d of %02d:%02d:%02d.%d\r",
          hours_elapsed, mins_elapsed, secs_elapsed, tenths_elapsed,
          total_hours, total_mins, total_secs, total_tenths);
  fflush(stderr); /* probably unnecessary */
}


/* Parses options into supplied structure, returns index of first
   non-option argument in argv */
static int parse_options(const int argc,
                         char *argv[],
                         nysagme_options *options)
{
  int option;
  extern char *optarg;
  extern int optind;

  while((option = getopt(argc, argv, "hl:o:p:r:t:vx")) != -1) {
    switch(option) {
    case 'h':
      display_help();
      exit(0);

    case 'l':
      options->desired_length = atoi(optarg);
      if(options->desired_length < 1) {
        fprintf(stderr, "bad length %s, ignoring\n", optarg);
        options->desired_length = 0;
      }
      break;

    case 'o':
      options->audio_device = optarg;
      break;

    case 'p':
      options->pause = atoi(optarg);
      break;

    case 'r':
      options->rate = atoi(optarg);
      if((options->rate < RATE_MIN) || (options->rate > RATE_MAX)) {
        fprintf(stderr, "bad rate %s, using default\n", optarg);
        options->rate = RATE_DEFAULT;
      }
      break;

    case 't':
      options->track = atoi(optarg) - 1;
      break;

    case 'v':
      verbose++;
      break;

    case 'x':
      options->write_header = 0;
      break;

    default:
      /* bad option -- getopt() prints error message */
      display_help();
      exit(1);
    }
  }

  return optind;
}


/*** Setup and teardown functions ***/

/* Frees resources */
static void cleanup(void)
{
  if(audio_fd != -1) close(audio_fd);
  if(emu) gme_delete(emu);
}


/* Sets up the audio device, returns sample rate */
static int init_audio(const char *audio_device, const int desired_rate)
{
  /* signed 16-bit linear PCM, native byte order, stereo */
  int channels = 2, format = AFMT_S16_NE, rate = desired_rate, width = 16, ver;

  if(audio_fd == -1) audio_fd = open(audio_device, O_RDWR);
  if(audio_fd == -1) die("couldn't open audio device %s", audio_device);

  errno = 0;
  ioctl(audio_fd, OSS_GETVERSION, &ver);
  if(errno) die("%s is not an OSS audio device", audio_device);

  ioctl(audio_fd, SNDCTL_DSP_RESET, 0);
  ioctl(audio_fd, SOUND_PCM_WRITE_BITS, &width);
  ioctl(audio_fd, SNDCTL_DSP_SETFMT, &format);
  if((width != 16) || (format != AFMT_S16_NE))
    die("%s does not support 16-bit samples", audio_device);
  ioctl(audio_fd, SOUND_PCM_WRITE_CHANNELS, &channels);
  if(channels != 2)
    die("%s does not support stereo output", audio_device);
  ioctl(audio_fd, SOUND_PCM_WRITE_RATE, &rate);
  
  return rate;
}


/* Sets up GME, returns millisecond length of track */
static long init_emu(const int track, const long desired_length_millis)
{
#define FADE_LENGTH (7500L) /* in milliseconds, from trial and error */
  gme_err_t gme_err;
  track_info_t info;
  long millis;

  if((gme_err = gme_track_info(emu, &info, track)))
    die(gme_err);

  /*
   * Heuristic for calculating length before fade:
   * - if the track length is > 0, use it or the desired length,
   *   whichever is shorter;
   * - if a desired length has been passed in, use it;
   * - if the loop length > 0, play for intro + loop * 2;
   * - otherwise, default to 2.5 minutes (150000 msec)
   */
  if((info.length > 0) && (info.loop_length == 0)) {
    /* track has a length and no loop */
    if((desired_length_millis <= 0) ||
       (info.length < (desired_length_millis + FADE_LENGTH)))
      gme_set_fade(emu, (millis = info.length));
    else {
      gme_set_fade(emu, (millis = desired_length_millis));
      millis += FADE_LENGTH;
    }
  } else if(info.length > 0) {
    /* has a loop; won't cut off immediately at info.length */
    if(desired_length_millis > 0)
      gme_set_fade(emu, (millis = desired_length_millis));
    else
      gme_set_fade(emu, (millis = info.length));
    /* total length includes fade */
    millis += FADE_LENGTH;
  } else { /* no length specified in track */
    if(desired_length_millis > 0)
      millis = desired_length_millis;
    else if(info.loop_length > 0)
      millis = info.intro_length + (2L * info.loop_length);
    else
      millis = 150000L;
    gme_set_fade(emu, millis);
    /* total length includes fade */
    millis += FADE_LENGTH;
  }

  /* this is mostly plagiarized from demo/features.c in the GME
     distribution */
  if(verbose) {
    fprintf(stderr, "\n" );
    print_info_field("System", info.system);
    print_info_field("Game", info.game);
    print_info_field("Author", info.author);
    print_info_field("Copyright", info.copyright);
    print_info_field("Comment", info.comment);
    print_info_field("Dumped by", info.dumper);
    fprintf(stderr, "\nTrack     : %d/%d\n",
            (int)(track + 1), (int)info.track_count );
    print_info_field("Name", info.song);
    if(!DISPLAY_TIMER)
      fprintf(stderr, "Length    : %02d:%02d:%02d.%d\n\n",
              (int)(millis / 3600000),
              (int)(millis / 60000) % 60,
              (int)(millis / 1000) % 60,
              (int)(millis / 100) % 10);
  }

  return millis;
}


/*** Audio output functions ***/

/* Dumps track to standard output */
static void dump(const long millisecs_length, const int bytes_per_millisec)
{
  short buf[NR_SAMPLES];
  short swab_buf[NR_SAMPLES];
  int bytes_written = 0;
  int little_endian = (htons((short)0x0001) != (short)0x0001);
  void *output_buf = little_endian ? &swab_buf : &buf;
  long millis_elapsed;

  if(DISPLAY_TIMER) {
    millis_elapsed = 0L;
    print_time(0L, millisecs_length);
  }

  /* generate samples and write to stdout */
  while(!gme_track_ended(emu)) {
    gme_err_t gme_err = gme_play(emu, NR_SAMPLES, buf);
    if(gme_err) die(gme_err);

    /* AU samples are in network byte order (big-endian) */
    if(little_endian)
      swab(&buf, &swab_buf, sizeof(short) * NR_SAMPLES);

    if(fwrite(output_buf, 1, sizeof(short) * NR_SAMPLES, stdout) !=
       sizeof(short) * NR_SAMPLES)
      die("error writing to stdout");
    bytes_written += sizeof(short) * NR_SAMPLES;

    if(DISPLAY_TIMER) { /* print timer if 1/10s of samples have been written */
      long millis_now = bytes_written / bytes_per_millisec;
      if((millis_now / 100) > (millis_elapsed / 100)) {
        if(millis_elapsed <= millisecs_length)
          print_time(millis_elapsed, millisecs_length);
      }
      millis_elapsed = millis_now;
    }
  }

  if(DISPLAY_TIMER) fprintf(stderr, "\n\n");
}


/* Generates the requested number of milliseconds of silence and
   writes them to stdout */
static void dump_silence(const int milliseconds, const int bytes_per_millisec)
{
  size_t bytes = (size_t)(milliseconds * bytes_per_millisec);
  void *zeroes = calloc(bytes, 1);
  int written = fwrite(zeroes, 1, bytes, stdout);

  free(zeroes);
  if(written != bytes) die("error writing to stdout");
}


/* Performs playback */
static void play(const long millisecs_length, const int bytes_per_millisec)
{
  short buf[NR_SAMPLES];
  count_info ci;
  long millis_elapsed;

  if(DISPLAY_TIMER) {
    millis_elapsed = 0L;
    print_time(0L, millisecs_length);
  }

  /* play stuff */
  while(!gme_track_ended(emu)) {
    gme_err_t gme_err = gme_play(emu, NR_SAMPLES, buf);
    if(gme_err) die(gme_err);

    errno = 0;
    write(audio_fd, buf, sizeof(short) * NR_SAMPLES);
    if(errno) die(strerror(errno));

    if(DISPLAY_TIMER) { /* print timer if 1/10s has elapsed */
      long millis_now;
      ioctl(audio_fd, SNDCTL_DSP_GETOPTR, &ci);
      millis_now = (ci.bytes) / bytes_per_millisec;
      if((millis_now / 100) > (millis_elapsed / 100)) {
        millis_elapsed = millis_now;
        if(millis_elapsed <= millisecs_length)
          print_time(millis_elapsed, millisecs_length);
      }
    }
  }

  /* keep the timer going until track has finished */
  if(DISPLAY_TIMER) {
    long millis_last = millis_elapsed;
    for(;;) {
      long millis_now;
      ioctl(audio_fd, SNDCTL_DSP_GETOPTR, &ci);
      millis_now = (ci.bytes) / bytes_per_millisec;
      if(millis_now == millis_last)
        /* nothing's happening -- track must've finished "early"
           (can happen when tracks lie about their lengths) */
        break;
      if(millis_now >= millisecs_length)
        /* all done */
        break;
      if((millis_now / 10) > (millis_elapsed / 10)) {
        millis_elapsed = millis_now;
        print_time(millis_elapsed, millisecs_length);
      }
      millis_last = millis_now;
      usleep(50000);
    }
    fprintf(stderr, "\n\n");
  }

  /* wait for playback to finish */
  ioctl(audio_fd, SOUND_PCM_SYNC, 0);
}


/* Writes an AU sample header to stdout */
static void write_header(const int rate)
{
  /* AU header values are always in network byte order (big-endian) */
  au_header header = {
    htonl(0x2e736e64),        /* magic number */
    htonl(sizeof(au_header)), /* header size */
    htonl(0xffffffff),        /* data size unknown */
    htonl(3),                 /* 16-bit linear PCM */
    htonl(rate),              /* sample rate */
    htonl(2)                  /* stereo */
  };

  fwrite(&header, 1, sizeof(au_header), stdout);
}


/*** Entry point ***/

int main(int argc, char *argv[])
{
  gme_err_t gme_err;
  nysagme_options options = { "/dev/dsp", 0, 0, RATE_DEFAULT, 0, 1 };
  long millisecs_length;
  int arg_index = parse_options(argc, argv, &options);
  int dump_to_stdout, rc = 0;

  if(arg_index >= argc) {
    /* no arguments */
    display_help();
    return 1;
  }

  atexit(cleanup);

  dump_to_stdout = (strcmp(options.audio_device, "-") == 0);
  if(dump_to_stdout && options.write_header) write_header(options.rate);

  for(; arg_index < argc; arg_index++) {
    int rate;

    if(!dump_to_stdout)
      rate = init_audio(options.audio_device, options.rate);
    else
      rate = options.rate;

    gme_err = gme_open_file(argv[arg_index], &emu, rate);
    if(!gme_err) gme_err = gme_start_track(emu, options.track);
    if(gme_err) {
      /* bad track, skip it */
      fprintf(stderr, "%s: %s\n", argv[arg_index], gme_err);
      if(emu) { gme_delete(emu); emu = 0; }
      rc = 1;
      continue;
    }

    millisecs_length = init_emu(options.track, options.desired_length * 1000L);

    if(dump_to_stdout)
      dump(millisecs_length, (rate * 2 * sizeof(short) / 1000));
    else
      play(millisecs_length, (rate * 2 * sizeof(short) / 1000));

    gme_delete(emu);
    emu = 0;
    close(audio_fd);
    audio_fd = -1;
    if(options.pause) {
      if(dump_to_stdout)
        dump_silence(options.pause, (rate * 2 * sizeof(short) / 1000));
      else
        usleep(options.pause * 1000);
    }
  }

  return rc;
}


Hmmm, I think this is going to be another of those entries that don't attract any comments...
Subscribe
  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 3 comments