#include "config.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <curses.h>
#include <signal.h>
#include <inttypes.h>
#include <sys/time.h>
#include <sys/signal.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#ifdef HAVE_SOUNDCARD_H
# include <soundcard.h>
#endif
#ifdef HAVE_SYS_SOUNDCARD_H
# include <sys/soundcard.h>
#endif

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

static void
tty_raw(void)
{
    initscr();
    cbreak();
    noecho();
    keypad(stdscr,1);
    refresh();
}

static void
tty_restore(void)
{
    endwin();
}

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

static int           sound_fd;
static int           sound_rcount;
static unsigned int  sound_blksize;
static int16_t       *sound_buffer;
static int           maxl,maxr;
static int           secl,secr;
static int           *histl,*histr,histn,histi;
static float         peak_seconds = 1.5;
static char          *audio_dev = "/dev/dsp";

static int
sound_open(int rate)
{
    int frag,afmt,channels,trigger,srate;
    
    if (-1 == (sound_fd = open(audio_dev, O_RDONLY))) {
	fprintf(stderr,"open %s: %s\n",audio_dev,strerror(errno));
	exit(1);
    }

    frag = 0x7fff000d; /* 8k */
    if (-1 == ioctl(sound_fd, SNDCTL_DSP_SETFRAGMENT, &frag))
        perror("ioctl SNDCTL_DSP_SETFRAGMENT");

    /* format */
    afmt = AFMT_S16_LE;
    if (-1 == ioctl(sound_fd, SNDCTL_DSP_SETFMT, &afmt)) {
	perror("ioctl SNDCTL_DSP_SETFMT");
	exit(1);
    }
    if (afmt != AFMT_S16_LE) {
	fprintf(stderr,"can't set sound format to 16 bit (le)\n");
	exit(1);
    }

    /* channels */
    channels = 2;
    if (-1 == ioctl(sound_fd, SNDCTL_DSP_CHANNELS, &channels)) {
	perror("ioctl SNDCTL_DSP_CHANNELS");
	exit(1);
    }
    if (channels != 2) {
	fprintf(stderr,"can't record in stereo\n");
	exit(1);
    }

    /* rate */
    srate = rate;
    if (-1 == ioctl(sound_fd, SNDCTL_DSP_SPEED, &srate)) {
	perror("ioctl SNDCTL_DSP_SPEED");
	exit(1);
    }
    /* accept +/- 1% */
    if (srate < rate *  99 / 100 ||
	srate > rate * 101 / 100) {
	fprintf(stderr,"can't set sample rate to %d (got %d)\n",
		rate,srate);
	exit(1);
    }

    /* get block size */
    if (-1 == ioctl(sound_fd, SNDCTL_DSP_GETBLKSIZE,  &sound_blksize)) {
	perror("ioctl SNDCTL_DSP_GETBLKSIZE");
	exit(1);
    }
    if (0 == sound_blksize)
	sound_blksize = 4096;
    sound_buffer = malloc(sound_blksize);

    /* peak level history */
    histn = peak_seconds * rate * 4 / sound_blksize;
    histl = malloc(histn * sizeof(int));
    histr = malloc(histn * sizeof(int));
    memset(histl,0,histn * sizeof(int));
    memset(histr,0,histn * sizeof(int));

    /* trigger record */
    trigger = ~PCM_ENABLE_INPUT;
    ioctl(sound_fd,SNDCTL_DSP_SETTRIGGER,&trigger);
    trigger = PCM_ENABLE_INPUT;
    ioctl(sound_fd,SNDCTL_DSP_SETTRIGGER,&trigger);

    return sound_fd;
}

static int
sound_read(void)
{
    unsigned int have;
    int     i,rc;
    int16_t *v;

    /* read */
    for (have = 0;have < sound_blksize;) {
	rc = read(sound_fd,sound_buffer+have,sound_blksize-have);
	switch (rc) {
	case -1:
	    if (EINTR != errno) {
		perror("read sound");
		exit(1);
	    }
	    break;
	case 0:
	    fprintf(stderr,"Huh? got 0 bytes from sound device?\n");
	    exit(1);
	default:
	    have += rc;

	}
    }

    /* look for peaks */
    maxl = 0;
    maxr = 0;
    for (i = sound_blksize>>2, v=sound_buffer; i > 0; i--) {
	if (abs(*v) > maxl)
	    maxl = abs(*v);
	v++;
	if (abs(*v) > maxr)
	    maxr = abs(*v);
	v++;	
    }

    /* max for the last second */
    histl[histi] = maxl;
    histr[histi] = maxr;
    histi++;
    if (histn == histi)
	histi = 0;

    for (secl = 0, secr = 0, i = 0; i < histn; i++) {
	if (secl < histl[i])
	    secl = histl[i];
	if (secr < histr[i])
	    secr = histr[i];
    }
    sound_rcount++;
    return 0;
}

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

char *names[SOUND_MIXER_NRDEVICES] = SOUND_DEVICE_NAMES;
char *config_names[SOUND_MIXER_NRDEVICES][4];

static int  mix;
static int  dev = -1;
static int  volume;
static char *mixer_dev = "/dev/mixer";

static int
mixer_open(char *filename, char *device)
{
    int i, devmask;

    if (-1 == (mix = open(filename,O_RDONLY))) {
	fprintf(stderr,"open %s: %s\n",filename,strerror(errno));
	exit(1);
    }
    if (-1 == ioctl(mix,MIXER_READ(SOUND_MIXER_DEVMASK),&devmask)) {
	perror("mixer read devmask");
	exit(1);
    }
    for (i = 0; i < SOUND_MIXER_NRDEVICES; i++) {
	if ((1<<i) & devmask && strcasecmp(names[i],device) == 0) {
	    if (-1 == ioctl(mix,MIXER_READ(i),&volume)) {
		perror("mixer read volume");
		exit(1);
	    } else {
		dev = i;
	    }
	}
    }
    if (-1 == dev) {
	fprintf(stderr,"mixer: havn't found device '%s'\nmixer: available: ",device);
	for (i = 0; i < SOUND_MIXER_NRDEVICES; i++)
	    if ((1<<i) & devmask)
		fprintf(stderr," '%s'",names[i]);
	fprintf(stderr,"\n");
	exit(1);
    }
    return (-1 != dev) ? 0 : -1;
}

static void
mixer_close(void)
{
    close(mix);
    dev = -1;
}

static int
mixer_get_volume(void)
{
    return (-1 == dev) ? -1 : (volume & 0x7f);
}

static int
mixer_set_volume(int val)
{
    if (-1 == dev)
	return -1;
    val   &= 0x7f;
    volume = val | (val << 8);;
    if (-1 == ioctl(mix,MIXER_WRITE(dev),&volume)) {
	perror("mixer write volume");
	return -1;
    }
    return 0;
}

/* ---------------------------------------------------------------------- */
/* *.wav I/O stolen from cdda2wav */

/* Copyright (C) by Heiko Eissfeldt */

typedef uint8_t   BYTE;
typedef uint16_t  WORD;
typedef uint32_t  DWORD;
typedef uint32_t  FOURCC;	/* a four character code */

/* flags for 'wFormatTag' field of WAVEFORMAT */
#define WAVE_FORMAT_PCM 1

/* MMIO macros */
#define mmioFOURCC(ch0, ch1, ch2, ch3) \
  ((DWORD)(BYTE)(ch0) | ((DWORD)(BYTE)(ch1) << 8) | \
  ((DWORD)(BYTE)(ch2) << 16) | ((DWORD)(BYTE)(ch3) << 24))

#define FOURCC_RIFF	mmioFOURCC ('R', 'I', 'F', 'F')
#define FOURCC_LIST	mmioFOURCC ('L', 'I', 'S', 'T')
#define FOURCC_WAVE	mmioFOURCC ('W', 'A', 'V', 'E')
#define FOURCC_FMT	mmioFOURCC ('f', 'm', 't', ' ')
#define FOURCC_DATA	mmioFOURCC ('d', 'a', 't', 'a')

typedef struct CHUNKHDR {
    FOURCC ckid;		/* chunk ID */
    DWORD dwSize; 	        /* chunk size */
} CHUNKHDR;

/* simplified Header for standard WAV files */
typedef struct WAVEHDR {
    CHUNKHDR chkRiff;
    FOURCC fccWave;
    CHUNKHDR chkFmt;
    WORD wFormatTag;	   /* format type */
    WORD nChannels;	   /* number of channels (i.e. mono, stereo, etc.) */
    DWORD nSamplesPerSec;  /* sample rate */
    DWORD nAvgBytesPerSec; /* for buffer estimation */
    WORD nBlockAlign;	   /* block size of data */
    WORD wBitsPerSample;
    CHUNKHDR chkData;
} WAVEHDR;

#define IS_STD_WAV_HEADER(waveHdr) ( \
  waveHdr.chkRiff.ckid == FOURCC_RIFF && \
  waveHdr.fccWave == FOURCC_WAVE && \
  waveHdr.chkFmt.ckid == FOURCC_FMT && \
  waveHdr.chkData.ckid == FOURCC_DATA && \
  waveHdr.wFormatTag == WAVE_FORMAT_PCM)

#define cpu_to_le32(x) (x)
#define cpu_to_le16(x) (x)
#define le32_to_cpu(x) (x)
#define le16_to_cpu(x) (x)

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

static WAVEHDR  fileheader;
static size_t   wav_size;
static size_t   done_size;

static void
wav_init_header(int rate)
{
    /* stolen from cdda2wav */
    int nBitsPerSample = 16;
    int channels = 2;

    unsigned long nBlockAlign = channels * ((nBitsPerSample + 7) / 8);
    unsigned long nAvgBytesPerSec = nBlockAlign * rate;
    unsigned long temp = /* data length */ 0 +
	sizeof(WAVEHDR) - sizeof(CHUNKHDR);

    fileheader.chkRiff.ckid    = cpu_to_le32(FOURCC_RIFF);
    fileheader.fccWave         = cpu_to_le32(FOURCC_WAVE);
    fileheader.chkFmt.ckid     = cpu_to_le32(FOURCC_FMT);
    fileheader.chkFmt.dwSize   = cpu_to_le32(16);
    fileheader.wFormatTag      = cpu_to_le16(WAVE_FORMAT_PCM);
    fileheader.nChannels       = cpu_to_le16(channels);
    fileheader.nSamplesPerSec  = cpu_to_le32(rate);
    fileheader.nAvgBytesPerSec = cpu_to_le32(nAvgBytesPerSec);
    fileheader.nBlockAlign     = cpu_to_le16(nBlockAlign);
    fileheader.wBitsPerSample  = cpu_to_le16(nBitsPerSample);
    fileheader.chkData.ckid    = cpu_to_le32(FOURCC_DATA);
    fileheader.chkRiff.dwSize  = cpu_to_le32(temp);
    fileheader.chkData.dwSize  = cpu_to_le32(0 /* data length */);
}

static void
wav_start_write(int fd,int rate)
{
    wav_init_header(rate);
    lseek(fd,0,SEEK_SET);
    write(fd,&fileheader,sizeof(WAVEHDR));
    wav_size = 0;
}

static int
wav_write_audio(int fd, void *data, int len)
{
    int rc;

    rc = write(fd,data,len);
    if (len == rc) {
	wav_size += len;
	return 0;
    } else
	return -1;
}

static void
wav_stop_write(int fd)
{
    unsigned long temp = wav_size + sizeof(WAVEHDR) - sizeof(CHUNKHDR);

    fileheader.chkRiff.dwSize = cpu_to_le32(temp);
    fileheader.chkData.dwSize = cpu_to_le32(wav_size);
    lseek(fd,0,SEEK_SET);
    write(fd,&fileheader,sizeof(WAVEHDR));
    done_size += wav_size;
}

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

static char full[] =
"##################################################"
"##################################################"
"##################################################"
"##################################################";

static char empty[] =
"--------------------------------------------------"
"--------------------------------------------------"
"--------------------------------------------------"
"--------------------------------------------------";

static char blank[] =
"                                                  "
"                                                  "
"                                                  "
"                                                  ";

static char alive[] = "-\\|/";
//static char alive[] = ".oOo";
#define ALIVE(count)  alive[count % (sizeof(alive)/sizeof(alive[0])-1)]

static void
print_bar(int line, char *name, int val1, int val2, int max)
{
    int total,len;
    
    total = COLS-16;
    len   = val1*total/max;

    mvprintw(line,0,"%-6s: %5d  ",name,(val2 != -1) ? val2 : val1);
    printw("%*.*s",len,len,full);
    printw("%*.*s",total-len,total-len,empty);
    if (val2 != -1)
	mvprintw(line,14+val2*total/max,"|");
}

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

enum MODE {
    NCURSES = 1,
    CONSOLE = 2,
};
enum MODE mode = NCURSES;
int       stop,verbose;
char      *filename = "record";
int       rate = 44100;

static void
ctrlc(int signal)
{
    if (verbose)
	fprintf(stderr,"\n%s - exiting\n",
		sys_siglist[signal]);
    stop = 1;
}

static int
record_start(char *outfile, int *nr)
{
    int wav;
    
    do {
	sprintf(outfile,"%s%03d.wav",filename,(*nr)++);
	wav = open(outfile, O_WRONLY | O_EXCL | O_CREAT, 0666);
    } while ((-1 == wav) && (EEXIST == errno));
    if (-1 == wav) {
	perror("open");
	exit(1);
    }
    wav_start_write(wav,rate);
    return wav;
}

static void
record_stop(int fd)
{
    wav_stop_write(fd);
    close(fd);
    switch (mode) {
    case CONSOLE:
	if (verbose)
	    printf("\n");
	break;
    case NCURSES:
	mvprintw(3,0,"%*.*s",COLS-1,COLS-1,blank);
	break;
    }
}

static size_t
parse_size(const char *arg)
{
    int value;
    char mul[4];
    off_t retval = -1;

    if (2 != sscanf(arg,"%d%3s",&value,mul))
	return 0;
    if (0 == strcasecmp(mul,"g") ||
	0 == strcasecmp(mul,"gb"))
	retval = (off_t)value * 1024 * 1024 * 1024;
    if (0 == strcasecmp(mul,"m") ||
	0 == strcasecmp(mul,"mb"))
	retval = (off_t)value * 1024 * 1024;
    if (0 == strcasecmp(mul,"k") ||
	0 == strcasecmp(mul,"kb"))
	retval = (off_t)value * 1024;
    return retval;
}

static char*
str_mb(off_t value)
{
    static char buf[32];

    if (value > (1 << 30)) {
	value = (value * 10) >> 30;
	sprintf(buf,"%d.%d GB",(int)(value/10),(int)(value%10));
	return buf;
    }
    if (value > (1 << 20)) {
	value = (value * 10) >> 20;
	sprintf(buf,"%d.%d MB",(int)(value/10),(int)(value%10));
	return buf;
    }
    value >>= 10;
    sprintf(buf,"%3d kB",(int)value);
    return buf;
}

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

char      *progname;
char      *input = "line";
char      *str_maxsize = "2GB";
int       level_trigger;

static void
usage(FILE *fp)
{
    fprintf(fp,
	    "\n"
	    "%s records sound in CD-Quality (44100/16bit/stereo).\n"
	    "It has a nice ascii-art input-level meter.  It is a\n"
	    "interactive curses application.  You'll need a fast\n"
	    "terminal, don't try this on a 9600 bps vt100...\n"
	    "\n"
	    "%s has several options:\n"
	    "  -h        this text\n"
	    "  -o file   output file basename [%s], a number and the .wav\n"
	    "            extention are added by %s.\n"
	    "  -i ctrl   mixer control [%s].  This should be the one\n"
	    "            where you can adjust the record level for\n"
	    "            your audio source, \"line\", \"mic\" and \"igain\"\n"
	    "            are good candidates.\n"
	    "  -m dev    set mixer device [%s]\n"
	    "  -d dev    set dsp device   [%s]\n"
	    "  -r rate   set sample rate  [%d]\n"
	    "  -p sec    peak seconds     [%.1f]\n"
	    "\n"
	    "for non-interactive usage only:\n"
	    "  -c        enable console (non-interactive) mode\n"
	    "  -v        be verbose (show progress)\n"
	    "  -t mm:ss  limit the time to record.  By default it records\n"
	    "            until stopped by a signal (^C)\n"
	    "  -s size   set max file size [%s]. You have to give number\n"
	    "            and unit without space inbetween, i.e. \"100mb\".\n"
	    "  -n num    limit amount of files recorded, quits when\n"
	    "            reached.\n"
	    "  -l        signal level triggered recording.\n"
	    "  -L level  same as above + specify trigger level [%d]\n"
	    "\n",
	    progname,progname,filename,progname,
	    input,mixer_dev,audio_dev,
	    rate,peak_seconds,str_maxsize,
	    level_trigger ? level_trigger : 1000);
}

int
main(int argc, char *argv[])
{
    int             c,key,vol,delay,auto_adjust;
    int             record,nr,wav=0;
    char            *outfile;
    fd_set          s;
    int             sec,maxhour,maxmin,maxsec;
    int             maxfiles = 0;
    size_t          maxsize;

    /* init some vars */
    progname = strrchr(argv[0],'/');
    progname = progname ? progname+1 : argv[0];
    maxsec  = 0;
    delay   = 0;
    auto_adjust   = 1;
    record = 0;
    nr = 0;

    /* parse options */
    for (;;) {
	if (-1 == (c = getopt(argc, argv, "vhlci:o:d:m:r:t:s:L:p:n:")))
	    break;
	switch (c) {
	case 'v':
	    verbose = 1;
	    break;
	case 'l':
	    level_trigger = 1000;
	    break;
	case 'L':
	    level_trigger = atoi(optarg);
	    break;
	case 'i':
	    input = optarg;
	    break;
	case 'o':
	    filename = optarg;
	    break;
	case 'd':
	    audio_dev = optarg;
	    break;
	case 'm':
	    mixer_dev = optarg;
	    break;
	case 'c':
	    mode = CONSOLE;
	    break;
	case 'r':
	    rate = atoi(optarg);
	    break;
	case 'p':
	    peak_seconds = atof(optarg);
	    break;
	case 't':
	    if (3 != sscanf(optarg,"%d:%d:%d",&maxhour,&maxmin,&maxsec)) {
		maxhour = 0;
		if (2 != sscanf(optarg,"%d:%d",&maxmin,&maxsec)) {
		    fprintf(stderr,"time parse error\n");
		    exit(1);
		}
	    }
	    maxsec += maxmin  * 60;
	    maxsec += maxhour * 60 * 60;
	    break;
	case 's':
	    str_maxsize = optarg;
	    break;
	case 'n':
	    maxfiles = atoi(optarg);
	    break;
	case 'h':
	    usage(stdout);
	    exit(0);
	default:
	    usage(stderr);
	    exit(1);
	}
    }
    maxsize = parse_size(str_maxsize);
    if (0 == maxsize) {
	fprintf(stderr,"maxsize parse error [%s]\n",str_maxsize);
	exit(1);
    }

    mixer_open(mixer_dev,input);
    sound_open(rate);
    outfile = malloc(strlen(filename)+16);

    if (mode == NCURSES) {
	tty_raw();
	atexit(tty_restore);
    }

    signal(SIGINT,ctrlc);
    signal(SIGQUIT,ctrlc);
    signal(SIGTERM,ctrlc);
    signal(SIGHUP,ctrlc);

    if (mode == NCURSES) {
	mvprintw( 5,0,"record to   %s*.wav",filename);
	mvprintw( 7,0,"left/right  adjust mixer level for \"%s\"",input);
	mvprintw( 8,0,"space       starts/stops recording");
	/* line 9 is printed later */
	mvprintw(10,0,"            auto-adjust reduces the record level on overruns");
	mvprintw(11,0,"'N'         next file (same as space twice, but without break)");
	mvprintw(12,0,"'Q'         quit");
	mvprintw(LINES-3,0,"--");
	mvprintw(LINES-2,0,"(c) 1999-2003 Gerd Knorr <kraxel@bytesex.org>");
	
	for (;!stop;) {
	    refresh();
	    FD_ZERO(&s);
	    FD_SET(0,&s);
	    FD_SET(sound_fd,&s);
	    if (-1 == select(sound_fd+1,&s,NULL,NULL,NULL)) {
		if (EINTR == errno)
		    continue;
		perror("select");
		break;
	    }
	    
	    if (FD_ISSET(sound_fd,&s)) {
		/* sound */
		if (-1 == sound_read())
		    break;
		if (delay)
		    delay--;
		if (auto_adjust && (0 == delay) &&
		    (maxl >= 32767 || maxr >= 32767)) {
		    /* auto-adjust */
		    vol = mixer_get_volume();
		    vol--;
		    if (vol < 0)
			vol = 0;
		    mixer_set_volume(vol);
		    delay = 3;
		}
		print_bar(0,input,mixer_get_volume(),-1,100);
		print_bar(1,"left",maxl,secl,32768);
		print_bar(2,"right",maxr,secr,32768);
		mvprintw(9,0,"'A'         toggle auto-adjust [%s] ",
			 auto_adjust ? "on" : "off");
		if (record) {
		    wav_write_audio(wav,sound_buffer,sound_blksize);
		    sec = wav_size / (rate*4);
		    mvprintw(3,0,"%s: %3d:%02d (%s) ",outfile,
			     sec/60,sec%60,str_mb(wav_size));
		} else {
		    mvprintw(3,0,"%c",ALIVE(sound_rcount));
		}
	    }
	    
	    if (FD_ISSET(0,&s)) {
		/* tty in */
		switch (key = getch()) {
		case 'Q':
		case 'q':
		    stop = 1;
		    break;
		case 'A':
		case 'a':
		    auto_adjust = !auto_adjust;
		    break;
		case 'N':
		case 'n':
		    if (record) {
			record_stop(wav);
			wav = record_start(outfile,&nr);
		    }
		    break;
		case ' ':
		    if (!filename)
			break;
		    if (!record) {
			/* start */
			wav = record_start(outfile,&nr);
			record=1;
			auto_adjust=0;
		    } else {
			/* stop */
			record_stop(wav);
			record=0;
		    }
		    break;
		case KEY_RIGHT:
		    vol = mixer_get_volume();
		    vol++;
		    if (vol > 100)
			vol = 100;
		    mixer_set_volume(vol);
		    break;
		case KEY_LEFT:
		    vol = mixer_get_volume();
		    vol--;
		    if (vol < 0)
			vol = 0;
		    mixer_set_volume(vol);
		    break;
		}
	    }
	}
    }

    if (mode == CONSOLE) {
	if (!level_trigger) {
	    wav = record_start(outfile,&nr);
	    record=1;
	}

	for (;!stop;) {
	    if (-1 == sound_read())
		break;
	    if (level_trigger) {
		if (!record &&
		    (maxl > level_trigger ||
		     maxr > level_trigger)) {
		    wav = record_start(outfile,&nr);
		    record=1;
		}
		if (record &&
		    secl < level_trigger &&
		    secr < level_trigger) {
		    record_stop(wav);
		    record=0;
		    if (maxfiles && nr == maxfiles)
			break;
		}
	    }
	    if (!record) {
		printf("waiting for signal %c [%d/%d]...  \r",
		       ALIVE(sound_rcount), maxl,maxr);
		fflush(stdout);
		continue;
	    }

            sec = (done_size + wav_size) / (rate*4);
	    if (maxsec && sec >= maxsec)
		break;
	    if (wav_size + sound_blksize + sizeof(WAVEHDR) > maxsize) {
		record_stop(wav);
		wav = record_start(outfile,&nr);
	    }
            wav_write_audio(wav,sound_buffer,sound_blksize);
	    if (verbose) {
		int total = 10;
		int len   = (maxl+maxr)*total/32768/2;
		printf("|%*.*s%*.*s|  %s  %d:%02d",
		       len,len,full, total-len,total-len,empty,
		       outfile,sec/60,sec%60);
		if (maxsec)
		    printf("/%d:%02d",maxsec/60,maxsec%60);
		printf(" (%s",str_mb(wav_size));
		if (done_size)
		    printf(", %s total",str_mb(done_size + wav_size));
		printf(")      \r");
		fflush(stdout);
	    }
	}
    }
    
    if (record)
	record_stop(wav);
    mixer_close();
    exit(0);
}
