/***************************************************************************
 midi_adlibemu.c Copyright (C) 2002 Solomon Peachy

 This program may be modified and copied freely according to the terms of
 the GNU general public license (GPL), as long as the above copyright
 notice and the licensing information contained herein are preserved.

 Please refer to www.gnu.org for licensing details.

 This work is provided AS IS, without warranty of any kind, expressed or
 implied, including but not limited to the warranties of merchantibility,
 noninfringement, and fitness for a specific purpose. The author will not
 be held liable for any damage caused by this work or derivatives of it.

 By using this source code, you agree to the licensing terms as stated
 above.

***************************************************************************/

#include <stdio.h>
#ifdef HAVE_UNISTD_H
  #include <unistd.h>
#endif
#include <string.h>
#include <midi_device.h>
#include <midiout.h>
#include <pcmout.h>
#include <soundserver.h>

#include "fmopl.h"

/* local function declarations */

int midi_adlibemu_reset(void);

//#define DEBUG_ADLIB
#define SCI_VOLUME_SCALE

/* portions shamelessly lifted from claudio's XMP */
/* other portions lifted from sound/opl3.c in the Linux kernel */

#define ADLIB_LEFT 0
#define ADLIB_RIGHT 1

/* #define OPL_INTERNAL_FREQ     3600000 */
#define OPL_INTERNAL_FREQ    3579545

static int ready = 0;

static int register_base[11] = {
    0x20, 0x23, 0x40, 0x43,
    0x60, 0x63, 0x80, 0x83,
    0xe0, 0xe3, 0xc0
};

static int register_offset[12] = {
  /* Channel           1     2     3     4     5     6     7     8     9  */
  /* Operator 1 */   0x00, 0x01, 0x02, 0x08, 0x09, 0x0A, 0x10, 0x11, 0x12, 0x18, 0x19, 0x1A

};

static int ym3812_note[13] = {
    0x157, 0x16b, 0x181, 0x198, 0x1b0, 0x1ca,
    0x1e5, 0x202, 0x220, 0x241, 0x263, 0x287,
    0x2ae
};

#ifdef SCI_VOLUME_SCALE
static guint8 sci_adlib_vol_base[16] = {
  0x00, 0x11, 0x15, 0x19, 0x1D, 0x22, 0x26, 0x2A, 
  0x2E, 0x23, 0x37, 0x3B, 0x3F, 0x3F, 0x3F, 0x3F
};
static guint8 sci_adlib_vol_tables[16][64];
#endif

/* ripped out of the linux kernel, of all places. */
static gint8 fm_volume_table[128] = {
  -64, -48, -40, -35, -32, -29, -27, -26,
  -24, -23, -21, -20, -19, -18, -18, -17,
  -16, -15, -15, -14, -13, -13, -12, -12,
  -11, -11, -10, -10, -10, -9, -9, -8,
  -8, -8, -7, -7, -7, -6, -6, -6,
  -5, -5, -5, -5, -4, -4, -4, -4,
  -3, -3, -3, -3, -2, -2, -2, -2,
  -2, -1, -1, -1, -1, 0, 0, 0,
  0, 0, 0, 1, 1, 1, 1, 1,
  1, 2, 2, 2, 2, 2, 2, 2,
  3, 3, 3, 3, 3, 3, 3, 4,
  4, 4, 4, 4, 4, 4, 4, 5,
  5, 5, 5, 5, 5, 5, 5, 5,
  6, 6, 6, 6, 6, 6, 6, 6,
  6, 7, 7, 7, 7, 7, 7, 7,
  7, 7, 7, 8, 8, 8, 8, 8
};

/* back to your regularly scheduled definitions */

static guint8 instr[MIDI_CHANNELS];
static guint16 pitch[MIDI_CHANNELS];
static guint8 vol[MIDI_CHANNELS];
static guint8 pan[MIDI_CHANNELS];
static int free_voices = ADLIB_VOICES;
static guint8 oper_note[ADLIB_VOICES];
static guint8 oper_chn[ADLIB_VOICES];

static FM_OPL *ym3812_L = NULL;
static FM_OPL *ym3812_R = NULL;

static guint8 adlib_reg_L[256];
static guint8 adlib_reg_R[256];
static guint8 adlib_master;


/* initialise note/operator lists, etc. */
void adlibemu_init_lists()
{
  int i;

#ifdef SCI_VOLUME_SCALE
  int j;

  for (i = 0 ; i < 16 ; i++) {
    for (j = 0; j < 64 ; j++) {
      sci_adlib_vol_tables[i][j] = ((guint16)sci_adlib_vol_base[i]) * j / 63;
    }
  }
#endif

  for (i = 0; i < MIDI_CHANNELS ; i++) {
    pitch[i] = 8192;  /* center the pitch wheel */
  }

  free_voices = ADLIB_VOICES;

  memset(instr, 0, sizeof(instr));
  memset(vol, 0x7f, sizeof(vol));
  memset(pan, 0x3f, sizeof(pan));
  memset(adlib_reg_L, 0, sizeof(adlib_reg_L));
  memset(adlib_reg_R, 0, sizeof(adlib_reg_R));
  memset(oper_chn, 0xff, sizeof(oper_chn));
  memset(oper_note, 0xff, sizeof(oper_note));
  adlib_master=12;
}

/* more shamelessly lifted from xmp and adplug.  And altered.  :) */

static inline int opl_write_L (int a, int v)
{
  adlib_reg_L[a] = v;
  OPLWrite (ym3812_L, 0x388, a);
  return OPLWrite (ym3812_L, 0x389, v);
}

static inline int opl_write_R (int a, int v)
{
  adlib_reg_R[a] = v;
  OPLWrite (ym3812_R, 0x388, a);
  return OPLWrite (ym3812_R, 0x389, v);
}

static inline int opl_write(int a, int v)
{
  opl_write_L(a,v);
  return opl_write_R(a,v);
}

/*
static inline guint8 opl_read (int a)
{
  OPLWrite (ym3812_L, 0x388, a);
  return OPLRead (ym3812_L, 0x389);
}
*/

void synth_setpatch (int voice, guint8 *data)
{
  int i;

  opl_write(0xBD, 0);

  for (i = 0; i < 10; i++)
    opl_write(register_base[i] + register_offset[voice], data[i]);

  opl_write(register_base[10] + voice, data[10]);

  /* mute voice after patch change */
  opl_write_L(0xb0 + voice, adlib_reg_L[0xb0+voice] & 0xdf);
  opl_write_R(0xb0 + voice, adlib_reg_R[0xb0+voice] & 0xdf);

#ifdef DEBUG_ADLIB
  for (i = 0; i < 10; i++)
    printf("%02x ", adlib_reg_L[register_base[i]+register_offset[voice]]);
    printf("%02x ", adlib_reg_L[register_base[10]+voice]);
#endif

}

void synth_setvolume_L (int voice, int volume)
{
  gint8 level1, level2;

  level1 = ~adlib_reg_L[register_base[2]+register_offset[voice]] & 0x3f;
  level2 = ~adlib_reg_L[register_base[3]+register_offset[voice]] & 0x3f;

  if (level1) {
#ifdef SCI_VOLUME_SCALE
    level1 += sci_adlib_vol_tables[adlib_master][volume>>1];
#else
    level1 += fm_volume_table[volume];
#endif
  }

  if (level2) {
#ifdef SCI_VOLUME_SCALE
    level2 += sci_adlib_vol_tables[adlib_master][volume>>1];
#else
    level2 += fm_volume_table[volume];
#endif
  }
  
  if (level1 > 0x3f)
    level1 = 0x3f;
  if (level1 < 0)
    level1 = 0;

  if (level2 > 0x3f)
    level2 = 0x3f;
  if (level2 < 0)
    level2 = 0;

  /* algorithm-dependent; we may need to set both operators. */
  if (adlib_reg_L[register_base[10]+voice] & 1)
    opl_write_L(register_base[2]+register_offset[voice],
	      (guint8)((~level1 &0x3f) |
	       (adlib_reg_L[register_base[2]+register_offset[voice]]&0xc0)));

  opl_write_L(register_base[3]+register_offset[voice],
	      (guint8)((~level2 &0x3f) |
	       (adlib_reg_L[register_base[3]+register_offset[voice]]&0xc0)));

}

void synth_setvolume_R (int voice, int volume)
{
  gint8 level1, level2;

  level1 = ~adlib_reg_R[register_base[2]+register_offset[voice]] & 0x3f;
  level2 = ~adlib_reg_R[register_base[3]+register_offset[voice]] & 0x3f;

  if (level1) {
#ifdef SCI_VOLUME_SCALE
    level1 += sci_adlib_vol_tables[adlib_master][volume>>1];
#else
    level1 += fm_volume_table[volume];
#endif
  }
    
  if (level2) {
#ifdef SCI_VOLUME_SCALE
    level2 += sci_adlib_vol_tables[adlib_master][volume>>1];
#else
    level2 += fm_volume_table[volume];
#endif
  }
  
  if (level1 > 0x3f)
    level1 = 0x3f;
  if (level1 < 0)
    level1 = 0;

  if (level2 > 0x3f)
    level2 = 0x3f;
  if (level2 < 0)
    level2 = 0;

  /* now for the other side. */
  if (adlib_reg_R[register_base[10]+voice] & 1)
    opl_write_R(register_base[2]+register_offset[voice],
		(guint8)((~level1 &0x3f) |
		 (adlib_reg_R[register_base[2]+register_offset[voice]]&0xc0)));
  
  opl_write_R(register_base[3]+register_offset[voice],
	      (guint8)((~level2 &0x3f) |
	       (adlib_reg_R[register_base[3]+register_offset[voice]]&0xc0)));

}

void synth_setnote (int voice, int note, int bend)
{
    int n, fre, oct, delta;

    delta = 0;

    n = note % 12;

    if (bend > 8192) {
      // pitch bend up.
      bend -= 8192;
      delta = (ym3812_note[n+1] - ym3812_note[n]) * bend / 8192; 
    } else if (bend < 8192) {
      // bend down.
      delta = (ym3812_note[n-1] - ym3812_note[n]) -
	((ym3812_note[n-1] - ym3812_note[n]) * bend / 8192);
    }

    fre = ym3812_note[n] + delta;

    oct = note / 12 - 1;

    if (oct < 0)
      oct = 0;

    opl_write(0xa0 + voice, fre & 0xff);
    opl_write(0xb0 + voice,
        0x20 | ((oct << 2) & 0x1c) | ((fre >> 8) & 0x03));
#ifdef DEBUG_ADLIB
    printf("-- %02x %02x\n", adlib_reg_L[0xa0+voice], adlib_reg_L[0xb0+voice]);
#endif

}


/* back to your regularly scheduled driver */

int adlibemu_stop_note(int chn, int note, int velocity)
{
  int i, op=255;

  for (i=0;i<ADLIB_VOICES && op==255;i++) {
    if (oper_chn[i] == chn)
      if (oper_note[i] == note)
	op=i;
  }

  if (op==255) {
#ifdef DEBUG_ADLIB
    printf ("ADLIB: can't stop note: C%02x N%02x V%02x\n", chn, note, velocity);
    printf ("C ");
    for (i = 0; i < ADLIB_VOICES ; i++ ) {
      printf ("%02x ", oper_chn[i]);
    }
    printf ("\n");
    printf ("N ");
    for (i = 0; i < ADLIB_VOICES ; i++ ) {
      printf ("%02x ", oper_note[i]);
    }
    printf ("\n");
#endif
    return -1; /* that note isn't playing.. */
  }

  opl_write_L(0xb0+op,(adlib_reg_L[0xb0+op] & 0xdf));
  opl_write_R(0xb0+op,(adlib_reg_R[0xb0+op] & 0xdf));

  oper_chn[op] = 255;
  oper_note[op] = 255;

  free_voices++;

#ifdef DEBUG_ADLIB
  printf("stop voice %d (%d rem):  C%02x N%02x V%02x\n", op, free_voices, chn, note, velocity);
#endif

  return 0;
}

int adlibemu_start_note(int chn, int note, int velocity)
{
  int op, volume_L, volume_R, inst = 0;

  if (velocity == 0) {
    return adlibemu_stop_note(chn, note, velocity);
  }

  if (free_voices <= 0) {
    printf("ADLIB: All voices full\n");  /* XXX implement overflow code */
    return -1;
  }
   
  for (op = 0; op < ADLIB_VOICES ; op++)
    if (oper_chn[op] == 255)
      break;

  if (op == ADLIB_VOICES) {
    printf("ADLIB:  WTF?  We couldn't find a voice yet it we have %d left.\n", free_voices);
    return -1;
  }

  /* Scale channel volume */
  volume_L = velocity * vol[chn] / 127;
  volume_R = velocity * vol[chn] / 127;

  /* Apply a pan */
  if (pcmout_stereo) {
    if (pan[chn] > 0x3f)  /* pan right; so we scale the left down. */
      volume_L = volume_L / 0x3f * (0x3f - (pan[chn] - 0x3f));
    else if (pan[chn] < 0x3f) /* pan left; so we scale the right down.*/
      volume_R = volume_R / 0x3f * (0x3f - (0x3f-pan[chn]));
  }
  inst = instr[chn];

  synth_setpatch(op, adlib_sbi[inst]);
  synth_setvolume_L(op, volume_L);
  synth_setvolume_R(op, volume_R);
  synth_setnote(op, note, pitch[chn]);

  oper_chn[op] = chn;
  oper_note[op] = note;
  free_voices--;

#ifdef DEBUG_ADLIB
  printf("play voice %d (%d rem):  C%02x N%02x V%02x/%02x-%02x P%02x (%02x/%02x)\n", op, free_voices, chn, note, velocity, volume_L, volume_R, inst,
	 adlib_reg_L[register_base[2]+register_offset[op]] & 0x3f,
	 adlib_reg_L[register_base[3]+register_offset[op]] & 0x3f);
#endif

  return 0;
}

void test_adlib () {

  int voice = 0;
#if 0
  guint8 data[] = { 0x25, 0x21, 0x48, 0x48, 0xf0, 0xf2, 0xf0, 0xa5, 0x00, 0x00, 0x06 };
#else
  guint8 *data = adlib_sbi[0x0a];
#endif

#if 1
  opl_write(register_base[0]+register_offset[voice], data[0]);
  opl_write(register_base[1]+register_offset[voice], data[1]);
  opl_write(register_base[2]+register_offset[voice], data[2]);
  opl_write(register_base[3]+register_offset[voice], data[3]);
  opl_write(register_base[4]+register_offset[voice], data[4]);
  opl_write(register_base[5]+register_offset[voice], data[5]);
  opl_write(register_base[6]+register_offset[voice], data[6]);
  opl_write(register_base[7]+register_offset[voice], data[7]);
  opl_write(register_base[8]+register_offset[voice], data[8]);
  opl_write(register_base[9]+register_offset[voice], data[9]);
  opl_write(register_base[10]+register_offset[voice], data[10]);
#else
  synth_setpatch(voice, data);
#endif

#if 0
  opl_write(0xA0 + voice, 0x57);
  opl_write(0xB0 + voice, 0x2d);
#else
  synth_setvolume_L(voice, 0x50);
  synth_setvolume_R(voice, 0x50);
  synth_setnote(voice, 0x30, 0);
#endif

  /*
  instr[0x0e] = 0x0a;
  instr[0x03] = 0x26;

  adlibemu_start_note(0x0e, 0x30, 0x40);
  sleep(1);
  adlibemu_start_note(0x03, 0x48, 0x40);
  sleep(1);
  */
}


/* count is # of FRAMES, not bytes.
   We assume 16-bit stereo frames (ie 4 bytes)
*/
static int synth_mixer (gint16 *buffer, int count)
{
  gint16 *ptr = buffer;

  if (!ready) {
      return 0;
  }

  if (!buffer)
  {
	  fprintf(stderr, "synth_mixer(): !buffer \n");
      return 0;
  }

#if 0
  {
    static unsigned long remaining_delta = 0;
    int samples;
    int remaining = count;

    while (remaining > 0) {
      samples = remaining_delta * pcmout_sample_rate / 1000000;
      samples = sci_min(samples, remaining);
      if (samples) {
        YM3812UpdateOne(ADLIB_LEFT, ptr, samples, 1);
        YM3812UpdateOne(ADLIB_RIGHT, ptr+1, samples, 1);
      }
      if (remaining > samples) {
        remaining_delta = (remaining - samples) * 1000000 / pcmout_sample_rate;
      } else {
        song->play_next_note();
        remaining_delta = song->get_next_delta();
        song->advance();
      }
      remaining -= samples;
    }
  }
#endif

  if (pcmout_stereo) {
    YM3812UpdateOne (ym3812_L, ptr, count, 1);
    YM3812UpdateOne (ym3812_R, ptr+1, count, 1);
  } else {
    YM3812UpdateOne (ym3812_L, ptr, count, 0);
  }

  return count;
}

int midi_adlibemu_open(guint8 *data_ptr, unsigned int data_length)
{
  int i;

  /* load up the patch.003 file, parse out the instruments */
  if (data_length < 1344) {
	  sciprintf ("[ADLIBEMU] invalid patch.003: Expected %d, got %d\n", 1344, data_length);
	  return -1;
  }

  for (i = 0; i < 48; i++)
    make_sbi((adlib_def *)(data_ptr+(28 * i)), adlib_sbi[i]);

  if (data_length > 1344)
    for (i = 48; i < 96; i++)
      make_sbi((adlib_def *)(data_ptr+2+(28 * i)), adlib_sbi[i]);

  OPLBuildTables(FMOPL_ENV_BITS_HQ, FMOPL_EG_ENT_HQ);

  if (!(ym3812_L = OPLCreate (OPL_TYPE_YM3812, OPL_INTERNAL_FREQ, pcmout_sample_rate)) ||
      !(ym3812_R = OPLCreate (OPL_TYPE_YM3812, OPL_INTERNAL_FREQ, pcmout_sample_rate))) {
    printf ("ADLIB:  Emulator init failed!\n");
    return -1;
  }

  ready = 1;

  pcmout_set_mixer(synth_mixer);

  return midi_adlibemu_reset();
}


int midi_adlibemu_close()
{
  FM_OPL *opl = ym3812_L;
  ym3812_L = NULL;
  OPLDestroy(opl);
  opl = ym3812_R;
  ym3812_R = NULL;
  OPLDestroy(opl);

  // XXX deregister with pcm layer.
  return 0;
}

int midi_adlibemu_reset(void)
{
  //  printf("AdlibEmu:  Reset\n");
  if (! ready)
    return -1;

  adlibemu_init_lists();

  OPLResetChip (ym3812_L);
  OPLResetChip (ym3812_R);

  opl_write(0x01, 0x20);
  opl_write(0xBD, 0xc0);

#ifdef DEBUG_ADLIB
  printf("ADLIB: reset complete\n");
#endif
  //  test_adlib();
  return 0;
}

int midi_adlibemu_reverb(short param)
{
  printf("ADLIB: reverb NYI %04x \n", param);
  return 0;
}

int midi_adlibemu_event(guint8 command, guint8 note, guint8 velocity, guint32 delta)
{
  guint8 channel, oper;

  channel = command & 0x0f;
  oper = command & 0xf0;

  switch (oper) {
  case 0x80:
    return adlibemu_stop_note(channel, note, velocity);
  case 0x90:  /* noteon and noteoff */
    return adlibemu_start_note(channel, note, velocity);
  case 0xe0:   /* Pitch bend */
    pitch[channel] = (note & 0x7f) | ((velocity & 0x7f) << 7);
    break;
  case 0xb0:    /* CC changes. */
    switch (note) {
    case 0x07:
      vol[channel] = velocity;
      break;
    case 0x0a:
      pan[channel] = velocity;
      break;
    case 0x7b:  { /* all notes off */
      int i = 0;
      for (i=0;i<ADLIB_VOICES;i++)
	if (oper_chn[i] == channel)
	  adlibemu_stop_note(channel, oper_note[i], 0);
      break;
    }
    default:
      ; /* XXXX ignore everything else for now */
    }
    return 0;
  case 0xd0:    /* aftertouch */
    /* XXX Aftertouch in the OPL thing? */
    return 0;
  default:
    printf("ADLIB: Unknown event %02x\n", command);
    return 0;
  }

  return 0;
}

int midi_adlibemu_event2(guint8 command, guint8 param, guint32 delta)
{
  guint8 channel;
  guint8 oper;

  channel = command & 0x0f;
  oper = command & 0xf0;
  switch (oper) {
  case 0xc0:   /* change instrument */
    instr[channel] = param;
    return 0;
  default:
    printf("ADLIB: Unknown event %02x\n", command);
  }

  return 0;
}

int midi_adlibemu_volume(guint8 volume)
{
  guint8 i;

  i = (guint8)volume * 15 / 100;

  adlib_master=i;

#ifdef DEBUG_ADLIB
  printf("ADLIB: master volume set to %d\n", adlib_master);
#endif

  return 0;
}

/* the driver struct */

midi_device_t midi_device_adlibemu = {
  "adlibemu",
  "v0.01",
  &midi_adlibemu_open,
  &midi_adlibemu_close,
  &midi_adlibemu_event,
  &midi_adlibemu_event2,
  &midi_adlibemu_reset,
#ifdef SCI_VOLUME_SCALE
  &midi_adlibemu_volume,
#else
  NULL,
#endif
  &midi_adlibemu_reverb,
  003,		/* patch.003 */
  0x04,		/* playflag */
  0, 		/* do not play channel 9 */
  ADLIB_VOICES  /* Max polyphony */
};
