/* gretl - The Gnu Regression, Econometrics and Time-series Library * Copyright (C) 2004 Allin Cottrell * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this software; if not, write to the * Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ /* gretl audio graph plugin */ #include #include #include #include #include "libgretl.h" #include "miditypes.h" #include "midi_utils.h" #include #include #define ADEBUG 0 #ifdef HAVE_FLITE # include extern cst_voice *register_cmu_us_kal (void); #endif #ifdef G_OS_WIN32 # define COBJMACROS # include #endif #if defined(HAVE_FLITE) || defined(G_OS_WIN32) # define DO_SPEECH # include "libgretl.h" #endif const char *track_hdr = "MTrk"; enum dataset_comments { TS_COMMENT = 0, YLABEL_COMMENT, XLABEL_COMMENT, XRANGE_COMMENT, N_COMMENTS }; #define DEFAULT_FORCE 96 /* volume of MIDI notes */ #ifndef G_OS_WIN32 # define DEFAULT_MIDI_PROG "timidity" /* MIDI player to be used */ # define DEFAULT_MIDI_OPT "-A500" /* option to pass to MIDI player */ #endif typedef struct _datapoint datapoint; typedef struct _dataset dataset; typedef struct _midi_spec midi_spec; typedef struct _midi_track midi_track; typedef struct _note_event note_event; struct _datapoint { double x; double y; }; struct _dataset { int pd; int n; int series2; double intercept; double slope; datapoint *points; double *y2; gchar *comments[N_COMMENTS]; }; struct _midi_spec { int ntracks; int nticks; int nsecs; dataset *dset; FILE *fp; }; struct _midi_track { unsigned char channel; unsigned char patch; int n_notes; note_event *notes; }; struct _note_event { double dtime; double duration; unsigned char pitch; unsigned char force; unsigned char channel; }; #define delta_time_zero(f) (putc(0x00, f)) #define YMAX 60 static int write_note_event (note_event *event, midi_spec *spec) { int len; len = delta_time(event->dtime, spec->nticks, spec->fp); putc(MIDI_NOTE_ON + event->channel, spec->fp); putc(event->pitch, spec->fp); putc(event->force, spec->fp); len += 3; /* use "running status" */ len += delta_time(event->duration, spec->nticks, spec->fp); putc(event->pitch, spec->fp); putc(0, spec->fp); len += 2; return len; } static void write_midi_track (midi_track *track, midi_spec *spec) { unsigned char bal = 0x7f * (track->channel % 2); char tmp[32]; long len = 0; int i, n; fwrite(track_hdr, 1, 4, spec->fp); write_be_long(0, spec->fp); /* revisited below */ delta_time_zero(spec->fp); len++; sprintf(tmp, "Series %d", track->channel + 1); n = strlen(tmp); len += write_midi_meta(MIDI_TRACK_NAME, spec->fp); len += write_var_len(n, spec->fp); for (i=0; ifp); } len += n; delta_time_zero(spec->fp); len++; strcpy(tmp, get_patch_name(track->patch)); n = strlen(tmp); len += write_midi_meta(MIDI_INSTR_NAME, spec->fp); len += write_var_len(n, spec->fp); for (i=0; ifp); } len += n; /* Stereo separation of series */ delta_time_zero(spec->fp); fputc(0xb0 + track->channel, spec->fp); /* Controller on channel */ fputc(MIDI_PAN_MSB, spec->fp); fputc(bal, spec->fp); /* MSB = 0 or 0x7f */ delta_time_zero(spec->fp); fputc(0xb0 + track->channel, spec->fp); fputc(MIDI_PAN_LSB, spec->fp); fputc(bal, spec->fp); /* LSB = 0 or 0x7f */ len += 8; delta_time_zero(spec->fp); len++; len += write_midi_byte(MIDI_PROGRAM_CHANGE + track->channel, spec->fp); len += write_midi_byte(track->patch, spec->fp); for (i=0; in_notes; i++) { track->notes[i].channel = track->channel; len += write_note_event(&track->notes[i], spec); } delta_time_zero(spec->fp); len++; len += write_midi_eot(spec->fp); fseek(spec->fp, -(len + 4), SEEK_CUR); /* now fix the length */ write_be_long(len, spec->fp); fseek(spec->fp, len, SEEK_CUR); } static void four_four_header (midi_spec *spec) { long min_note_time = 40000; long max_note_time = 180000; int ticklen, len = 0; long pos, pos1; write_midi_header(1, spec->ntracks + 1, spec->nticks, spec->fp); /* tempo/time track */ fwrite(track_hdr, 1, 4, spec->fp); pos = ftell(spec->fp); write_be_long(0, spec->fp); /* revisited below */ #if 0 /* time signature (4/4 is the MIDI default) */ delta_time_zero(spec->fp); len++; len += write_midi_meta(MIDI_TIME_SIG, spec->fp); /* size (bytes) */ len += write_midi_byte(4, spec->fp); /* 4/4 time */ len += write_midi_byte(4, spec->fp); len += write_midi_byte(2, spec->fp); /* 24 MIDI clocks per metronome click */ len += write_midi_byte(24, spec->fp); /* 8 32nd notes per tick */ len += write_midi_byte(8, spec->fp); #endif /* tempo */ delta_time_zero(spec->fp); len++; len += write_midi_meta(MIDI_TEMPO, spec->fp); /* bytes */ len += write_midi_byte(3, spec->fp); if (spec->dset == NULL) { /* microseconds per quarter note */ len += write_be_24(500000, spec->fp); } else { /* total number of microseconds */ double nms = spec->nsecs * 1.0e6; /* microseconds per quarter note */ long msq = nms / spec->dset->n; #if ADEBUG fprintf(stderr, "Secs = %d, microsecs = %g, msq = %d\n", spec->nsecs, nms, (int) msq); #endif if (msq > max_note_time) { msq = max_note_time; } else if (msq < min_note_time) { msq = min_note_time; } #if ADEBUG fprintf(stderr, "After adj, msq = %d\n", (int) msq); #endif len += write_be_24(msq, spec->fp); } /* end */ if (spec->dset == NULL) { ticklen = 4 * spec->nticks; #if ADEBUG fprintf(stderr, "dset is NULL, ticklen = %d\n", ticklen); #endif } else { ticklen = spec->dset->n * spec->nticks; #if ADEBUG fprintf(stderr, "ticklen = %d * %d = %d\n", spec->dset->n, spec->nticks, ticklen); #endif } len += write_var_len(ticklen, spec->fp); len += write_midi_eot(spec->fp); /* length of header */ pos1 = ftell(spec->fp); fseek(spec->fp, pos, SEEK_SET); write_be_long(len, spec->fp); fseek(spec->fp, pos1, SEEK_SET); } static void dataset_init (dataset *dset) { int i; dset->points = NULL; dset->y2 = NULL; dset->n = 0; dset->pd = 0; dset->series2 = 0; dset->intercept = NADBL; dset->slope = NADBL; for (i=0; icomments[i] = NULL; } } static void dataset_free (dataset *dset) { int i; free(dset->points); if (dset->y2 != NULL) { free(dset->y2); } for (i=0; icomments[i] != NULL) { g_free(dset->comments[i]); } } } const char *cent_str (int cent) { switch (cent) { case 14: return "fourteen"; case 15: return "fifteen"; case 16: return "sixteen"; case 17: return "seventeen"; case 18: return "eighteen"; case 19: return "nineteen"; case 20: return "two thousand"; default: return "unknown"; } } static int make_xrange_comment (const char *line, dataset *dset) { double x1, x2; if (sscanf(line, "set xrange [%lf:%lf]", &x1, &x2) != 2) { return 0; } if (dset->pd == 0) { dset->comments[XRANGE_COMMENT] = g_strdup_printf("x range %.4g to %.4g", x1, x2); } else { char tmp1[64], tmp2[64]; int ix1 = x1; int ix2 = x2; int cent, yr; cent = x1 / 100; yr = ix1 - 100 * cent; sprintf(tmp1, "%s %d", cent_str(cent), yr); cent = x2 / 100; yr = ix2 - 100 * cent; sprintf(tmp2, "%s %d", cent_str(cent), yr); dset->comments[XRANGE_COMMENT] = g_strdup_printf("%s to %s", tmp1, tmp2); } return 1; } static int make_axis_label_comment (const char *line, dataset *dset) { char tmp[16]; int ret = 0; if (sscanf(line, "set ylabel '%15[^']", tmp) == 1) { if (dset->pd > 0) { dset->comments[YLABEL_COMMENT] = g_strdup(tmp); } else { dset->comments[YLABEL_COMMENT] = g_strdup_printf("y variable %s", tmp); } ret = 1; } else if (sscanf(line, "set xlabel '%15[^']", tmp) == 1) { if (dset->pd > 0) { dset->comments[XLABEL_COMMENT] = g_strdup(tmp); } else { dset->comments[XLABEL_COMMENT] = g_strdup_printf("x variable %s", tmp); } ret = 1; } return ret; } static void make_ts_comment (const char *line, dataset *dset) { const char *pdstr = NULL; sscanf(line, "# timeseries %d", &dset->pd); switch (dset->pd) { case 1: pdstr = "Annual"; break; case 4: pdstr = "Quarterly"; break; case 5: case 7: pdstr = "Daily"; break; case 12: pdstr = "Monthly"; break; case 24: pdstr = "Hourly"; break; default: break; } if (pdstr != NULL) { dset->comments[TS_COMMENT] = g_strdup_printf("%s time series", pdstr); } else { dset->comments[TS_COMMENT] = g_strdup("time series"); } } static int get_comment (const char *line, dataset *dset) { int ret = 0; if (!strncmp(line, "# timeseries", 12)) { make_ts_comment(line, dset); ret = 1; } else if (!strncmp(line, "set ylabel", 10) || !strncmp(line, "set xlabel", 10)) { make_axis_label_comment(line, dset); ret = 1; } else if (!strncmp(line, "set xrange", 10)) { make_xrange_comment(line, dset); ret = 1; } return ret; } #ifdef PLAY_AUTOFIT_LINE static int get_fit_params (char *line, dataset *dset) { double a, b; char *p = strchr(line, '*'); if (p != NULL) *p = '\0'; if (sscanf(line, "%lf + %lf", &a, &b) == 2) { dset->intercept = a; dset->slope = b; dset->series2 = 1; return 1; } return 0; } #endif static int get_data_x_y (const char *line, double *x, double *y) { if (sscanf(line, "%lf %lf", x, y) == 2) return 0; if (sscanf(line, "%lf ?", x) == 1) { *y = NADBL; return 0; } return 1; } static int read_datafile (const char *fname, dataset *dset) { char line[256]; int i, err = 0; int got_e = 0, y2data = 0, fitline = 0; FILE *fdat; dataset_init(dset); fdat = gretl_fopen(fname, "r"); if (fdat == NULL) { fprintf(stderr, "Couldn't open '%s'\n", fname); return 1; } else { fprintf(stderr, "Reading %s...\n", fname); } while (fgets(line, sizeof line, fdat)) { tailstrip(line); if (get_comment(line, dset)) { continue; } else if (!strcmp(line, "e")) { fprintf(stderr, "Got end of data marker\n"); got_e++; if (got_e == 2) { /* can't handle more than two series! */ break; } } else if (strstr(line, "automatic fitted")) { fitline = 1; } else if (isdigit((unsigned char) line[0])) { if (strstr(line, "title")) { #ifdef PLAY_AUTOFIT_LINE if (fitline) { get_fit_params(line, dset); } #endif continue; } if (!got_e) { dset->n += 1; } else if (!y2data) { y2data = 1; } } } if (dset->n == 0) { fprintf(stderr, "No data in '%s'\n", fname); err = 1; goto bailout; } dset->points = malloc(dset->n * sizeof *dset->points); if (dset->points == NULL) { err = 1; fputs("Out of memory\n", stderr); goto bailout; } if (y2data) { dset->y2 = malloc(dset->n * sizeof *dset->y2); if (dset->y2 == NULL) { err = 1; fputs("Out of memory\n", stderr); goto bailout; } dset->series2 = 1; } rewind(fdat); i = got_e = 0; while (!err && fgets(line, 256, fdat)) { tailstrip(line); if (!strcmp(line, "e")) { got_e++; if (got_e == 2) { break; } i = 0; } else if (isdigit((unsigned char) line[0])) { double x, y; if (strstr(line, "title")) { continue; } if (get_data_x_y(line, &x, &y)) { fprintf(stderr, "Couldn't read data on line %d\n", i + 1); err = 1; } else { if (!got_e) { dset->points[i].x = x; dset->points[i].y = y; } else { dset->y2[i] = y; } } i++; } } bailout: fclose(fdat); if (err) { dataset_free(dset); } return err; } static void points_min_max (const datapoint *points, double *xmin, double *xmax, double *ymin, double *ymax, int n) { int i; *xmin = *xmax = points[0].x; *ymin = *ymax = points[0].y; for (i=1; i *xmax) *xmax = points[i].x; if (!na(points[i].y)) { if (points[i].y < *ymin) *ymin = points[i].y; if (points[i].y > *ymax) *ymax = points[i].y; } } } static void min_max (const double *x, double *min, double *max, int n) { int i; *min = *max = x[0]; for (i=1; i *max) *max = x[i]; } } } #if defined(HAVE_FLITE) #if 0 static int save_dataset_comments (const dataset *dset) { int i, j; cst_voice *v; cst_wave *w, *fullw = NULL; flite_init(); v = register_cmu_us_kal(); j = 0; for (i=0; icomments[i] != NULL) { if (j == 0) { fullw = flite_text_to_wave(dset->comments[i], v); } else { w = flite_text_to_wave(dset->comments[i], v); concat_wave(fullw, w); delete_wave(w); } j++; } } cst_wave_save_riff(fullw, "gretl_flite.wav"); delete_wave(fullw); return 0; } #endif static void speak_dataset_comments (const dataset *dset) { int i; cst_voice *v; flite_init(); v = register_cmu_us_kal(); for (i=0; icomments[i] != NULL) { flite_text_to_speech(dset->comments[i], v, "play"); } } } #elif defined(G_OS_WIN32) static WCHAR *wide_string (const char *s) { unsigned char *ret; int i, j, n = strlen(s); ret = malloc(2 * (n + 1)); if (ret == NULL) return NULL; j = 0; for (i=0; i<=n; i++) { ret[j++] = s[i]; ret[j++] = '\0'; } return (WCHAR *) ret; } static void speak_dataset_comments (const dataset *dset) { int i; ISpVoice *v = NULL; HRESULT hr; hr = CoInitialize(NULL); if (!SUCCEEDED(hr)) { fprintf(stderr, "CoInitialize failed\n"); return; } hr = CoCreateInstance(&CLSID_SpVoice, NULL, CLSCTX_ALL, &IID_ISpVoice, (void **) &v); if (SUCCEEDED(hr)) { for (i=0; icomments[i] != NULL) { WCHAR *w = wide_string(dset->comments[i]); ISpVoice_Speak(v, w, 0, NULL); free(w); } } ISpVoice_Release(v); } CoUninitialize(); } #endif static void print_dataset_comments (const dataset *dset) { int i; for (i=0; icomments[i] != NULL) { printf("Comment[%d]: %s\n", i+1, dset->comments[i]); } } #ifdef DO_SPEECH speak_dataset_comments(dset); #endif } static void audio_graph_error (const char *msg) { #ifdef HAVE_FLITE cst_voice *v; #endif #ifdef G_OS_WIN32 ISpVoice *v = NULL; HRESULT hr; #endif fprintf(stderr, "%s\n", msg); #ifdef HAVE_FLITE flite_init(); v = register_cmu_us_kal(); flite_text_to_speech(msg, v, "play"); #endif #ifdef G_OS_WIN32 hr = CoInitialize(NULL); if (!SUCCEEDED(hr)) return; hr = CoCreateInstance(&CLSID_SpVoice, NULL, CLSCTX_ALL, &IID_ISpVoice, (void **) &v); if (SUCCEEDED(hr)) { wchar_t *w = wide_string(msg); ISpVoice_Speak(v, w, 0, NULL); free(w); ISpVoice_Release(v); } #endif } static int compare_points (const void *a, const void *b) { const datapoint *pa = (const datapoint *) a; const datapoint *pb = (const datapoint *) b; return (pa->x > pb->x) - (pa->x < pb->x); } static int play_dataset (midi_spec *spec, midi_track *track, const dataset *dset) { double xmin, xmax, xavg; double ymin, ymax; double xscale, yscale; int i; track->notes = malloc(dset->n * sizeof *track->notes); if (track->notes == NULL) { fputs("out of memory\n", stderr); return 1; } track->channel = 0; track->patch = PC_GRAND; track->n_notes = dset->n; if (dset->pd == 0) { /* scatter plot: sort data by x value */ qsort((void *) dset->points, (size_t) dset->n, sizeof dset->points[0], compare_points); } points_min_max(dset->points, &xmin, &xmax, &ymin, &ymax, dset->n); if (dset->y2 != NULL) { double y2min, y2max; min_max(dset->y2, &y2min, &y2max, dset->n); if (y2min < ymin) ymin = y2min; if (y2max > ymax) ymax = y2max; } xavg = (xmax - xmin) / (dset->n - 1); /* normalize average x step to quarter note */ xscale = 1.0 / xavg; #if ADEBUG fprintf(stderr, "xavg = %g, xscale = %g\n", xavg, xscale); #endif yscale = YMAX / (ymax - ymin); for (i=0; in; i++) { double dtx, dux, ypos; if (!na(dset->points[i].y)) { ypos = (dset->points[i].y - ymin) * yscale; } else { ypos = NADBL; } if (i == 0) { dtx = 0.0; } else { dtx = xscale * (dset->points[i].x - dset->points[i-1].x); } if (i == dset->n - 1) { dux = xscale * xavg; } else { dux = xscale * (dset->points[i+1].x - dset->points[i].x); } track->notes[i].dtime = dtx; track->notes[i].duration = dux; if (!na(ypos)) { track->notes[i].pitch = 36 + (int) (ypos + 0.5); track->notes[i].force = DEFAULT_FORCE; } else { track->notes[i].pitch = 36; track->notes[i].force = 0; } #if ADEBUG fprintf(stderr, "Obs %d: x = %g, y = %g, ypos = %g\n", i, dset->points[i].x, dset->points[i].y, ypos); fprintf(stderr, " dtime=%g, duration=%g, pitch=%d\n", track->notes[i].dtime, track->notes[i].duration, track->notes[i].pitch); #endif } write_midi_track(track, spec); if (dset->series2) { for (i=0; in; i++) { double yi, ypos = 0; if (dset->y2 != NULL) { yi = dset->y2[i]; } else { yi = dset->intercept + dset->slope * dset->points[i].x; } if (!na(yi)) { ypos = (yi - ymin) * yscale; track->notes[i].pitch = 36 + (int) (ypos + 0.5); track->notes[i].force = DEFAULT_FORCE; } else { track->notes[i].pitch = 36; track->notes[i].force = 0; } #if ADEBUG fprintf(stderr, "Series2, Obs %d: x = %g, y = %g, ypos = %g\n", i, dset->points[i].x, yi, ypos); fprintf(stderr, " dtime=%g, duration=%g, pitch=%d\n", track->notes[i].dtime, track->notes[i].duration, track->notes[i].pitch); #endif } track->channel = 1; /* below: was PC_MARIMBA, but that's not in freepats */ track->patch = PC_CELLO; write_midi_track(track, spec); } free(track->notes); return 0; } #if defined(G_OS_WIN32) static int midi_fork (const char *fname, const char *midiplayer) { int err = 0; if ((long) ShellExecute(NULL, "open", fname, NULL, NULL, SW_SHOW) < 32) err = 1; return err; } #else /* non-Windows version */ static char **get_midi_args (int *argc, const char *fname, const char *midiplayer) { char **argv = NULL; const char *midistr; if (midiplayer != NULL && *midiplayer != '\0') { midistr = midiplayer; } else { midistr = (const char *) getenv("GRETL_MIDI_PLAYER"); } if (midistr == NULL || *midistr == '\0') { *argc = 3; argv = malloc((*argc + 1) * sizeof *argv); if (argv == NULL) return NULL; argv[0] = g_strdup(DEFAULT_MIDI_PROG); argv[1] = g_strdup(DEFAULT_MIDI_OPT); argv[2] = g_strdup(fname); argv[3] = NULL; } else { char *tmp = g_strdup(midistr); char *s = tmp; int i, ntoks = 1; while (*s) { if (*s == ' ') ntoks++; s++; } *argc = ntoks + 1; argv = malloc((*argc + 1) * sizeof *argv); if (argv == NULL) { g_free(tmp); return NULL; } if (ntoks > 1) { argv[0] = g_strdup(strtok(tmp, " ")); for (i=1; i