/* linux/kernel/chr_drv/sound/ym3812.c

A low level driver for Yamaha YM3812 FM -chips (AdLib).

(C) 1992  Hannu Savolainen (hsavolai@cs.helsinki.fi) */

/* Major improvements to the FM handling 30AUG92 by Rob Hooft, */
/* hooft@chem.ruu.nl */

#include "sound_config.h"

#if defined(CONFIGURE_SOUNDCARD) && !defined(EXCLUDE_YM3812)

#include <linux/types.h>
#include <linux/errno.h>
#include <linux/signal.h>
#include <linux/fcntl.h>
#include <linux/sched.h>
#include <linux/timer.h>
#include <linux/tty.h>
#include <linux/ctype.h>
#include <asm/io.h>
#include <asm/segment.h>
#include <asm/system.h>
#include <sys/kd.h>
#include <linux/wait.h>
#include <linux/soundcard.h>
#include "sound_calls.h"

#include "dev_table.h"

#define OUTB outb
#define DEB(WHAT)		/* (WHAT) */
#define DEB1(WHAT)		/* (WHAT) */

static unsigned char curr_octave[20] = {0};	

typedef struct sbi_instrument instr_array[SBFM_MAXINSTR];
static instr_array instrmap;
static struct sbi_instrument *active_instrument[30] =
{NULL};

static struct synth_info fm_info =
{SYNTH_TYPE_FM, FM_TYPE_ADLIB, 0, 9, 0, SBFM_MAXINSTR};

static int already_initialized = 0;

static int ym3812_ok = 0;
static int ym3812_busy = 0;
static int fm_model = 0;	/* 0=no fm, 1=mono, 2=SB Pro 1, 3=SB Pro 2	 */
static unsigned char drum_mask = 0;

static int store_instr (int instr_no, struct sbi_instrument *instr);
static void note_to_tone (int note, int *block, int *fnum);
static void ym3812_command (int io_addr, const unsigned char addr, const unsigned char val);
static int ym3812_kill_note (int dev, int v_cell);
static void ym3812_initperc (void);

static int
ym3812_ioctl (int dev,
	      unsigned int cmd, unsigned int arg)
{
  switch (cmd)
    {

      case SNDCTL_FM_LOAD_INSTR:
      {
	struct sbi_instrument ins;

	memcpy_fromfs ((char *) &ins, (char *) arg, sizeof (ins));

	if (ins.channel < 0 || ins.channel >= SBFM_MAXINSTR)
	  {
	    printk ("SB Error: Invalid instrument number %d\n", ins.channel);
	    return -EINVAL;
	  }

	return store_instr (ins.channel, &ins);
      }
      break;

    case SNDCTL_SYNTH_INFO:
      memcpy_tofs ((char *) arg, &fm_info, sizeof (fm_info));
      return sizeof (fm_info);
      break;

    case SNDCTL_SEQ_PERCMODE:
      if (arg)
	{
	  int instr;
	  for (instr = 6; instr < 9; instr++)
	    ym3812_kill_note (dev, instr);

	  ym3812_initperc ();
	  fm_info.perc_mode = 1;
	  fm_info.nr_voices = 6;
	  fm_info.nr_drums = 5;
	  drum_mask = 0;
	}
      else
	{
	  int instr;
	  ym3812_command (FM_MONO, 0xbd, 0x00);	/* Perc mode off */
	  for (instr = 6; instr < 9; instr++)
	    ym3812_kill_note (dev, instr);


	  fm_info.perc_mode = 0;
	  fm_info.nr_voices = 9;
	  fm_info.nr_drums = 0;
	}
      return arg;
      break;

    default:
      return -EINVAL;
    }

}

int
ym3812_detect (int ioaddr)
{
  /* This function returns 1 if the FM chicp is present at the given I/O port
     The detection algorithm plays with the timer built in the FM chip and
     looks for a change in the status register.
  
  Note! The timers of the FM chip are not connected to AdLib (and compatible)
     boards.
  
  Note2! The chip is initialized if detected. */

  unsigned char stat1, stat2;
  unsigned long jif;
  int i;

  if (already_initialized)
    return 0;			/* Do avoid duplicate initializations */

  ym3812_command (ioaddr, 4, 0x60);	/* Reset timers 1 and 2 */
  ym3812_command (ioaddr, 4, 0x80);	/* Reset the IRQ of FM chicp */
  stat1 = inb (ioaddr);		/* Read status register */

  if ((stat1 & 0xE0) != 0x00)
    return 0;			/* Should be 0x00	 */

  ym3812_command (ioaddr, 2, 0xff);	/* Set timer 1 to 0xff */
  ym3812_command (ioaddr, 4, 0x21);	/* Unmask and start timer 1 */

  /* Now we have to delay at least 80 msec so we wait at least 10 ms to be
     sure. */
  jif = jiffies;
  while (jif == jiffies);	/* Wait until the timer interrupt occurs */
  jif = jiffies;
  while (jif == jiffies);	/* Wait until the timer interrupt occurs
				   again. Delay between two interrupts is 10
				   ms */

  stat2 = inb (ioaddr);		/* Read status after timers have expired */

  ym3812_command (ioaddr, 4, 0x60);	/* Reset timers 1 and 2 (cleanup) */
  ym3812_command (ioaddr, 4, 0x80);	/* Reset the IRQ of FM chicp
					   (cleanup) */

  if ((stat2 & 0xE0) != 0xc0)
    return 0;			/* There is no YM3812 */


  /* There is a FM chicp in this address. Now set some default values. */

  for (i = 0; i < 9; i++)
    ym3812_command (ioaddr, i + 0xb0, 0);	/* Note off */

  ym3812_command (ioaddr, 1, 0x20);	/* Reset chip, enable waveform
					   selection */
  ym3812_command (ioaddr, 0xbd, 0);	/* Melodic mode */

  return 1;
}


static void
vcell_addr (int v_cell, int *cell, int *ioaddr)
{
  if (fm_model == 1)
    {
      *cell = v_cell;
      *ioaddr = FM_MONO;
      return;
    }

  /* SoundBlaster dual FM chips in mono mode */
  *cell = v_cell >> 1;

  if (v_cell & 0x01)
    *ioaddr = FM_LEFT;
  else
    *ioaddr = FM_RIGHT;
}

static int
ym3812_kill_note (int dev, int v_cell)
{
  unsigned char op_cell_num;
  int ioaddr, cell_no;

  vcell_addr (v_cell, &cell_no, &ioaddr);
  /* cell = cell % 3 + ((cell / 3) << 3); */

  DEB (printk ("Kill note %d\n", v_cell));

  op_cell_num = 0xB0 + cell_no;
  ym3812_command (ioaddr, op_cell_num, curr_octave[v_cell]);
  return 0;
}

#define HIHAT			0
#define CYMBAL			1
#define TOMTOM			2
#define SNARE			3
#define BDRUM			4
#define UNDEFINED		TOMTOM
#define DEFAULT			TOMTOM

static unsigned char drum_map[128] =
{
  UNDEFINED,			/* 0 */
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,			/* 20 */
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,			/* 30 */
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,
  UNDEFINED,			/* 34 */
  BDRUM,
  BDRUM,
  DEFAULT,			/* Side Stick */
  SNARE,
  DEFAULT,			/* Hand Clap */
  SNARE,			/* 40 */
  TOMTOM,
  HIHAT,
  TOMTOM,
  HIHAT,
  TOMTOM,
  HIHAT,
  TOMTOM,
  TOMTOM,
  CYMBAL,
  TOMTOM,			/* 50 */
  CYMBAL,
  CYMBAL,
  DEFAULT,			/* Ride Bell */
  DEFAULT,			/* Tambourine */
  CYMBAL,
  DEFAULT,			/* Cowbell */
  CYMBAL,
  DEFAULT,			/* Vibraslap */
  CYMBAL,
  DEFAULT,			/* High Bongo *//* 60 */
  DEFAULT,			/* Low Bongo */
  DEFAULT,			/* Mute High Conga */
  DEFAULT,			/* Open High Conga */
  DEFAULT,			/* Low Conga */
  DEFAULT,			/* High Timbale */
  DEFAULT,			/* Low Timbale */
  DEFAULT,			/* High Agogo */
  DEFAULT,			/* Low Agogo */
  DEFAULT,			/* Cabasa */
  DEFAULT,			/* Maracas *//* 70 */
  DEFAULT,			/* Short Whistle */
  DEFAULT,			/* Long Whistle */
  DEFAULT,			/* Short Guiro */
  DEFAULT,			/* Long Guiro */
  DEFAULT,			/* Claves */
  DEFAULT,			/* High Wood Block */
  DEFAULT,			/* Low Wood Block */
  DEFAULT,			/* Mute Cuica */
  DEFAULT,			/* Mute Triangle */
  DEFAULT			/* Open Triangle *//* 80 */
};

static int
ym3812_kill_drum (int dev, int drum)
{
  unsigned char mask;

  drum = drum_map[drum];

  mask = 1 << drum;
  drum_mask &= ~mask;

  ym3812_command (FM_MONO, 0xbd, 0x20 | drum_mask);
  return 0;
}

static int
store_instr (int instr_no, struct sbi_instrument *instr)
{
  memcpy ((char *) &(instrmap[instr_no]), (char *) instr, sizeof (*instr));

  return 0;
}

static int
ym3812_set_instr (int dev, int v_cell, int instr_no)
{
  int cell, ioaddr, cell_no;
  unsigned char op_cell_num;
  struct sbi_instrument *instr;

  instr = &instrmap[instr_no];
  active_instrument[v_cell] = instr;

  vcell_addr (v_cell, &cell_no, &ioaddr);

  cell = cell_no % 3 + ((cell_no / 3) << 3);

  DEB (printk ("set_instr, chan=%d, cell=%d ioaddr=%x\n", cell_no, instr_no, ioaddr));
  if (instr->channel < 0)
    printk (
	  "SB Warning: initializing cell %d with undefined instrument %d\n",
	     v_cell, instr_no);

  /* Set Sound Characteristics */
  op_cell_num = 0x20 + (char) cell;
  ym3812_command (ioaddr, op_cell_num, instr->operators[0]);
  op_cell_num += 3;
  ym3812_command (ioaddr, op_cell_num, instr->operators[1]);

  /* Set Attack/Decay */
  op_cell_num = 0x60 + (char) cell;
  ym3812_command (ioaddr, op_cell_num, instr->operators[4]);
  op_cell_num += 3;
  ym3812_command (ioaddr, op_cell_num, instr->operators[5]);

  /* Set Sustain/Release */
  op_cell_num = 0x80 + (char) cell;
  ym3812_command (ioaddr, op_cell_num, instr->operators[6]);
  op_cell_num += 3;
  ym3812_command (ioaddr, op_cell_num, instr->operators[7]);

  /* Set Wave Select */
  op_cell_num = 0xE0 + (char) cell;
  ym3812_command (ioaddr, op_cell_num, instr->operators[8]);
  op_cell_num += 3;
  ym3812_command (ioaddr, op_cell_num, instr->operators[9]);

  /* Set Wave Feedback/Selectivity */
  op_cell_num = 0xC0 + (char) cell_no;
  ym3812_command (ioaddr, op_cell_num, instr->operators[10]);

  return 0;
}

/* The next table looks magical, but it certainly is not. Its values have
   been calculated as table[i]=8*log(i/64)/log(2) with an obvious exception
   for i=0. This log-table converts a linear volume-scaling (0..127) to a
   logarithmic scaling as present in the FM-synthesizer chips. so :    Volume
   64 =  0 db = relative volume  0 and:    Volume 32 = -6 db = relative
   volume -8 it was implemented as a table because it is only 128 bytes and
   it saves a lot of log() calculations. (RH) */
char fm_volume_table[128] =
{-64, -48, -40, -35, -32, -29, -27, -26,	/* 0 -   7 */
 -24, -23, -21, -20, -19, -18, -18, -17,	/* 8 -  15 */
 -16, -15, -15, -14, -13, -13, -12, -12,	/* 16 -  23 */
 -11, -11, -10, -10, -10, -9, -9, -8,	/* 24 -  31 */
 -8, -8, -7, -7, -7, -6, -6, -6,/* 32 -  39 */
 -5, -5, -5, -5, -4, -4, -4, -4,/* 40 -  47 */
 -3, -3, -3, -3, -2, -2, -2, -2,/* 48 -  55 */
 -2, -1, -1, -1, -1, 0, 0, 0,	/* 56 -  63 */
 0, 0, 0, 1, 1, 1, 1, 1,	/* 64 -  71 */
 1, 2, 2, 2, 2, 2, 2, 2,	/* 72 -  79 */
 3, 3, 3, 3, 3, 3, 3, 4,	/* 80 -  87 */
 4, 4, 4, 4, 4, 4, 4, 5,	/* 88 -  95 */
 5, 5, 5, 5, 5, 5, 5, 5,	/* 96 - 103 */
 6, 6, 6, 6, 6, 6, 6, 6,	/* 104 - 111 */
 6, 7, 7, 7, 7, 7, 7, 7,	/* 112 - 119 */
 7, 7, 7, 8, 8, 8, 8, 8};	/* 120 - 127 */

static void
calc_vol (unsigned char *regbyte, int volume)
{
  int level = (~*regbyte & 0x3f);

  if (level)
    level += fm_volume_table[volume];

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

  *regbyte = (*regbyte & 0xc0) | (~level & 0x3f);
}

static int
ym3812_start_note (int dev, int v_cell, int note, int volume)
{
  int cell, ioaddr, cell_no;
  unsigned char op_cell_num, data, vol1, vol2;
  int block, fnum;
  struct sbi_instrument *instr;

  instr = active_instrument[v_cell];

  if (!instr)
    instr = &instrmap[0];

  vcell_addr (v_cell, &cell_no, &ioaddr);

  cell = cell_no % 3 + ((cell_no / 3) << 3);

  DEB (printk ("Start note %d, chan=%d, cell=%d\n", note, cell_no, cell));

  note_to_tone (note, &block, &fnum);

  /* Kill previous note before playing */
  op_cell_num = 0xB0 + cell_no;
  ym3812_command (ioaddr, op_cell_num, 0);

  /* Set level */
  vol1 = instr->operators[2];
  vol2 = instr->operators[3];

  if ((instr->operators[10] & 0x01))
    {				/* Additive synthesis	 */
      calc_vol (&vol1, volume);
      calc_vol (&vol2, volume);
    }
  else
    {				/* FM synthesis */
      calc_vol (&vol2, volume);
    }

  op_cell_num = 0x40 + (char) cell;
  ym3812_command (ioaddr, op_cell_num, vol1);
  op_cell_num += 3;
  ym3812_command (ioaddr, op_cell_num, vol2);

  /* Play note */

  op_cell_num = 0xA0 + cell_no;
  data = fnum & 0xff;		/* Least significant bits of fnumber */
  ym3812_command (ioaddr, op_cell_num, data);

  op_cell_num = 0xB0 + cell_no;
  data = 0x20 | (curr_octave[v_cell] = ((block & 0x7) << 2) | ((fnum >> 8) & 0x3));	/* KEYON|OCTAVE|MS bits
								   of fnumber */
  ym3812_command (ioaddr, op_cell_num, data);

  return 0;
}

static int
ym3812_start_drum (int dev, int v_cell, int drum, int volume)
{
  unsigned char mask;

  volume >>= 2;	/* Kludge */

  ym3812_kill_drum (dev, drum);

  drum = drum_map[drum];

  mask = 1 << drum;
  drum_mask |= mask;

  ym3812_command (FM_MONO, 0xbd, 0x20 | drum_mask);

  return 0;
}

static void
note_to_tone (int note, int *block, int *fnum)
{

  /* Converts a MIDI note to block and fnum values for the FM chip */

  static short fntab[12] =
  {
    363, 385, 408, 432, 458, 485,
    514, 544, 577, 611, 647, 686
  };

  *block = (note - 1) / 12;
  *fnum = fntab[(note - 1) % 12];

  if (*block > 7)
    {				/* Note too high */

      *block = 0;
      *fnum = 0;
    }
}

static void
ym3812_command (int io_addr, const unsigned char addr, const unsigned char val)
{
  OUTB (addr, io_addr);		/* Select register	 */
  tenmicrosec ();
  OUTB (val, io_addr + 1);	/* Write to register	 */
  tenmicrosec ();
  tenmicrosec ();
  tenmicrosec ();		/* thirtymicrosec() */
}

static void
ym3812_reset (int dev)
{
  int i;

  for (i = 0; i < fm_info.nr_voices; i++)
    ym3812_kill_note (dev, i);

}

static int
ym3812_open (int dev, int mode)
{
  if (!ym3812_ok)
    return -ENODEV;
  if (ym3812_busy)
    return -EBUSY;
  ym3812_busy = 1;
  return 0;
}

static void
ym3812_close (int dev)
{
  ym3812_busy = 0;
  if (fm_info.perc_mode)
    ym3812_command (FM_MONO, 0xbd, 0x00);	/* Perc. mode off */
  fm_info.nr_voices = 9;
  fm_info.nr_drums = 0;
  fm_info.perc_mode = 0;

  ym3812_reset (dev);
}

static struct synth_operations ym3812_operations =
{
  ym3812_open,
  ym3812_close,
  ym3812_ioctl,
  ym3812_kill_note,
  ym3812_start_note,
  ym3812_set_instr,
  ym3812_reset,
  ym3812_kill_drum,
  ym3812_start_drum
};

long
ym3812_init (long mem_start)
{
  synth_devs[num_synths++] = &ym3812_operations;
  fm_model = 0;
  ym3812_ok = 1;

  if (0)			/* (dsp_model == 2 && detect_adlib (FM_LEFT)
				   && detect_adlib (FM_RIGHT)) */
    {
      printk ("SoundBlaster Pro: dual FM chips detected and initialized\n");
      fm_model = 2;
      fm_info.nr_voices = 18;
      fm_info.nr_drums = 0;
    }
  else
    {
      printk ("AdLib compatible FM chip detected and initialized\n");
      fm_model = 1;
      fm_info.nr_voices = 9;
      fm_info.nr_drums = 0;
    };

  already_initialized = 1;

  return mem_start;
}

static void
ym3812_init_drumcell (int offs)
{

  /* ADSR 	 */

  ym3812_command (FM_MONO, 0x60 + offs, 0x88);
  ym3812_command (FM_MONO, 0x80 + offs, 0x88);

  /* KSL/Total Level 	 */

  ym3812_command (FM_MONO, 0x40 + offs, 0x00);
}

static void
ym3812_initperc (void)
{
  int cell;

  ym3812_command (FM_MONO, 0xBD, 0x20);	/* Percussive mode ON */

  /* Bass drum frequencies (melodic voices 7 and 8 */

  ym3812_command (FM_MONO, 0xA6, 0x4E);
  ym3812_command (FM_MONO, 0xB6, 0x10);
  ym3812_command (FM_MONO, 0xA7, 0xC8);
  ym3812_command (FM_MONO, 0xB7, 0x0E);
  ym3812_command (FM_MONO, 0xA8, 0xA8);
  ym3812_command (FM_MONO, 0xB8, 0x0E);

  for (cell = 0x10; cell < 0x16; cell++)
    ym3812_init_drumcell (cell);
}

#endif
