/* 
   Sea noise recording program that uses direct access to the I2S and I2C
   busses to control the WM8731 codec.  Written to work on the Iriver H10
   PP5020 processor.
   
   Christian Lees
   DSTO
*/


#include "plugin.h"


PLUGIN_HEADER

/* pointer to the plugin api */
static const struct plugin_api *rb;

/* riff header used in wav files */
struct riff_header
{
    uint8_t  riff_id[4];      /* 00h - "RIFF"                            */
    uint32_t riff_size;       /* 04h - sz following headers + data_size  */
    /* format header */
    uint8_t  format[4];       /* 08h - "WAVE"                            */
    uint8_t  format_id[4];    /* 0Ch - "fmt "                            */
    uint32_t format_size;     /* 10h - 16 for PCM (sz format data)       */
    /* format data */
    uint16_t audio_format;    /* 14h - 1=PCM                             */
    uint16_t num_channels;    /* 16h - 1=M, 2=S, etc.                    */
    uint32_t sample_rate;     /* 18h - HZ                                */
    uint32_t byte_rate;       /* 1Ch - num_channels*sample_rate*bits_per_sample/8 */
    uint16_t block_align;     /* 20h - num_channels*bits_per_samples/8   */
    uint16_t bits_per_sample; /* 22h - 8=8 bits, 16=16 bits, etc.        */
    /* Not for audio_format=1 (PCM) */
/*  unsigned short extra_param_size;   24h - size of extra data          */ 
/*  unsigned char  *extra_params; */
    /* data header */
    uint8_t  data_id[4];      /* 24h - "data" */
    uint32_t data_size;       /* 28h - num_samples*num_channels*bits_per_sample/8 */
/*  unsigned char  *data;              2ch - actual sound data */
} __attribute__((packed));

#define RIFF_FMT_HEADER_SIZE       12 /* format -> format_size */
#define RIFF_FMT_DATA_SIZE         16 /* audio_format -> bits_per_sample */
#define RIFF_DATA_HEADER_SIZE       8 /* data_id -> data_size */

#define PCM_DEPTH_BYTES             2
#define PCM_DEPTH_BITS             16

static const struct riff_header riff_header =
{
    /* "RIFF" header */
    { 'R', 'I', 'F', 'F' },         /* riff_id          */
    0,                              /* riff_size   (*)  */
    /* format header */ 
    { 'W', 'A', 'V', 'E' },         /* format           */
    { 'f', 'm', 't', ' ' },         /* format_id        */
    H_TO_LE32(16),                  /* format_size      */
    /* format data */
    H_TO_LE16(1),                   /* audio_format     */
    0,                              /* num_channels (*) */
    0,                              /* sample_rate  (*) */
    0,                              /* byte_rate    (*) */
    0,                              /* block_align  (*) */
    H_TO_LE16(PCM_DEPTH_BITS),      /* bits_per_sample  */
    /* data header */
    { 'd', 'a', 't', 'a' },         /* data_id          */
    0                               /* data_size    (*) */
    /* (*) updated during ENC_END_FILE event */
};


/* structure to hold the plugin information */
struct noise_data {
	int interval;
	int duration;
	int runtime;
	int sample_rate;
	int sample_size;
	int fp;
	int pcm_samples;
	int channels;
	char filename[MAX_PATH];
};
static struct noise_data noise_data;

/* buffer used for recording */
static unsigned char *rec_buffer;
static ssize_t rec_buffer_size;

static int debug_fp;
static char str_buf[10];
static int count;

/*************************************************************************************************
 * low level code to handle the I2S data
 * borrowed from /firmware/target/arm/pcm_pp.c
 ************************************************************************************************/

#define SAMPLE_SIZE 16
#define PCM_CHUNK_SIZE 8192

struct dma_data
{
/* NOTE: The order of size and p is important if you use assembler
   optimised fiq handler, so don't change it. */
#if SAMPLE_SIZE == 16
    uint32_t *p;
#elif SAMPLE_SIZE == 32
    uint16_t *p;
#endif
    size_t size;
#if NUM_CORES > 1
    unsigned core;
#endif
    int locked;
    int state;
};

volatile pcm_more_callback_type2 pcm_callback_more_ready SHAREDBSS_ATTR = NULL;
static int pcm_record_callback(int);

extern void *fiq_function;

/* Dispatch to the proper handler and leave the main vector table alone */
void fiq_handler(void) ICODE_ATTR __attribute__((naked));
void fiq_handler(void)
{
    asm volatile (
        "ldr pc, [pc, #-4] \n"
    	"fiq_function:     \n"
        ".word 0           \n"
    );
}


static struct dma_data dma_rec_data SHAREDBSS_ATTR =
{
    /* Initialize to a locked, stopped state */
    .p = NULL,
    .size = 0,
#if NUM_CORES > 1
    .core = 0x00,
#endif
    .locked = 0,
    .state  = 0
};

/* For the locks, FIQ must be disabled because the handler manipulates
   IISCONFIG and the operation is not atomic - dual core support
   will require other measures */
void pcm_rec_lock(void)
{
    int status = disable_fiq_save();

    if (++dma_rec_data.locked == 1)
        IIS_IRQRX_REG &= ~IIS_IRQRX;

    restore_fiq(status);
}

void pcm_rec_unlock(void)
{
    int status = disable_fiq_save();

    if (--dma_rec_data.locked == 0 && dma_rec_data.state != 0)
        IIS_IRQRX_REG |= IIS_IRQRX;

    restore_fiq(status);
}

/* NOTE: direct stack use forbidden by GCC stack handling bug for FIQ */
void fiq_record(void) ICODE_ATTR __attribute__((interrupt ("FIQ")));
void fiq_record(void)
{
	pcm_rec_dma_stop();
/*
    register pcm_more_callback_type2 more_ready;
	count = 0;
	
    while (dma_rec_data.size > 0) {
        if (IIS_RX_FULL_COUNT < 2) {
            return;
        }

#if SAMPLE_SIZE == 16
        *dma_rec_data.p++ = IISFIFO_RD;
#elif SAMPLE_SIZE == 32
        *dma_rec_data.p++ = IISFIFO_RD >> 16;
        *dma_rec_data.p++ = IISFIFO_RD >> 16;
#endif
        dma_rec_data.size -= 4;
    }
    
    more_ready = pcm_callback_more_ready;


    if (more_ready == NULL || more_ready(0) < 0) {
        pcm_rec_dma_stop();
//        pcm_rec_dma_stopped_callback();
    }
*/
}

void pcm_record_more(void *start, size_t size)
{
    dma_rec_data.p    = start; /* Start of RX buffer    */
    dma_rec_data.size = size;  /* Bytes to transfer     */
}

void pcm_rec_dma_stop(void)
{
    /* disable interrupt */
    IIS_IRQRX_REG &= ~IIS_IRQRX;

    dma_rec_data.state = 0;
    dma_rec_data.size = 0;
    dma_rec_data.core = 0x00;

    /* disable fifo */
    IISCONFIG &= ~IIS_RXFIFOEN;
    IISFIFO_CFG |= IIS_RXCLR;    
}

void pcm_rec_dma_start(void *addr, size_t size)
{
    pcm_rec_dma_stop();

    dma_rec_data.p    = addr;
    dma_rec_data.size = size;

    dma_rec_data.core = processor_id(); /* save initiating core */

    /* setup FIQ handler */
    fiq_function = fiq_record;

    /* interrupt on full fifo, enable record fifo interrupt */
    dma_rec_data.state = 1;
    /* enable RX FIFO */
    IISCONFIG |= IIS_RXFIFOEN;

    /* enable IIS interrupt as FIQ */
    CPU_INT_PRIORITY |= IIS_MASK;
    CPU_INT_EN = IIS_MASK;
   	rb->fdprintf(debug_fp, "done pcm_rec_dma_start\n");
}

void pcm_rec_dma_init(void) {
    pcm_rec_dma_stop();
} /* pcm_init */


void pcm_record_start(void *addr, size_t size) {
	rb->fdprintf(debug_fp, "pcm_record_start\n");
	pcm_rec_lock();
	pcm_callback_more_ready	= pcm_record_callback;
	pcm_rec_dma_start(addr, size);
	pcm_rec_unlock();
}

void pcm_record_stop(void) {
	rb->fdprintf(debug_fp, "pcm_record_stop\n");
	pcm_rec_lock();
	pcm_rec_dma_stop();
	pcm_rec_unlock();
}

void pcm_record_init(void) {
	pcm_rec_lock();
	pcm_rec_dma_init();
	pcm_rec_unlock();
}

void setup_IIS(void) {
	/* lets configure the IIS bus to suit our purposes */
	/* 16 bit LE for now, this will change when we get 24 bit working */
	int status = disable_fiq_save();
	IISCONFIG |= IIS_RESET;		/* reset the bus */
	IISCONFIG &= ~IIS_RESET;
	
	IISCONFIG = (IISCONFIG &~ IIS_FORMAT_MASK) | IIS_FORMAT_IIS;
	IISCONFIG = (IISCONFIG &~ IIS_SIZE_MASK) | IIS_SIZE_16BIT;
	IISCONFIG = (IISCONFIG &~ IIS_FIFO_FORMAT_MASK) | IIS_FIFO_FORMAT_LE16_2;
	
	IISFIFO_CFG |= IIS_RX_FULL_LVL_12 | IIS_TX_EMPTY_LVL_12;
	IISFIFO_CFG |= IIS_RXCLR | IIS_TXCLR;
	restore_fiq(status);
}
	

/**************************************************************************************************
 * Plugin part of the code
 *************************************************************************************************/

/* menus */
static const struct opt_items sample_rate_menu[6] = {
	{"8 kHz", -1},
	{"32 kHz", -1},
	{"44.1 kHz", -1},
	{"48 kHz", -1},
	{"88.2 kHz", -1},
	{"96 kHz", -1}
};

static const struct opt_items sample_size_menu[2] = {
	{"16 bit", -1},
	{"24 bit", -1}
};

static const struct opt_items interval_menu[6] = {
	{"1 min", -1},
	{"2 min", -1},
	{"5 min", -1},
	{"10 min", -1},
	{"20 min", -1},
	{"1 hour", -1}
};

static const struct opt_items duration_menu[6] = {
	{"5 sec", -1},
	{"10 sec", -1},
	{"20 sec", -1},
	{"1 min", -1},
	{"5 min", -1},
	{"10 min", -1}
};

static const struct opt_items runtime_menu[6] = {
	{"forever", -1},
	{"1 hour", -1},
	{"5 hour", -1},
	{"10 hour", -1},
	{"24 hour", -1},
	{"48 hour", -1}
};


/* register values */
static char sample_rate[6] = {0x4d, 0x59, 0x63, 0x41, 0x7f, 0x5d};
static char sample_size[2] = {0x42, 0x4e};

/* time values */
static uint32_t interval[6] = {60, 120, 300, 600, 1200, 3600};
static uint32_t duration[6] = {5, 10, 20, 60, 300, 600};
static uint32_t runtime[6] = {0, 3600, 18000, 36000, 86400, 172800};

/* file values */
static uint32_t file_sample_rate[6] = {8000, 32000, 44100, 48000, 88200, 96000};

/* enable/disable the codec */
void codec_enable(int state) {
	rb->wmcodec_write(0x09, state);
}

/* configure the codec */
void codec_config(void) {
	/* set the sample rate */
	rb->wmcodec_write(0x08, sample_rate[noise_data.sample_rate]);
	
	/* set the sample size and interface type to I2S */
	rb->wmcodec_write(0x07, sample_size[noise_data.sample_size]);
	
	/* set the gains */
	rb->wmcodec_write(0x00, 0x17);	/* left 0dB */
	rb->wmcodec_write(0x01, 0x17);	/* right 0dB */

	/* select line in, mute the mic input, disable the DAC */
	rb->wmcodec_write(0x04, 0x02);
}


uint32_t get_time_secs(void) {
	/* returns the current time in seconds since 1900 */
	struct tm *ct;
	uint32_t secs;
	ct = rb->get_time();
	secs = ct->tm_sec + ct->tm_min*60 + ct->tm_hour*3600 + ct->tm_yday*86400 + ct->tm_year*31536000;
	return secs;
}

void create_filename(char *buffer) {
	/* since the plugin API doesn't have the datetime_filename function we will have to write one */
	struct tm *ct;
	ct = rb->get_time();
	rb->snprintf(buffer, MAX_PATH, "/%04d%02d%02d%02d%02d.wav", ct->tm_year+1900, ct->tm_mon+1, ct->tm_mday, ct->tm_hour, ct->tm_min);
}

void open_wave_file(void) {
	/* open a wav file and write the header */
	noise_data.fp = rb->open(noise_data.filename, O_RDWR|O_CREAT|O_TRUNC);
	rb->write(noise_data.fp, &riff_header, sizeof(riff_header));
	noise_data.pcm_samples = 0;
}

void close_wave_file(void) {
	/* re-write the header with the corrent lengths and close the file */
	struct riff_header hdr;
	uint32_t data_size;
	
	rb->memcpy(&hdr, &riff_header, sizeof(riff_header));
	data_size = noise_data.pcm_samples * noise_data.channels * PCM_DEPTH_BYTES;
	
	hdr.riff_size    = htole32(RIFF_FMT_HEADER_SIZE + RIFF_FMT_DATA_SIZE
                               + RIFF_DATA_HEADER_SIZE + data_size);

    /* format data */
    hdr.num_channels = htole16(noise_data.channels);
    hdr.sample_rate  = htole32(file_sample_rate[noise_data.sample_rate]);
    hdr.byte_rate    = htole32(noise_data.sample_rate*noise_data.channels* PCM_DEPTH_BYTES);
    hdr.block_align  = htole16(noise_data.channels*PCM_DEPTH_BYTES);

    /* data header */
    hdr.data_size    = htole32(data_size);
	rb->lseek(noise_data.fp, 0, SEEK_SET);
	rb->write(noise_data.fp, &hdr, sizeof(hdr));
	rb->close(noise_data.fp);
}

/* this is the callback that handles the file writing */
static int pcm_record_callback(int status) {
	if(status < 0) {
		if(status == DMA_REC_ERROR_DMA)
			rb->splash(HZ*2, "DMA error");
	}

	rb->write(noise_data.fp, rec_buffer, PCM_CHUNK_SIZE);
			
	/* reset the buffer and go around again */
	pcm_record_more(rec_buffer, PCM_CHUNK_SIZE);
		
	return 0;
}

bool start_record(void) {
	bool exit = false;
	uint32_t time_passed = 0;
	uint32_t current_time, next_time, start_time;
	bool recording = false;
	uint32_t total_start_time;
	long button;
	char buf[10];

	/* check to see if we haven't picked some stupid settings */
	if(duration[noise_data.duration] >= interval[noise_data.interval]) {
		rb->splash(HZ*2, "duration too long or interval too short");
		return false;
	}
	
	/* clear the screen so we can display the time information */
	rb->lcd_clear_display();
	rb->lcd_puts(1, 1, "Noise recorder");
	rb->lcd_puts(1, 2, "Press select to exit");	
	rb->lcd_puts(1, 4, "Time till next record");	
	rb->lcd_update();
	
	
	/* Configure the codec */
	/* disable the codec to make the changes */
	codec_enable(0);
	codec_config();
	setup_IIS();
	
	rb->fdprintf(debug_fp, "done codec config\n");
	
	/* start the recording */
	pcm_record_init();

	/* set up the inital times */
	current_time = get_time_secs();
	total_start_time = current_time;
	start_time = 0;
	next_time = current_time + 10;	/* test only */
		
	while(!exit && (time_passed < runtime[noise_data.runtime])) {
		/* this is the main loop */
		current_time = get_time_secs();
		if(noise_data.runtime != 0)
			time_passed = current_time - total_start_time;
		
		rb->snprintf(buf, sizeof(buf), "%d", (int)(next_time - current_time));
		rb->lcd_puts(1, 5, buf);
		
		if(current_time >= next_time) {
			/* start a new recording */
			rb->lcd_puts(1, 7, "Recording");
			start_time = current_time;
			next_time = start_time + interval[noise_data.interval];	/* the next start time */
			recording = true;
			create_filename(noise_data.filename);
			open_wave_file();
			pcm_record_start(rec_buffer, PCM_CHUNK_SIZE);
			codec_enable(1);
			rb->fdprintf(debug_fp, "start recording\n");
		}
		if(recording) {
			rb->snprintf(buf, sizeof(buf), "%d", (int)(start_time + duration[noise_data.duration] - current_time));
			rb->lcd_puts(1, 8, buf);
			rb->lcd_puts(10, 8, "sec to go");
			if(current_time >= start_time + duration[noise_data.duration]) {
				/* stop the recording */
				codec_enable(0);
				pcm_record_stop();
				recording = false;
				close_wave_file();
				rb->lcd_puts(1, 7, "             ");
				rb->lcd_puts(1, 8, "             ");
				rb->logf("stop recording");
				rb->fdprintf(debug_fp, "stop recording\n");
			}
		}
		/* check the key pad to see if we want to exit */
		button = rb->button_get_w_tmo(HZ);
		if(button == BUTTON_RIGHT)
			exit = true;
			
		/* update the display */
		rb->lcd_update();
	}
	/* just in case we have exited during a record */		
	if(recording) {
		pcm_record_stop();
		close_wave_file();
	}
	return true;	
}	

	 
/* main plugin entry point */
enum plugin_status plugin_start(const struct plugin_api *api, const void *parameter) {
	bool menu_exit = false;
	int selection;
	char buf[10];
	
	(void)parameter;
	rb = api;
	
	/* lets get us some memory */
	rec_buffer = rb->plugin_get_audio_buffer(&rec_buffer_size);
	if(rec_buffer_size < 1024*1024) {
		rb->splash(HZ*2, "Not enough memory");
		return PLUGIN_ERROR;
	}
	
	/* open the debug file */
	debug_fp = rb->open("/debug.txt", O_RDWR|O_CREAT|O_APPEND);
	rb->fdprintf(debug_fp, "Starting\n");
	
	/* set the defaults */
	noise_data.interval = 3; 	/* 5 minutes */
	noise_data.duration = 2; 	/* 20 seconds */
	noise_data.runtime = 4;		/* 24 hours */
	noise_data.sample_rate = 2;	/* 44.1 kHz */
	noise_data.sample_size = 0;	/* 16 bit */
	noise_data.channels = 2;
	
	MENUITEM_STRINGLIST(menu, "Noise logger", NULL, "Interval", "Duration", "Time to run", "Sample rate", "Sample size", "Start", "Quit");
	while(!menu_exit) {
		switch(rb->do_menu(&menu, &selection, NULL, false)) {
			case 0:		/* Interval */
				rb->set_option("Time between recordings", &noise_data.interval, INT, interval_menu, 6, NULL);
				rb->snprintf(buf, 10, "int %d", noise_data.interval);
				rb->logf(buf);
				break;
			case 1:		/* Duration */
				rb->set_option("Length of recording", &noise_data.duration, INT, duration_menu, 6, NULL);
				break;
			case 2:		/* Time to run */
				rb->set_option("Time to run for", &noise_data.runtime, INT, runtime_menu, 6, NULL);
				break;
			case 3:		/* Sample rate */
				rb->set_option("Sample rate", &noise_data.sample_rate, INT, sample_rate_menu, 6, NULL);		
				break;
			case 4:		/* Sample size */
				rb->set_option("Sample size", &noise_data.sample_size, INT, sample_size_menu, 2, NULL);		
				break;
			case 5:		/* Start */
				/* set the sample rate */
				start_record();
				break;
			case 6:		/* Quit */
				menu_exit = true;
				break;
		}
	}
	rb->close(debug_fp);
	return PLUGIN_OK;
}
				
