sspect.cpp 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931
  1. /*
  2. sspect - A simple spectrogram
  3. Copyright (C) 2018, 2019 Emily, emily@countermail.com
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU Affero General Public License as
  6. published by the Free Software Foundation, either version 3 of the
  7. License, or (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU Affero General Public License for more details.
  12. You should have received a copy of the GNU Affero General Public License
  13. along with this program. If not, see <https://www.gnu.org/licenses/>.
  14. */
  15. /*
  16. Based on glSpect (2012) by Alex Barnett, ahb@math.dartmouth.edu,
  17. and on glScope (2005) by Luke Campagnola, lcampagn@mines.edu. Both
  18. the above were licensed by
  19. Distributed under a completely free license; this means you can do
  20. absolutely anything you want with this code. I'd appreciate if you
  21. credit me where appropriate, though.
  22. */
  23. #include <GL/glut.h>
  24. #include <GL/gl.h>
  25. #include <alsa/asoundlib.h>
  26. #include <stdlib.h>
  27. #include <string.h>
  28. #include <stdio.h>
  29. #include <pthread.h>
  30. #include <sys/time.h>
  31. #include <math.h>
  32. #include <fftw3.h>
  33. #define PI 3.14159265358979323846
  34. class audioInput; // declarations (things needed before defined)
  35. void computespecslice(audioInput *ai);
  36. int chooseTics(float lo, float range, float fac, float *tics);
  37. static int verb; // global verbosity
  38. struct Param { // parameters object
  39. int devicenumber; // ALSA device number
  40. int windowtype; // DFT windowing function type
  41. int twowinsize; // power of 2 giving DFT win_size (N) in samples
  42. float freqline[100]; // at which frequencies to draw a line
  43. bool drawline = false; // has freqline been used?
  44. int nfreqlines = 0; // number of actual frequencies added to freqline
  45. bool drawcentroid = false; // has freqline been used?
  46. }; // note trailing ;
  47. Param param; // global parameters object
  48. const char *notenames[] = { "C","C#","D","Eb","E","F","F#","G","G#","A","Bb","B"}; // 1/17/16
  49. float timeDiff(timeval a, timeval b) {
  50. return (float)(b.tv_sec - a.tv_sec) + (float)(b.tv_usec - a.tv_usec) * 0.000001F;
  51. }
  52. void drawText(float x, float y, char *string) {
  53. int len, i;
  54. glRasterPos2f(x, y);
  55. len = (int) strlen(string);
  56. for (i = 0; i < len; i++)
  57. {
  58. glutBitmapCharacter(GLUT_BITMAP_HELVETICA_18, string[i]);
  59. }
  60. }
  61. void smallText(float x, float y, char *string) {
  62. int len, i;
  63. glRasterPos2f(x, y);
  64. len = (int) strlen(string);
  65. for (i = 0; i < len; i++)
  66. {
  67. glutBitmapCharacter(GLUT_BITMAP_HELVETICA_10, string[i]);
  68. }
  69. }
  70. ///////////// AUDIOINPUT class (no references to scn) ////////////////////
  71. class audioInput {
  72. public:
  73. char *chunk;
  74. float *b, *specslice, *bwin, *winf, *sg, *centroids;
  75. char *sgb;
  76. int b_ind, b_size, n_f, n_tw, sg_size, win_size;
  77. float dt, t_memory, Hz_per_pixel;
  78. bool quit, pause;
  79. pthread_t capture_thread;
  80. fftwf_plan fftw_p;
  81. // Audio device data (modified from ALSA tutorial)
  82. int bytes_per_frame, frames_per_period, nperiods, channels;
  83. int req_rate, rate; /* Requested and actual sample rate */
  84. int dir; /* rate == req_rate --> dir = 0 */
  85. /* rate < req_rate --> dir = -1 */
  86. /* rate > req_rate --> dir = 1 */
  87. snd_pcm_uframes_t period_size; // Period size (bytes)
  88. snd_pcm_uframes_t req_size, size; // requested and actual ALSA buffer size
  89. snd_pcm_t *pcm_handle; /* Handle for the PCM device */
  90. snd_pcm_stream_t stream; /* Playback stream */
  91. /* This structure contains information about */
  92. /* the hardware and can be used to specify the */
  93. /* configuration to be used for the PCM stream. */
  94. snd_pcm_hw_params_t *hwparams;
  95. /* Name of the PCM device, like plughw:0,0 */
  96. /* The first number is the number of the soundcard, */
  97. /* the second number is the number of the device. */
  98. char *pcm_name;
  99. void setupWindowFunc(float *w, int N) {
  100. float W;
  101. int i;
  102. if (verb) printf("windowtype=%d\n", param.windowtype);
  103. switch (param.windowtype) {
  104. case 0: // no window (crappy frequency spillover)
  105. for( i=0; i<N; ++i)
  106. w[i] = 1.0F;
  107. break;
  108. case 1: // Hann window (C^1 cont, so third-order tails)
  109. W = N/2.0F;
  110. for( i=0; i<N; ++i)
  111. w[i] = (1.0F + cos(PI*(i-W)/W))/2;
  112. break;
  113. case 2: // truncated Gaussian window (Gaussian tails + exp small error)
  114. W = N/5.0F; // width: keep small truncation but wide to not waste FFT
  115. for( i=0; i<N; ++i) w[i] = exp(-(i-N/2)*(i-N/2)/(2*W*W));
  116. break;
  117. default:
  118. fprintf(stderr, "unknown windowtype!\n");
  119. }
  120. }
  121. audioInput() { // nontrivial constructor
  122. quit = false;
  123. pause = false;
  124. channels = 2; // Had to change to stereo for System76 ! (was mono)
  125. bytes_per_frame = 2 * channels; // 16-bit
  126. req_rate = 44100; // audio sampling rate in Hz
  127. frames_per_period = (int)(req_rate/60.0); // 735 = 44100Hz/60fps assumed
  128. nperiods = 2; // >=2, see ALSA manual
  129. t_memory = 20.0; // memory of our circular buffer in secs
  130. period_size = frames_per_period * bytes_per_frame;
  131. chunk = new char[period_size]; // raw data buffer for PCM read: 1 period
  132. req_size = frames_per_period * nperiods; // ALSA device buffer size (frames)
  133. b_ind = 0; // integer index where to write to in buffer
  134. if( initDevice() < 0 ) // set up sound card for recording (sets rate, size)
  135. exit(1);
  136. dt = 1.0 / (float)rate; // sampling period
  137. b_size = (int)(t_memory * rate); // buffer size
  138. if (verb) printf("memory buffer size %d samples\n", b_size);
  139. b = new float[b_size]; // our circular audio buffer
  140. win_size = 1<<param.twowinsize; // FFT size
  141. bwin = new float[win_size]; // windowed recent audio data
  142. winf = new float[win_size];
  143. setupWindowFunc(winf, win_size); // windowing function
  144. // set up fast in-place single-precision real-to-half-complex DFT:
  145. fftw_p = fftwf_plan_r2r_1d(win_size, bwin, bwin, FFTW_R2HC, FFTW_MEASURE);
  146. n_f = 560; // # freqs ...spectrogram stuff
  147. specslice = new float[n_f];
  148. n_tw = 940; // # time windows: should be multiple of 4 for glDrawPixels
  149. centroids = new float[n_tw]; // array for storing centroids
  150. for (int i=0; i<n_tw; ++i)
  151. centroids[i]=0;
  152. sg_size = n_f * n_tw;
  153. sg = new float[sg_size]; // spectrogram array float
  154. sgb = new char[sg_size]; // spectrogram array 8-bit
  155. //for( int i=0; i<sg_size; ++i ) // fill with random for now
  156. //sg[i] = (float)(random()/(float)RAND_MAX);
  157. //for( int i=0; i<sg_size; ++i ) // fill with random for now
  158. //sgb[i] = (int)(256.0*random()/(float)RAND_MAX);
  159. Hz_per_pixel = 1.0F / (win_size*dt);
  160. if (verb) printf("Hz per pixel = %.3f\n", Hz_per_pixel);
  161. // Start recording thread... runs independently, writing data into ai->b
  162. pthread_create(&capture_thread, NULL, audioCapture, (void*)this); // this?
  163. }
  164. ~audioInput() { // destructor
  165. snd_pcm_close (pcm_handle);
  166. fftwf_destroy_plan(fftw_p);
  167. }
  168. int initDevice() { // ........ set up sound card for recording ........
  169. // ALSA tutorial, taken from http://www.suse.de/~mana/alsa090_howto.html
  170. stream = SND_PCM_STREAM_CAPTURE;
  171. /* Init pcm_name. Accepts device numbers up to 10^21-1. */
  172. char pcm_devnum[21];
  173. snprintf(pcm_devnum, 21, "%d", param.devicenumber);
  174. pcm_name = strdup("plughw:");
  175. strcat(pcm_name, pcm_devnum);
  176. strcat(pcm_name, ",0");
  177. /* Allocate the snd_pcm_hw_params_t structure on the stack. */
  178. snd_pcm_hw_params_alloca(&hwparams);
  179. /* Open PCM. The last parameter of this function is the mode. */
  180. /* If this is set to 0, the standard mode is used. Possible */
  181. /* other values are SND_PCM_NONBLOCK and SND_PCM_ASYNC. */
  182. /* If SND_PCM_NONBLOCK is used, read / write access to the */
  183. /* PCM device will return immediately. If SND_PCM_ASYNC is */
  184. /* specified, SIGIO will be emitted whenever a period has */
  185. /* been completely processed by the soundcard. */
  186. if (snd_pcm_open(&pcm_handle, pcm_name, stream, 0) < 0) {
  187. fprintf(stderr, "Error opening PCM device %s\n", pcm_name);
  188. return(-1);
  189. }
  190. /* Init hwparams with full configuration space */
  191. if (snd_pcm_hw_params_any(pcm_handle, hwparams) < 0) {
  192. fprintf(stderr, "Can not configure this PCM device.\n");
  193. return(-1);
  194. }
  195. /* Set access type. This can be either */
  196. /* SND_PCM_ACCESS_RW_INTERLEAVED or */
  197. /* SND_PCM_ACCESS_RW_NONINTERLEAVED. */
  198. /* There are also access types for MMAPed */
  199. /* access, but this is beyond the scope */
  200. /* of this introduction. */
  201. if (snd_pcm_hw_params_set_access(pcm_handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED) < 0) {
  202. fprintf(stderr, "Error setting access.\n");
  203. return(-1);
  204. }
  205. /* Set sample format */
  206. if (snd_pcm_hw_params_set_format(pcm_handle, hwparams, SND_PCM_FORMAT_S16_LE) < 0) {
  207. fprintf(stderr, "Error setting format.\n");
  208. return(-1);
  209. }
  210. /* Set sample rate. If the requested rate is not supported */
  211. /* by the hardware, use nearest possible rate. */
  212. rate = req_rate;
  213. if (snd_pcm_hw_params_set_rate_near(pcm_handle, hwparams, (uint*)&rate, 0) < 0) {
  214. fprintf(stderr, "Error setting rate.\n");
  215. return(-1);
  216. }
  217. if (rate != req_rate) {
  218. fprintf(stderr, "The rate %d Hz is not supported by your hardware.\n \
  219. ==> Using %d Hz instead.\n", req_rate, rate);
  220. }
  221. /* Set number of channels */
  222. if (snd_pcm_hw_params_set_channels(pcm_handle, hwparams, channels) < 0) {
  223. fprintf(stderr, "Error setting channels.\n");
  224. return(-1);
  225. }
  226. /* Set number of periods. Periods used to be called fragments. */
  227. if (snd_pcm_hw_params_set_periods(pcm_handle, hwparams, nperiods, 0) < 0) {
  228. fprintf(stderr, "Error setting number of periods.\n");
  229. return(-1);
  230. }
  231. /* Set buffer size (in frames). The resulting latency is given by */
  232. /* latency = period_size * nperiods / (rate * bytes_per_frame) */
  233. size = req_size;
  234. if (snd_pcm_hw_params_set_buffer_size_near(pcm_handle, hwparams, &size) < 0) {
  235. fprintf(stderr, "Error setting buffersize.\n");
  236. return(-1);
  237. }
  238. if( size != req_size ) {
  239. fprintf(stderr, "Buffer size %d is not supported, using %d instead.\n", (int)req_size, (int)size);
  240. }
  241. /* Apply HW parameter settings to PCM device and prepare device */
  242. if (snd_pcm_hw_params(pcm_handle, hwparams) < 0) {
  243. fprintf(stderr, "Error setting HW params.\n");
  244. return(-1);
  245. }
  246. return 1;
  247. } // ........................................
  248. void quitNow() {
  249. quit = true;
  250. // pthread_kill_other_threads_np();
  251. snd_pcm_close (pcm_handle);
  252. }
  253. union byte { // used to convert from signed to unsigned
  254. unsigned char uchar_val;
  255. char char_val;
  256. };
  257. int mod( int i ) { // true modulo (handles negative) into our buffer b
  258. // wraps i to lie in [0, (b_size-1)]. rewritten Barnett
  259. int r = i % b_size;
  260. if (r<0)
  261. r += b_size;
  262. return r;
  263. }
  264. static void* audioCapture(void* a) { //-------- capture: thread runs indep --
  265. // still mostly Luke's code, some names changed. Aims to read 1 "period"
  266. // (ALSA device setting) into the current write index of our ai->b buffer.
  267. fprintf(stderr, "audioCapture thread started...\n");
  268. audioInput* ai = (audioInput*) a; // shares data with main thread = cool!
  269. float inv256 = 1.0 / 256.0;
  270. float inv256_2 = inv256*inv256;
  271. while( ! ai->quit ) { // loops around until state of ai kills it
  272. int n;
  273. if( ! ai->pause ) {
  274. // keep trying to get exactly 1 "period" of raw data from sound card...
  275. while((n = snd_pcm_readi(ai->pcm_handle, ai->chunk, ai->frames_per_period)) < 0 ) {
  276. // if (n == -EPIPE) fprintf(stderr, "Overrun occurred: %d\n", n); // broken pipe
  277. fprintf(stderr, "Error occured while recording: %s\n", snd_strerror(n));
  278. //n = snd_pcm_recover(ai->pcm_handle, n, 0); // ahb
  279. //fprintf(stderr, "Error occured while recording: %s\n", snd_strerror(n));
  280. snd_pcm_prepare(ai->pcm_handle);
  281. //fprintf(stderr, "Dropped audio data (frames read n=%d)\n", n);
  282. } // n samples were got
  283. if (verb>1) printf("snd_pcm_readi got n=%d frames\n", n);
  284. byte by;
  285. int write_ptr, read_ptr;
  286. for( int i = 0; i < n; i++ ) { // read chunk into our buffer ai->b ...
  287. read_ptr = i * ai->bytes_per_frame;
  288. write_ptr = ai->mod(ai->b_ind + i); // wraps around
  289. by.char_val = ai->chunk[read_ptr];
  290. // compute float in [-1/2,1/2) from 16-bit raw... (LSB unsigned char)
  291. ai->b[write_ptr] = (float)ai->chunk[read_ptr+1]*inv256 + (float)by.uchar_val*inv256_2;
  292. }
  293. ai->b_ind = ai->mod(ai->b_ind+n); // update index (in one go)
  294. computespecslice(ai); // compute spectral slice of recent buffer history
  295. }
  296. else {
  297. usleep(10000); // wait 0.01 sec if paused (keeps thread CPU usage low)
  298. }
  299. }
  300. fprintf(stderr, "audioCapture thread exiting.\n");
  301. } // ----------------------- end capture thread ----
  302. };
  303. //////////////////////////////////////// SCENE CLASS /////////////////////////
  304. class scene
  305. {
  306. public:
  307. float viewport_size[2]; // gl window width, height
  308. float mouse[3];
  309. bool pause;
  310. audioInput* ai;
  311. float color_scale[2]; // sg image log color mapping params
  312. bool diagnose; // medical stuff
  313. char diagnosis[1000];
  314. time_t fps_tic; // FPS time reference
  315. int frameCount, fps; // frames per sec and counter
  316. int scroll_fac, scroll_count, colormode;
  317. float run_time;
  318. timeval start_time;
  319. int freqind; // toggles frequency readoff line(s), either 0,1,2
  320. scene() { } // trivial constructor, to prevent global init fiasco
  321. ~scene() { } // destructor
  322. void init() { // meaningful constructor - could move some to main to
  323. // parse cmd line
  324. pause = false;
  325. color_scale[0] = 100.0+40; // 8-bit intensity offset
  326. color_scale[1] = 255/120.0/1.5; // 8-bit intensity slope (per dB units)
  327. ai = new audioInput();
  328. fps_tic = time(NULL); frameCount = fps = 0;
  329. colormode = 2;
  330. scroll_count = 0;
  331. gettimeofday(&start_time, NULL);
  332. run_time = 0.0;
  333. freqind = 0;
  334. diagnose = 0; // initialize Medical stuff
  335. strcpy(diagnosis, "no diagnosis ...");
  336. }
  337. void graph() // simple graph of ai->float_data ............................
  338. {
  339. float tshow = 0.1; // only show the most recent tshow secs of samples
  340. glDisable(GL_DEPTH_TEST); glDisable(GL_BLEND);
  341. glDisable(GL_LINE_SMOOTH); glLineWidth(2);
  342. glMatrixMode(GL_MODELVIEW);
  343. glPushMatrix(); // use modelview matrix to xform [-tshow,0]x[-1,1] somewhere
  344. glTranslatef(0.97,0.1,0);
  345. glScalef(4.0,1.0,1.0); // x-scale for time-units, y-scale is 1
  346. char xlab[] = "t(s)", ylab[] = ""; // axis labels
  347. //drawAxes(-tshow, 0, -0.1, 0.1, 1, 1, 0, 0, xlab, ylab); // not needed?
  348. glColor4f(0.4, 1.0, 0.6, 1);
  349. glBegin(GL_LINE_STRIP);
  350. float x = -tshow;
  351. int ilo = ai->b_ind-(int)(tshow*ai->rate);
  352. int ihi = ai->b_ind; // NB get now since capture thread may change it!
  353. for( int i=ilo; i<ihi; i++ ) { // graph the most recent piece of buffer
  354. glVertex2f(x, ai->b[ai->mod(i)]);
  355. x += ai->dt; // x time increment
  356. }
  357. glEnd();
  358. glPopMatrix();
  359. } // ............................................................
  360. void slice() // simple graph of spectral slice with log power scale
  361. {
  362. glMatrixMode(GL_MODELVIEW);
  363. glPushMatrix(); // use modelview matrix to xform [-tshow,0]x[-1,1] somewhere
  364. glTranslatef(0.15,0.11,0); // decide the location of 0,0 of the graph
  365. float max_Hz = ai->Hz_per_pixel*ai->n_f;
  366. glScalef(0.3/max_Hz,0.0015,1.0); // x-scale for freq-units, y-scale for dB
  367. glLineWidth(1);
  368. glColor4f(0.5, 0.4, 0.2, 1); // lines showing spectrogram color range
  369. glBegin(GL_LINES);
  370. float dB_min = -color_scale[0]/color_scale[1];
  371. float dB_max = (255.0-color_scale[0])/color_scale[1];
  372. glVertex2f(0,dB_min);glVertex2f(max_Hz,dB_min);
  373. glVertex2f(0,dB_max);glVertex2f(max_Hz,dB_max);
  374. glEnd();
  375. glColor4f(1.0, 0.8, 0.3, 1); // the graph
  376. glBegin(GL_LINE_STRIP);
  377. for( int i=0; i<ai->n_f; i++ )
  378. glVertex2f(i*ai->Hz_per_pixel, 20*log10(ai->specslice[i]));
  379. glEnd();
  380. char xlab[] = "f(Hz)", ylab[] = "I(dB)"; // axis labels
  381. drawAxes(0-1e-7, max_Hz, -50, 50, 1.0, 1.0, 1, 0, xlab, ylab);
  382. glPopMatrix();
  383. }
  384. void drawAxes(float x0, float x1, float y0, float y1, float xf, float yf, bool box, bool grid, char* xlab, char* ylab) // draws axes in [x0,x1]x[y0,y1]
  385. // box: 0 coord axes no box, 1 left-bottom axes no box, 2 box no axes
  386. // xf, yf are density factors, roughly given by screen size of object.
  387. // xlab, ylab are labels
  388. {
  389. int n_tics, i, len;
  390. char label[20];
  391. float tics[100];
  392. float x_pix_size = 2.0*(x1-x0)/(viewport_size[0]*xf); // fudge fac
  393. float y_pix_size = 2.0*(y1-y0)/(viewport_size[1]*yf);
  394. glColor4f(0.7, 1.0, 1.0, 1);
  395. glDisable(GL_LINE_SMOOTH); glLineWidth(1);
  396. if (box) { glBegin(GL_LINE_STRIP); // box and LB axes variants
  397. glVertex2f(x0,y1); glVertex2f(x0,y0); glVertex2f(x1,y0);
  398. if (box>1) { glVertex2f(x1,y1); glVertex2f(x0,y1); }
  399. glEnd();
  400. } else { glBegin(GL_LINES); // coord axes lines
  401. glVertex2f(0,y0); glVertex2f(0,y1); glVertex2f(x0,0); glVertex2f(x1,0);
  402. glEnd(); }
  403. smallText(x1+8*x_pix_size,y0-8*y_pix_size,xlab); // axes labels
  404. smallText(x0-8*x_pix_size,y1+8*y_pix_size,ylab);
  405. n_tics = chooseTics(x0,x1-x0,xf,tics); // x-axis tics
  406. float xtic_top = (box==0) ? 0.0 : y0;
  407. float xtic_bot = xtic_top - 0.02*(y1-y0)/yf;
  408. glBegin(GL_LINES);
  409. for (i=0;i<n_tics;++i) {
  410. glVertex2d(tics[i], xtic_bot); glVertex2d(tics[i], xtic_top);
  411. }
  412. glEnd();
  413. for (i=0;i<n_tics;++i) {
  414. sprintf(label,"%.6g",tics[i]);
  415. smallText((float)(tics[i] - 4*strlen(label)*x_pix_size), \
  416. (float)(xtic_bot - 12*y_pix_size), label);
  417. }
  418. n_tics = chooseTics(y0,y1-y0,yf,tics); // y-axis tics
  419. float ytic_top = (box==0) ? 0.0 : x0;
  420. float ytic_bot = ytic_top - 0.02*(x1-x0)/xf;
  421. glBegin(GL_LINES);
  422. for (i=0;i<n_tics;++i) {
  423. glVertex2d(ytic_bot, tics[i]); glVertex2d(ytic_top, tics[i]);
  424. }
  425. glEnd();
  426. for (i=0;i<n_tics;++i) {
  427. sprintf(label,"%.6g",tics[i]);
  428. smallText((float)(ytic_bot - 8*strlen(label)*x_pix_size), \
  429. (float)(tics[i] - 4*y_pix_size), label);
  430. }
  431. }
  432. void drawLines(float x0, float x1) // draws lines in [x0,x1]x{param.freqlines}
  433. {
  434. glColor4f(0.7, 1.0, 1.0, 1);
  435. glDisable(GL_LINE_SMOOTH); glLineWidth(1);
  436. glBegin(GL_LINES);
  437. if(param.drawline) {
  438. for (int n=0; n<param.nfreqlines; ++n) {
  439. glVertex2d(x0,param.freqline[n]); glVertex2d(x1,param.freqline[n]); // draw line at frequency freqline[n]
  440. }
  441. }
  442. glEnd();
  443. }
  444. void drawCentroids(float x0, float x1) // draws lines in [x0,x1]x{param.freqlines}
  445. {
  446. glColor4f(0.7, 1.0, 1.0, 1);
  447. glDisable(GL_LINE_SMOOTH); glLineWidth(1);
  448. glBegin(GL_LINES);
  449. float dt = (x1-x0)/ai->n_tw;
  450. for (int i=0; i < ai->n_tw; ++i) {
  451. glVertex2d(x0+i*dt, ai->centroids[i]);
  452. glVertex2d(x0+(i+1)*dt, ai->centroids[i+1]);
  453. }
  454. glEnd();
  455. }
  456. void spectrogram() // show spectrogram as 2D pixel array, w/ axes............
  457. {
  458. float x0=0.05, y0=0.22; // bot-left location in viewport (as unit square)
  459. float currfreq, linefreq; char str[50]; // for freqind
  460. int nummults, i, notenum, octave; // for freqind
  461. glRasterPos2f(x0,y0);
  462. glDisable(GL_DEPTH_TEST); glDisable(GL_BLEND);
  463. //glPixelZoom(1.f,1.f); // zoom: non-integers are quite a bit slower (ie twice), and seems to have permanently worsened the DrawPixels speed by factor 2!
  464. //glPixelZoom(1.F,2.F);
  465. //float z; glGetFloatv(GL_ZOOM_Y, &z); printf("%f\n",z); // read a zoom
  466. if (colormode<2) // b/w
  467. glDrawPixels(ai->n_tw, ai->n_f, GL_LUMINANCE, GL_UNSIGNED_BYTE, ai->sgb);
  468. else
  469. glDrawPixels(ai->n_tw, ai->n_f, GL_RGB, GL_UNSIGNED_BYTE_3_3_2, ai->sgb);
  470. //glDrawPixels(ai->n_tw, ai->n_f, GL_RED, GL_UNSIGNED_BYTE, ai->sgb); //test
  471. // from: http://www.gamedev.net/topic/510914-glcolortable-with-textures/
  472. //float c[256][3];
  473. //for (int i=0;i<256;i++) {
  474. // c[i][0] = ((float)i)/256;
  475. // c[i][1] = 1.0f - ((float)i)/256;
  476. // c[i][2] = ((float)i)/256;
  477. //}
  478. //glColorTable(GL_COLOR_TABLE, GL_RGB, 256, GL_RGB, GL_FLOAT, c);
  479. //glEnable(GL_COLOR_TABLE);
  480. //glDrawPixels(ai->n_tw, ai->n_f, GL_BITMAP,GL_COLOR_INDEX, ai->sgb);
  481. // fails!
  482. glMatrixMode(GL_MODELVIEW); glPushMatrix(); // xform axes into right place
  483. float secs_per_pixel = scroll_fac / 60.0; // (float)fps, or longer FPS mean?
  484. float max_Hz = ai->Hz_per_pixel*ai->n_f;
  485. float max_t = secs_per_pixel*ai->n_tw;
  486. glTranslatef(x0 + (ai->n_tw-run_time/secs_per_pixel)/(float)viewport_size[0], y0, 0);
  487. glScalef(1.0F/(viewport_size[0]*secs_per_pixel),1.0F/(viewport_size[1]*ai->Hz_per_pixel),1); // makes units match the spectrogram t (scrolling) & freq axes
  488. // plots now occur in physical t (s) and f (Hz) units, relative to start_t..
  489. char xlab[] = "t(s)", ylab[] = "f(Hz)";
  490. drawAxes(run_time-max_t, run_time, 0 - 1e-7, max_Hz, 2.0, 2.0, 2, 0, xlab, ylab);
  491. drawLines(run_time-max_t, run_time);
  492. if(param.drawcentroid)
  493. drawCentroids(run_time-max_t, run_time);
  494. // 1e-7 is hack to show 0 Hz
  495. if (freqind) { // horiz freq readout line, in Hz/sec coords, 1/17/16
  496. // reverse-engineer the freq from current mouse y in pixels (yuk)...
  497. currfreq = ai->Hz_per_pixel*(viewport_size[1]*(1-y0)-mouse[1]);
  498. if (currfreq>0.0) { // only show if meaningful freq
  499. nummults = (freqind>1) ? 10 : 1;
  500. for (i=1;i<=nummults;++i) { // do either one or many lines
  501. linefreq = i*currfreq;
  502. if (i==1)
  503. glColor4f(0.8, 0.5, 0.0, 1); // orange
  504. else
  505. glColor4f(0.8, 0.5, 0.0, 1); // darker orange
  506. glDisable(GL_LINE_SMOOTH); glLineWidth(1);
  507. glBegin(GL_LINES);
  508. glVertex2f(run_time-max_t, linefreq);
  509. glVertex2f(run_time, linefreq);
  510. glEnd();
  511. glEnable(GL_BLEND); // text label: overwrite entirely
  512. glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
  513. notenum = (int)roundf(12 * logf(linefreq/261.626)/logf(2.0));
  514. octave = 4 + notenum/12; // relies on int division
  515. if (octave<0) octave = (int)NAN;
  516. sprintf(str, " %.d %s%d",(int)roundf(linefreq),notenames[(notenum+1200) % 12],octave); // use global lookup table of note names
  517. smallText(run_time-max_t,linefreq+3.0*ai->Hz_per_pixel,str);
  518. }
  519. }
  520. }
  521. glPopMatrix();
  522. } // .......................................................
  523. char color_byte(float x) // convert spectrogram entry to 8bit color char
  524. // barnett 1/25/15
  525. {
  526. float fac = 20.0 * color_scale[1]; // color_scale[1] units: intensity/dB
  527. int k = (int)(color_scale[0] + fac*log10(x));
  528. if (k>255) k=255; else if (k<0) k=0;
  529. if (colormode==0) // b/w
  530. return (char)k;
  531. else if (colormode==1) // inverse b/w
  532. return (char)(255-k);
  533. else if (colormode==2) { // color (pack into 3_3_2 RGB format) .. SLOW?
  534. float a = k/255.0f; // 0<a<1. now map from [0,1] to rgb in [0,1]
  535. float r = 5*(a-0.2); if (r<0) r=0.0; else if (r>=1) r=.955; // clip
  536. float g = 5*(a-0.6); if (g<0) g=0.0; else if (g>=1) g=.995;
  537. float b = 5*a;
  538. if (a>0.8) b = 5*(a-0.8); else if (a>0.4) b = 5*(0.6-a);
  539. if (b<0) b=0.0; else if (b>=1) b=.995;
  540. return (char)(b*4 + 4*((int)(g*8)) + 32*((int)(r*8))); // pack to 8bits
  541. }
  542. }
  543. void regen_sgb() // recompute 8bit spectrogram from float spectrogram
  544. // can only be done at a few FPS, so only do it when color scale changes
  545. {
  546. int i, j, n = ai->n_tw;
  547. for ( i=0; i<n; ++i) // loop over whole sg array
  548. for ( j=0; j<ai->n_f; ++j)
  549. ai->sgb[j*n + i] = color_byte(ai->sg[j*n + i]);
  550. }
  551. }; //////////////////////////////////////////// END SCENE CLASS ////////////////
  552. static scene scn; // global scene which contains everything (eg via scn.ai)
  553. int chooseTics(float lo, float range, float fac, float *tics)
  554. /* returns the number of tics, and their locations in zero-indexed tics[].
  555. * Barnett 99/10/20, single-prec version of ~visu/viewer/viewer.c
  556. * But, has density fudge factor fac (if 0, chooses default value).
  557. */
  558. {
  559. int i, n_tics, tic_start;
  560. float exponent, logr, spacing;
  561. if (fac==0.0) fac = 1.0;
  562. /* adjust the range multiplier here to give good tic density... */
  563. logr = log10(range * 0.4 / fac);
  564. exponent = floor(logr);
  565. spacing = pow(10.0, exponent);
  566. if (logr-exponent > log10(5.0))
  567. spacing *= 5.0;
  568. else if (logr-exponent > log10(2.0))
  569. spacing *= 2.0;
  570. /* (int) and copysign trick is to convert the floor val to an int... */
  571. tic_start = (int)(copysign(0.5,lo) + 1.0 + floor(lo / spacing));
  572. n_tics = (int)(1.0 + (lo + range - tic_start*spacing)/spacing);
  573. for (i=0;i<n_tics;++i) {
  574. tics[i] = spacing * (tic_start + i);
  575. }
  576. return n_tics;
  577. }
  578. void drawText() // Medical text and other text overlays................
  579. {
  580. glEnable (GL_BLEND); // glBlendFunc (GL_ONE,GL_ZERO); // overwrite entirely
  581. glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
  582. glColor4f(1, .4, .4, 1.0); // write FPS, other params... coords in unit square
  583. char str[50]; sprintf(str, "%d FPS",scn.fps); smallText(0.92,0.96,str);
  584. sprintf(str, "gain offset %.0f dB",scn.color_scale[0]);
  585. smallText(0.02,0.04,str);
  586. sprintf(str, "dyn range %.1f dB",255.0/scn.color_scale[1]);
  587. smallText(0.02,0.02,str);
  588. if (scn.diagnose) { // show diagnosis
  589. glColor4f(.2, .2, .2, 0.8); // transparent box
  590. glMatrixMode(GL_MODELVIEW);
  591. glPushMatrix(); // use modelview matrix to xform unit square to text box
  592. glTranslatef(0.5,0.5,0); glScalef(0.5,0.4,1); glTranslatef(-0.5,-0.5,0);
  593. glBegin(GL_POLYGON);
  594. glVertex2f(0,0); glVertex2f(1,0); glVertex2f(1,1); glVertex2f(0,1); glVertex2f(0,0); // unit square
  595. glEnd();
  596. glColor4f(1, 1, 1, 1); // text
  597. drawText(0.2, 0.5, scn.diagnosis); // coords relative to box as unit sq
  598. glPopMatrix();
  599. }
  600. }
  601. void display() // GLUT's display routine: 2D layering by write order
  602. {
  603. glClear(GL_COLOR_BUFFER_BIT); // no depth buffer
  604. glMatrixMode(GL_PROJECTION);
  605. glLoadIdentity();
  606. glOrtho(0, 1, 0, 1, -1, 1); // l r b t n f
  607. glMatrixMode(GL_MODELVIEW);
  608. glLoadIdentity();
  609. scn.graph(); // simple graph of signal
  610. scn.slice(); // spectral slice
  611. scn.spectrogram(); // note, overlays the signal and spectrum graphs
  612. drawText(); // overlaid
  613. // glFinish(); // wait for all gl commands to complete
  614. glutSwapBuffers(); // for this to WAIT for vSync, need enable in NVIDIA OpenGL
  615. }
  616. void computespecslice(audioInput *ai) // windowed spectral slice!
  617. {
  618. int N = ai->win_size; // transform length
  619. int nf = ai->n_f; // # freqs to fill in powerspec
  620. if (0)
  621. for ( int i=0; i<nf; ++i ) // dummy just copy last n_f samples
  622. ai->specslice[i] = 100.0 * ai->b[ai->mod(ai->b_ind - nf + i)];
  623. else { // actual windowed power spectrum
  624. for ( int i=0; i<N; ++i ) // copy last N samples & mult by window func
  625. ai->bwin[i] = ai->winf[i] * ai->b[ai->mod(ai->b_ind - N + i)];
  626. fftwf_execute(ai->fftw_p); // do the already-planned fft
  627. if (nf>N/2) {fprintf(stderr,"window too short cf n_f!\n"); return;}
  628. ai->specslice[0] = ai->bwin[0]*ai->bwin[0]; // zero-freq has no imag
  629. for ( int i=1; i<nf; ++i ) // compute power spectrum from hc dft
  630. ai->specslice[i] = ai->bwin[i]*ai->bwin[i] + ai->bwin[N-i]*ai->bwin[N-i];
  631. }
  632. }
  633. void scroll_sg(audioInput *ai, float *newcol, float *centroids)
  634. // Scroll spectrogram data 1 pixel in t direction, add new col & make 8bit
  635. {
  636. int i, j, n = ai->n_tw;
  637. for ( i=0; i<n; ++i) // float: NB x (ie t) is the fast storage direc
  638. for ( j=0; j<ai->n_f; ++j)
  639. ai->sg[j*n + i] = ai->sg[j*n + i + 1];
  640. for ( i=0; i<n; ++i) // scroll the 8bit char data too
  641. for ( j=0; j<ai->n_f; ++j)
  642. ai->sgb[j*n + i] = ai->sgb[j*n + i + 1];
  643. for ( i=0; i<n; ++i)
  644. centroids[i] = centroids[i+1];
  645. // Calculate the new centroid
  646. float num = 0;
  647. float denom = 0;
  648. float curr_freq;
  649. for (j=0; j<ai->n_f; ++j) {
  650. curr_freq = ai->Hz_per_pixel * j; // frequency whose amplitude is newcol[j]
  651. denom += newcol[j];
  652. num += newcol[j] * curr_freq;
  653. }
  654. float centroid = num / denom;
  655. centroids[n] = centroid;
  656. for ( j=0; j<ai->n_f; ++j) { // set the last col of float data
  657. ai->sg[j*n + n - 1] = newcol[j];
  658. }
  659. for ( j=0; j<ai->n_f; ++j) { // color xform for last col of 8bit data
  660. ai->sgb[j*n + n - 1] = scn.color_byte(newcol[j]);
  661. }
  662. }
  663. // --------------------------- IDLE FUNC ------------------------------------
  664. void idle(void) // put numerical intensive part here? only scrolling
  665. {
  666. ++scn.frameCount; // compute FPS...
  667. time_t now = time(NULL); // integer in seconds
  668. if (now>scn.fps_tic) { // advanced by at least 1 sec?
  669. scn.fps = scn.frameCount;
  670. scn.frameCount = 0;
  671. scn.fps_tic = now;
  672. }
  673. if (!scn.ai->pause) { // scroll every scroll_fac vSyncs, if not paused:
  674. timeval nowe; // update runtime
  675. gettimeofday(&nowe, NULL);
  676. //scn.run_time = timeDiff(scn.start_time, nowe); // update run_time data
  677. scn.run_time += 1/60.0; // is less jittery, approx true
  678. scn.run_time = fmod(scn.run_time, 100.0); // wrap time label after 100 secs
  679. ++scn.scroll_count;
  680. if (scn.scroll_count==scn.scroll_fac) {
  681. scroll_sg(scn.ai, scn.ai->specslice, scn.ai->centroids); // add spec slice to sg & scroll
  682. scn.scroll_count = 0;
  683. }
  684. }
  685. glutPostRedisplay(); // trigger GLUT display func
  686. }
  687. void keyboard(unsigned char key, int xPos, int yPos)
  688. {
  689. if( key == 27 || key == 'q') { // esc or q to quit
  690. scn.ai->quitNow();
  691. exit(0);
  692. } else if( key == ' ' ) {
  693. scn.pause = ! scn.pause;
  694. scn.ai->pause = ! scn.ai->pause;
  695. } else if( key == 'd' ) {
  696. scn.diagnose = ! scn.diagnose; // toggle text overlay
  697. } else if( key == ']' ) { // speed up scroll rate
  698. if (scn.scroll_fac>1) {scn.scroll_fac--; scn.scroll_count=0; }
  699. } else if( key == '[' ) {
  700. if (scn.scroll_fac<50) scn.scroll_fac++;
  701. } else if( key == 'i' ) {
  702. scn.colormode = (scn.colormode + 1) % 3; // spectrogram color scheme
  703. scn.regen_sgb();
  704. } else {
  705. fprintf(stderr, "pressed key %d\n", (int)key);
  706. }
  707. }
  708. void special(int key, int xPos, int yPos) { // special key handling
  709. if( key == 102 ) { // rt
  710. scn.color_scale[1] *= 1.5; scn.regen_sgb(); // contrast
  711. } else if( key == 100 ) { // lt
  712. scn.color_scale[1] /= 1.5; scn.regen_sgb(); // contrast
  713. } else if( key == 103 ) { // dn
  714. scn.color_scale[0] -= 20; scn.regen_sgb(); // brightness
  715. } else if( key == 101 ) { // up
  716. scn.color_scale[0] += 20; scn.regen_sgb(); // brightness
  717. } else {
  718. fprintf(stderr, "pressed special key %d\n", key);
  719. }
  720. }
  721. void reshape(int w, int h) // obsolete in game mode
  722. {
  723. scn.viewport_size[0] = w;
  724. scn.viewport_size[1] = h;
  725. if (verb) fprintf(stderr, "Setting w=%d, h=%d\n", w, h);
  726. glViewport(0, 0, w, h);
  727. }
  728. void mouse(int button, int state, int x, int y)
  729. {
  730. scn.mouse[0] = x; // this seems to be needed for correct panning
  731. scn.mouse[1] = y;
  732. scn.mouse[2] = button;
  733. if (verb) printf("Mouse event: button: %d state: %d pos: %d, %d\n", button, state, x, y);
  734. if (button==GLUT_LEFT_BUTTON && state==GLUT_DOWN) scn.freqind = 1; // toggle
  735. if (button==GLUT_LEFT_BUTTON && state==GLUT_UP) scn.freqind = 0;
  736. if (button==GLUT_RIGHT_BUTTON && state==GLUT_DOWN) scn.freqind = 2; // toggle w/ freq multiples shown
  737. if (button==GLUT_RIGHT_BUTTON && state==GLUT_UP) scn.freqind = 0;
  738. if (button==GLUT_MIDDLE_BUTTON && state==GLUT_UP) scn.regen_sgb();
  739. }
  740. void motion(int x, int y) // handles mouse drag effects
  741. {
  742. int dx = (int)(x-scn.mouse[0]);
  743. int dy = (int)(y-scn.mouse[1]);
  744. if( scn.mouse[2] == GLUT_MIDDLE_BUTTON ) { // controls color scale
  745. scn.color_scale[0] += dx/5.0; // brightness
  746. scn.color_scale[1] *= exp(-dy/200.0); // contrast
  747. }
  748. scn.mouse[0] = x;
  749. scn.mouse[1] = y;
  750. }
  751. const char *helptext[] = {
  752. " Simple Spectrogram: real-time OpenGL spectrogram.\n",
  753. " (based on Alex Barnett glSpect, Dec 2010 and Luke Campagnola glScope)\n\n",
  754. "Usage: sspect [-f] [-v] [-d <device_number>] [-sf <scroll_factor>] [-w <windowtype>] [-t twowinsize] [-fl freqline] [-c]\n\n",
  755. "Command line arguments:\n",
  756. "-f fullscreen\n",
  757. "-v verbose mode\n",
  758. "device_number = 0,1,... the ALSA input device number (default 0)\n",
  759. "windowtype = \t0 (no window)\n\t\t1 (Hann)\n\t\t2 (Gaussian trunc at +-4sigma) (default)\n",
  760. "scroll_factor = 1,2,... # vSyncs (60Hz) to wait per scroll pixel (default 1)\n",
  761. "twowinsize = 11,12,...,16 is power of 2 giving FFT win_size N (default 12)\n\t(Note: this controls the vertical frequency resolution and range)\n",
  762. "freqline draws at line at frequency freqline. Several -fl options can be used to draw several lines\n",
  763. "-c draws the centroid line\n\n",
  764. "Keys & mouse: \tarrows or middle button drag - brightness/contrast\n",
  765. "\t\tleft button shows horizontal frequency readoff line\n",
  766. "\t\tright button shows horizontal frequency readoff with multiples\n",
  767. "\t\ti - cycles through color maps (B/W, inverse B/W, color)\n",
  768. "\t\tq or Esc - quit\n",
  769. "\t\t[ and ] - control horizontal scroll factor (rate)\n",
  770. NULL };
  771. // ===========================================================================
  772. int main(int argc, char** argv)
  773. {
  774. int scrnmode = 0; // 0 for window, 1 fullscreen **Defaults go here**
  775. verb = 0; // 0 silent, 1 debug, etc
  776. scn.scroll_fac = 2; // how many vSyncs to wait before scrolling sg
  777. param.devicenumber = 0; // ALSA input device number
  778. param.windowtype = 2; // Gaussian
  779. param.twowinsize = 12; // 4096 samples
  780. param.drawcentroid = false;
  781. for (int i=1; i<argc; ++i) { // .....Parse cmd line options....
  782. if (!strcmp(argv[i], "-f")) // option -f makes full screen
  783. scrnmode = 1;
  784. else if (!strcmp(argv[i], "-v")) // option -v makes verbose
  785. verb = 1;
  786. else if (!strcmp(argv[i], "-sf")) {
  787. sscanf(argv[++i], "%d", &scn.scroll_fac); // read in scroll factor
  788. if (scn.scroll_fac<1) scn.scroll_fac=1; // sanitize it
  789. } else if (!strcmp(argv[i], "-t")) {
  790. sscanf(argv[++i], "%d", &param.twowinsize);
  791. if (param.twowinsize<10) param.twowinsize=10; // sanitize it
  792. if (param.twowinsize>18) param.twowinsize=18;
  793. } else if (!strcmp(argv[i], "-w")) {
  794. sscanf(argv[++i], "%d", &param.windowtype); // read in windowtype
  795. if (param.windowtype<0) param.windowtype=0; // sanitize it
  796. } else if (!strcmp(argv[i], "-d")) {
  797. sscanf(argv[++i], "%d", &param.devicenumber); // read in devicenumber
  798. if (param.devicenumber<0) param.devicenumber=0; // sanitize it
  799. } else if (!strcmp(argv[i], "-fl")) {
  800. sscanf(argv[++i], "%f", &param.freqline[param.nfreqlines]); // read in at which frequency to draw a line
  801. param.drawline = true;
  802. param.nfreqlines++;
  803. } else if (!strcmp(argv[i], "-c")) {
  804. param.drawcentroid = true;
  805. } else { // misuse (or -h): print out usage text...
  806. fprintf(stderr, "bad command line option %s\n\n", argv[i]);
  807. for (int i=0; helptext[i]; i++) fprintf(stderr, "%s", helptext[i]);
  808. exit(1);
  809. }
  810. }
  811. scn.init(); // true constructor for global scn object
  812. glutInit(&argc, argv);
  813. glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
  814. if (scrnmode==1) { // implement desired screen mode
  815. //glutGameModeString( "1680x1050:24@60" ); // res, pixel depth, refresh
  816. glutGameModeString( "1024x768:24@60" ); // for XGA projector
  817. glutEnterGameMode(); // start fullscreen game mode
  818. } else {
  819. glutInitWindowSize(1024, 768); // window same size as XGA
  820. int mainWindow = glutCreateWindow("sspect");
  821. // glutFullScreen(); // maximizes window, but is not game mode
  822. }
  823. glutDisplayFunc(display);
  824. glutReshapeFunc(reshape);
  825. glutKeyboardFunc(keyboard);
  826. glutSpecialFunc(special);
  827. glutMouseFunc(mouse);
  828. glutMotionFunc(motion);
  829. glutIdleFunc(idle);
  830. glutMainLoop(); // pass control to GLUT
  831. return 0;
  832. }