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

             TUNES is an Ugly (Nevertheless, Expedient) "Studio"
             (and it's not vaporware, unlike the other TUNES :-)

                 A simple sound recording & editing program

        Copyright 2002-2003  Tom Novelli       http://tom.bespin.org


NO WARRANTY. USE AT YOUR OWN RISK. This is free software. You may do
whatever you want with this code as long as you give credit where it's due.

This is hacker-ware. Customize it to your liking, and be thankful that it's
not obfuscated with luser-friendly misfeatures :)


To compile:

    Edit the "configuration" section a few pages down, then:

       gcc tunes.c -o tunes -lvga -lvgagl


Requirements:

    I use a 150mhz Pentium laptop with 80M RAM, running Linux (2.2 kernel
    and libc5) with SVGALIB 1.4.3 for graphics. With some modification it'll
    run on just about anything.

    An MP3 or OGG encoder is optional; I use LAME (www.sulaco.org/lame) for
    encoding and Ximp3 for playback.


User's Guide:

   Press (R)ecord, grab your instrument and start playing. Hit any key to
stop. You see some wavy lines: waveform on top, amplitude on the bottom. The
vertical lines are your trim points and the horizontal line is the trim
threshold.
   Now let's get rid of that "dead time" where you were picking up your
instrument. (T)rim samples the background noise threshold and sets trim
points where the music drops below that level. If you want a higher
threshold, just say "sssshhhhhhhh" while you hit (T)rim again :)
   For more precise editing, select your trim points usings the left & right
mouse buttons. You can (Z)oom your selection to full screen, (P)lay it, or
(S)ave it.
   Filenames are only 1 letter, to cut down on keystrokes. A '*' next to the
filename indicates that it has been modified. Files are raw 44100hz 16-bit
signed PCM.


Changes:

    REV   DATE      CHANGES
     -   9/2/02     Initial release.
     A   9/4/03     Inline asm and non-asm scale(). Mouse support. Better
                    prompts. Sensible load/save behavior. More accurate
                    waveform display. Redraws only when necessary.


Bugs: None known, but I've fixed plenty in the past. There may be more.

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

#include <stdio.h>
#include <fcntl.h>
#include <vga.h>
#include <vgagl.h>
#include <vgamouse.h>
#include <sys/soundcard.h>

#define min(x,y) ((x<y)? x:y)
#define max(x,y) ((x>y)? x:y)
#define xchg(a,b) { int c=a; a=b; b=c; }

#define gl_vline(a,b,c,d) gl_line(a,b,a,c,d);

/*********************************************************************/
/*                           CONFIGURATION                           */
/*********************************************************************/
#define ASM  // Use 80386 assembler for integer scale()

#define VGAMODE G800x600x256
#define width 800
#define height 600
#define xmax (width-1)
#define ymax (height-1)
#define bg 0   // Background color
#define fg 15  // Text color (other colors are hardcoded)

#define ENCODE "lame -x -r -m m -s44.1 -b32 "

#define TIME 300  // Maximum recording time (seconds)

/****** You probably don't need to change anything below here ********/

#define RATE 44100    // Sample rate
#define QUEUE 1024    // samples to read/write at a time (4096 works ok)
#define DSPBUF 0x8000 // actual kernel DSP buffer size  0x8000 = 32k samples (64kB)
                      // (not critical, only used for display purposes)

#define SCALE 256       // Used in draw() etc..
#define INSTEP (RATE/5) // Ignore first & last .2s
#define DECAY (RATE/2)  // Leave .5s for attack/decay below threshold

short buf[TIME*RATE];
int count=0;
int zl=0, zr=0; // zoom points (left/right)
int tl=0, tr=0; // trim points (left/right)
int thres=3000; // Volume threshold for trim()

#define sx(s) (scale(s-zl,xmax,zr-zl)) // Convert sample# to x-coord
#define xs(x) (zl+scale(x,zr-zl,xmax)) // Convert x-coord to sample#

#define xc width/2  // Center of screen
#define yc height/2

char fn[2]={0};  // Filename
int modified=0;  // 1 if modified since last save

int dsp;  // DSP device's file descriptor
int bits=16, stereo=0, speed=44100;

void *font;
char *vbuf;
GraphicsContext *screen, *wave;

/*********************************************************************/
/* From SVGALIB gl/line.c */

#ifdef ASM

static inline int scale(int m1, int m2, int d)
{
/* int32 * int32 -> int64 / int32 -> int32 */
    int result;
    int dummy;
    __asm__(
	       "imull %%edx\n\t"
	       "idivl %4\n\t"
  :	       "=a"(result), "=d"(dummy)	/* out */
  :	       "0"(m1), "1"(m2), "g"(d)		/* in */
	       /***rjr***:	       "ax", "dx" ***/	/* mod */
	);
    return result;
}

#else

static inline int scale(int m1, int m2, int d)
{
    return (float) m1 * (float) m2 / ((float) d);
}

#endif

/*********************************************************************/
/*                               START                               */
/*********************************************************************/
main() {
  vga_init();
  vga_setmousesupport(1);
  vga_setmode(VGAMODE);
  gl_setcontextvga(VGAMODE);

  screen = gl_allocatecontext();
  gl_getcontext(screen);

  font = (void*) malloc(256 * 8 * 8);
  gl_expandfont(8, 8, fg, gl_font8x8, font);
  gl_setfont(8, 8, font);
  gl_setwritemode(WRITEMODE_MASKED);

  wave = gl_allocatecontext();
  vbuf = (char*) malloc(width * (height-200));
  gl_setcontextvirtual(width, height-200, 1, 8, vbuf);
  gl_getcontext(wave);
  
  gl_enableclipping();

  menu();

  vga_setmode(TEXT);
  exit(0);
}
/*********************************************************************/
/*                                MENU                               */
/*********************************************************************/
menu() {
  char c;
  int event, button;
  int mx=0;	// mouse X coordinate
  int redraw=1;

  for (;;) {

    if (redraw) {
      redraw=0;
      gl_setcontext(screen);
      gl_clearscreen(bg);
      gl_copyboxfromcontext(wave, 0,0, width,height-200, 0,200);

      if (count) {
        gl_vline( sx(tl), 200, ymax, 4);  // trim points
        gl_vline( sx(tr), 200, ymax, 5);
      }
      gl_hline(0, ymax-thres/SCALE, xmax, 14);	// amplitude threshold for autotrim()

      gl_printf (500,0, "(L)oad  (S)ave (A)s  (Q)uit");
      gl_printf (500,12, "(E)ncode MP3");
      gl_printf (500,24, "(R)ecord  (P)lay (SPACE BAR)");
      gl_printf (500,36, "(T)rim  (Z)oom  (U)nzoom");
      gl_printf (0,00, "Filename: %s %s", fn, modified?"*":"" );
      gl_printf (0,10, "Total: %d samples (%.4fs)", count, (double) count/speed );
      gl_printf (0,20, "Zoom:  %d samples (%.4fs)", zr-zl, (double) (zr-zl)/speed );
      gl_printf (0,30, "Trim:  %d samples (%.4fs)", tr-tl, (double) (tr-tl)/speed );
      gl_printf (0,40, "Trim points:  %d  %d  (%d threshold)", tl, tr, thres );
    }

    event=vga_waitevent(VGA_MOUSEEVENT|VGA_KEYEVENT,0,0,0,0);

    if (event & VGA_MOUSEEVENT && count) {
      int t;
      gl_setcontext(screen);

      // Erase old marker
      if(mx!=sx(tl) && mx!=sx(tr))
        gl_copyboxfromcontext(wave, mx,0, 1,400, mx,200);

      // Get current position      
      mx = mouse_getx();
      if(mx!=sx(tl) && mx!=sx(tr))
        gl_vline( mx, 200, ymax, 2);
      button = mouse_getbutton();
      t=xs(mx+1);
      if(button & MOUSE_LEFTBUTTON && tl!=t)  {tl=t; redraw=1;}
      if(button & MOUSE_RIGHTBUTTON && tr!=t) {tr=t; redraw=1;}
    }

    if (event & VGA_KEYEVENT) {
      redraw=1;
      c = vga_getch();
      switch (tolower(c)) {
        case 'l': load(); break;
        case 'a': saveas(); break;
        case 's': save(); break;
        case 'r': rec(); break;
        case 32:
        case 'p': play(); break;
        case 't': trim(); break;
        case 'c': crop(); break;
        case 'z': zl=min(tl,tr); zr=max(tl,tr);   // Zoom
                  if(zl==zr) zr++;  draw(); break;
        case 'u': zl=0; zr=count; draw(); break;  // Unzoom
        case 'e': encode(); break;
        case 'q': return;
		case '?': {        // Show colors
		  int i; for (i=0; i<16; i++) gl_vline(400+i*8,60,160,i);
		  vga_getch();
		  break; }
        default: c=0; redraw=0;
      }
    }
    
  }
}
/*********************************************************************/
/* Draw the waveform & amplitude graph
 */
draw() {
  int x,y,ox,oy,m;
  #define CTR 120   // Centerline of waveform

  if (!count) return;

  msg("Please Wait");
  gl_setcontext(wave);
  gl_clearscreen(bg);
  ox=0;
  oy=CTR + buf[zl]/SCALE;
  if(oy<0) oy=0; else if(oy>2*CTR) oy=2*CTR;

  for (x=1; x<width; x++) {
    int s= xs(x); // sample no.
    
    y= CTR + buf[s]/SCALE;                    // Waveform
    if(y<0) y=0; else if(y>2*CTR) y=2*CTR;
    if(y!=oy) {
      gl_line(ox,oy, x,y, 1);
      ox=x; oy=y;
    }
    
    m= amax(s-(zr-zl)/width, s) / SCALE;  // Amplitude
    gl_vline(x, ymax-200-m, ymax-200, 3);
  }
  gl_hline(0,CTR,xmax,6); // Waveform centerline
}
/*********************************************************************/
/* Popup message
 */
msg(char *s) {
  int w=strlen(s)*8;
  gl_setcontext(screen);
  gl_fillbox(xc-w/2-10, yc-14, w+20, 30, fg);
  gl_fillbox(xc-w/2-8 , yc-12, w+16, 26, bg);
  gl_write(xc-w/2, yc-4, s);
}
/*********************************************************************/
/*                              FILE I/O                             */
/*********************************************************************/
load() {
  int c, input, n;
  
  msg("File to load (1 char, ESC to cancel):   ");
  c = vga_getch();
  if (c==27) return;
  fn[0]=c;
  if ((input = open(fn, O_RDONLY)) == -1)
    { msg("LOAD ERROR!"); vga_getch(); return; }
  count=0;
  do {
    n = read(input, buf+count, QUEUE);
    count += n>>1;
  } while (n);
  close(input);
  zl=tl=0; zr=tr=count;
  modified=0;
  gl_clearscreen(bg);
  draw();
}
/*********************************************************************/
save() {
  if (fn[0])
    do_save();
  else
    saveas();
}
/*********************************************************************/
saveas() {
  int c;
  msg("Save filename (1 char, ESC to cancel):   ");
  c = vga_getch();
  if (c==27) return;
  fn[0]=c;
  do_save();
}
/*********************************************************************/
do_save() {
  int output, i, n;
  if ((output = creat(fn, 0644)) == -1)
    { msg("SAVE ERROR! Unable to create file!"); vga_getch(); return; }
  if(tl>tr) xchg(tl,tr);
  for (i=tl; i<tr; i+=n>>1)
    if ((n = write (output, buf+i, min(QUEUE,(count-i)<<1) )) < 0)
      { msg("SAVE ERROR!"); vga_getch(); return; }
  close(output);
  modified=0;
  msg("Saved"); vga_getch();
}
/*********************************************************************/
encode() {
  char cmd[256]= ENCODE;
  if(!fn[0]) saveas();
  if(modified) {
    msg("Not saved. Encode anyway?");
    if(tolower(vga_getch()) != 'y') return;
  }
  strcat(cmd, fn);
  vga_setmode(TEXT);
  system(cmd);
  vga_setmode(VGAMODE);
}
/*********************************************************************/
/*                             SOUND I/O                             */
/*********************************************************************/
rec() {
  dsp = open("/dev/dsp", O_RDONLY);
  if (dsp == -1)                                        exit(1);
  if (ioctl (dsp, SNDCTL_DSP_SYNC, NULL) < 0)           exit(2);
  if (ioctl (dsp, SNDCTL_DSP_SAMPLESIZE, &bits) < 0)    exit(3);
  if (ioctl (dsp, SNDCTL_DSP_STEREO, &stereo) < 0)      exit(3);
  if (ioctl (dsp, SNDCTL_DSP_SPEED, &speed) < 0)        exit(3);

  read(dsp, buf, QUEUE*2);      // Click prevention

  gl_clearscreen(bg);
  msg("Recording... hit any key when done.");
  count=0;
  while ( (!vga_getkey()) && (count+QUEUE < TIME*RATE) ) {
    gl_fillbox (xc-20,yc-50,40,8,bg);
    gl_printf (xc-20,yc-50, "%02d:%02d", count/speed/60, count/speed%60 );
    if (ioctl (dsp, SNDCTL_DSP_SYNC, NULL) < 0)         exit(5);
    count += read(dsp, buf+count, QUEUE*2) / 2;
  }
  close(dsp);
  zl=tl=0; zr=tr=count;
  fn[0]=0;
  modified=1;
  draw();
}
/*********************************************************************/
play() {
  int i, c;
  int s,os=-1;  // Seconds display
  int x, ox=0;  // X-position
  
  dsp = open("/dev/dsp", O_WRONLY);
  if (dsp == -1)                                        exit(1);
  if (ioctl(dsp, SNDCTL_DSP_SYNC, 0) < 0)               exit(2);
  if (ioctl(dsp, SNDCTL_DSP_SAMPLESIZE, &bits) < 0)     exit(3);
  if (ioctl(dsp, SNDCTL_DSP_STEREO, &stereo) < 0)       exit(3);
  if (ioctl(dsp, SNDCTL_DSP_SPEED, &speed) < 0)         exit(3);

  gl_printf (10,100, "Playing... Space=stop  Z=rewind  X=forward.");
  if(tl>tr) xchg(tl,tr);
  for (i=tl; i<tr; i+=QUEUE) {
    s=(i-DSPBUF)/speed;
    if (s!=os) {
      gl_fillbox (10,110,100,8,bg);
      gl_printf (10,110, "%02d:%02d", s/60, s%60 );
      os=s;
    }
    x=sx(i-DSPBUF);
    if(x!=ox && x>0 && x<width) {
      gl_copyboxfromcontext(wave, ox,0, 1,400, ox,200);
      gl_vline( x, 200, ymax, 2);
      ox=x;
    }
    if ((write(dsp, buf+i, QUEUE*2)) < 0)               exit(4);
    c=tolower(vga_getkey());
    if (c==32) break;
    else if (c=='x') i+=RATE*10;
    else if (c=='z') { i-=RATE*10; if (i<0) i=0; }
  }

  close(dsp);
}
/*********************************************************************/
/*                          SOUND PROCESSING                         */
/*********************************************************************/
trim() {
  short *p, *pmax=buf+count;
  autothres();
  for (p=buf+INSTEP; p<pmax; p++)
    if ( abs(*p) > thres )
      { tl = max(p-buf-DECAY,0); break; }
  if (p==pmax) tl=0;
  for (p=buf+count-INSTEP; p>buf; --p)
    if ( abs(*p) > thres )
      { tr = min(p-buf+DECAY,count-1); return; }
  tr=count;
}
/*********************************************************************/
// Maximum amplitude over a range
amax(a, b) {
  int tmp, x=0;
  short *p=buf+a, *pmax=buf+b;
  for (; p<=pmax; p++) {
    tmp = abs(*p);
    if (tmp > x) x=tmp;
  }
  return x;
}
/*********************************************************************/
autothres() {
  short buf[QUEUE/2], *p, x=0;
  
  dsp = open("/dev/dsp", O_RDONLY);
  if (dsp == -1)                                        exit(1);
  if (ioctl (dsp, SNDCTL_DSP_SYNC, 0) < 0)              exit(2);
  if (ioctl (dsp, SNDCTL_DSP_SAMPLESIZE, &bits) < 0)    exit(3);
  if (ioctl (dsp, SNDCTL_DSP_STEREO, &stereo) < 0)      exit(3);
  if (ioctl (dsp, SNDCTL_DSP_SPEED, &speed) < 0)        exit(3);
  read(dsp, buf, QUEUE);                // Click -- discard
  read(dsp, buf, QUEUE);                // This one counts
  close(dsp);
  for (p=buf; p<buf+QUEUE/2; p++)
    if (abs(*p) > x) x=abs(*p);
  thres = x*2;
}
/*********************************************************************/
crop() {
  short *p,*q;
  if(tl>tr) xchg(tl,tr);
  p=buf; q=buf+tl;  // Move data from q to p
  count=tr-tl;
  for(; p<buf+count; p++, q++)
    *p=*q;
  zl=tl=0; zr=tr=count;
  modified=1;
  draw();
}
/*********************************************************************/
