1 #include "config.h"
2 #include "util.h"
3 
4 #include <ctype.h>
5 #include <iniparser.h>
6 #include <math.h>
7 
8 #ifdef SNDIO
9 #include <sndio.h>
10 #endif
11 
12 #include <stdarg.h>
13 #include <stdbool.h>
14 #include <sys/stat.h>
15 
16 double smoothDef[5] = {1, 1, 1, 1, 1};
17 
18 enum input_method default_methods[] = {
19     INPUT_FIFO,
20     INPUT_PORTAUDIO,
21     INPUT_ALSA,
22     INPUT_PULSE,
23 };
24 
25 char *outputMethod, *channels, *xaxisScale;
26 
27 const char *input_method_names[] = {
28     "fifo", "portaudio", "alsa", "pulse", "sndio", "shmem",
29 };
30 
31 const bool has_input_method[] = {
32     true, /** Always have at least FIFO and shmem input. */
33     HAS_PORTAUDIO, HAS_ALSA, HAS_PULSE, HAS_SNDIO, true,
34 };
35 
input_method_by_name(const char * str)36 enum input_method input_method_by_name(const char *str) {
37     for (int i = 0; i < INPUT_MAX; i++) {
38         if (!strcmp(str, input_method_names[i])) {
39             return (enum input_method)i;
40         }
41     }
42 
43     return INPUT_MAX;
44 }
45 
write_errorf(void * err,const char * fmt,...)46 void write_errorf(void *err, const char *fmt, ...) {
47     struct error_s *error = (struct error_s *)err;
48     va_list args;
49     va_start(args, fmt);
50     error->length +=
51         vsnprintf((char *)error->message + error->length, MAX_ERROR_LEN - error->length, fmt, args);
52     va_end(args);
53 }
54 
validate_color(char * checkColor,void * params,void * err)55 int validate_color(char *checkColor, void *params, void *err) {
56     struct config_params *p = (struct config_params *)params;
57     struct error_s *error = (struct error_s *)err;
58     int validColor = 0;
59     if (checkColor[0] == '#' && strlen(checkColor) == 7) {
60         // If the output mode is not ncurses, tell the user to use a named colour instead of hex
61         // colours.
62         if (p->output != OUTPUT_NCURSES) {
63 #ifdef NCURSES
64             write_errorf(error,
65                          "hex color configured, but ncurses not set. Forcing ncurses mode.\n");
66             p->output = OUTPUT_NCURSES;
67 #else
68             write_errorf(error,
69                          "Only 'ncurses' output method supports HTML colors "
70                          "(required by gradient). "
71                          "Cava was built without ncurses support, install ncurses(w) dev files "
72                          "and rebuild.\n");
73             return 0;
74 #endif
75         }
76         // 0 to 9 and a to f
77         for (int i = 1; checkColor[i]; ++i) {
78             if (!isdigit(checkColor[i])) {
79                 if (tolower(checkColor[i]) >= 'a' && tolower(checkColor[i]) <= 'f') {
80                     validColor = 1;
81                 } else {
82                     validColor = 0;
83                     break;
84                 }
85             } else {
86                 validColor = 1;
87             }
88         }
89     } else {
90         if ((strcmp(checkColor, "black") == 0) || (strcmp(checkColor, "red") == 0) ||
91             (strcmp(checkColor, "green") == 0) || (strcmp(checkColor, "yellow") == 0) ||
92             (strcmp(checkColor, "blue") == 0) || (strcmp(checkColor, "magenta") == 0) ||
93             (strcmp(checkColor, "cyan") == 0) || (strcmp(checkColor, "white") == 0) ||
94             (strcmp(checkColor, "default") == 0))
95             validColor = 1;
96     }
97     return validColor;
98 }
99 
validate_colors(void * params,void * err)100 bool validate_colors(void *params, void *err) {
101     struct config_params *p = (struct config_params *)params;
102     struct error_s *error = (struct error_s *)err;
103 
104     // validate: color
105     if (!validate_color(p->color, p, error)) {
106         write_errorf(error, "The value for 'foreground' is invalid. It can be either one of the 7 "
107                             "named colors or a HTML color of the form '#xxxxxx'.\n");
108         return false;
109     }
110 
111     // validate: background color
112     if (!validate_color(p->bcolor, p, error)) {
113         write_errorf(error, "The value for 'background' is invalid. It can be either one of the 7 "
114                             "named colors or a HTML color of the form '#xxxxxx'.\n");
115         return false;
116     }
117 
118     if (p->gradient) {
119         for (int i = 0; i < p->gradient_count; i++) {
120             if (!validate_color(p->gradient_colors[i], p, error)) {
121                 write_errorf(
122                     error,
123                     "Gradient color %d is invalid. It must be HTML color of the form '#xxxxxx'.\n",
124                     i + 1);
125                 return false;
126             }
127         }
128     }
129 
130     // In case color is not html format set bgcol and col to predefinedint values
131     p->col = -1;
132     if (strcmp(p->color, "black") == 0)
133         p->col = 0;
134     if (strcmp(p->color, "red") == 0)
135         p->col = 1;
136     if (strcmp(p->color, "green") == 0)
137         p->col = 2;
138     if (strcmp(p->color, "yellow") == 0)
139         p->col = 3;
140     if (strcmp(p->color, "blue") == 0)
141         p->col = 4;
142     if (strcmp(p->color, "magenta") == 0)
143         p->col = 5;
144     if (strcmp(p->color, "cyan") == 0)
145         p->col = 6;
146     if (strcmp(p->color, "white") == 0)
147         p->col = 7;
148     // default if invalid
149 
150     // validate: background color
151     if (strcmp(p->bcolor, "black") == 0)
152         p->bgcol = 0;
153     if (strcmp(p->bcolor, "red") == 0)
154         p->bgcol = 1;
155     if (strcmp(p->bcolor, "green") == 0)
156         p->bgcol = 2;
157     if (strcmp(p->bcolor, "yellow") == 0)
158         p->bgcol = 3;
159     if (strcmp(p->bcolor, "blue") == 0)
160         p->bgcol = 4;
161     if (strcmp(p->bcolor, "magenta") == 0)
162         p->bgcol = 5;
163     if (strcmp(p->bcolor, "cyan") == 0)
164         p->bgcol = 6;
165     if (strcmp(p->bcolor, "white") == 0)
166         p->bgcol = 7;
167     // default if invalid
168 
169     return true;
170 }
171 
validate_config(struct config_params * p,struct error_s * error)172 bool validate_config(struct config_params *p, struct error_s *error) {
173     // validate: output method
174     p->output = OUTPUT_NOT_SUPORTED;
175     if (strcmp(outputMethod, "ncurses") == 0) {
176         p->output = OUTPUT_NCURSES;
177         p->bgcol = -1;
178 #ifndef NCURSES
179         write_errorf(error, "cava was built without ncurses support, install ncursesw dev files "
180                             "and run make clean && ./configure && make again\n");
181         return false;
182 #endif
183     }
184     if (strcmp(outputMethod, "raw") == 0) { // raw:
185         p->output = OUTPUT_RAW;
186         p->bar_spacing = 0;
187         p->bar_width = 1;
188 
189         // checking data format
190         p->is_bin = -1;
191         if (strcmp(p->data_format, "binary") == 0) {
192             p->is_bin = 1;
193             // checking bit format:
194             if (p->bit_format != 8 && p->bit_format != 16) {
195                 write_errorf(
196                     error,
197                     "bit format  %d is not supported, supported data formats are: '8' and '16'\n",
198                     p->bit_format);
199                 return false;
200             }
201         } else if (strcmp(p->data_format, "ascii") == 0) {
202             p->is_bin = 0;
203             if (p->ascii_range < 1) {
204                 write_errorf(error, "ascii max value must be a positive integer\n");
205                 return false;
206             }
207         } else {
208             write_errorf(error,
209                          "data format %s is not supported, supported data formats are: 'binary' "
210                          "and 'ascii'\n",
211                          p->data_format);
212             return false;
213         }
214     }
215     if (p->output == OUTPUT_NOT_SUPORTED) {
216 #ifndef NCURSES
217         write_errorf(
218             error,
219             "output method %s is not supported, supported methods are: 'raw'\n",
220             outputMethod);
221         return false;
222 #endif
223 
224 #ifdef NCURSES
225         write_errorf(error,
226                      "output method %s is not supported, supported methods are: 'ncurses', "
227                      "and 'raw'\n",
228                      outputMethod);
229         return false;
230 #endif
231     }
232 
233     p->xaxis = NONE;
234     if (strcmp(xaxisScale, "none") == 0) {
235         p->xaxis = NONE;
236     }
237     if (strcmp(xaxisScale, "frequency") == 0) {
238         p->xaxis = FREQUENCY;
239     }
240     if (strcmp(xaxisScale, "note") == 0) {
241         p->xaxis = NOTE;
242     }
243 
244     // validate: output channels
245     p->stereo = -1;
246     if (strcmp(channels, "mono") == 0) {
247         p->stereo = 0;
248         if (strcmp(p->mono_option, "average") != 0 && strcmp(p->mono_option, "left") != 0 &&
249             strcmp(p->mono_option, "right") != 0) {
250 
251             write_errorf(error,
252                          "mono option %s is not supported, supported options are: 'average', "
253                          "'left' or 'right'\n",
254                          p->mono_option);
255             return false;
256         }
257     }
258     if (strcmp(channels, "stereo") == 0)
259         p->stereo = 1;
260     if (p->stereo == -1) {
261         write_errorf(
262             error,
263             "output channels %s is not supported, supported channelss are: 'mono' and 'stereo'\n",
264             channels);
265         return false;
266     }
267 
268     // validate: bars
269     p->autobars = 1;
270     if (p->fixedbars > 0)
271         p->autobars = 0;
272     if (p->fixedbars > 256)
273         p->fixedbars = 256;
274     if (p->bar_width > 256)
275         p->bar_width = 256;
276     if (p->bar_width < 1)
277         p->bar_width = 1;
278 
279     // validate: framerate
280     if (p->framerate < 0) {
281         write_errorf(error, "framerate can't be negative!\n");
282         return false;
283     }
284 
285     // validate: colors
286     if (!validate_colors(p, error)) {
287         return false;
288     }
289 
290     // validate: gravity
291     p->gravity = p->gravity / 100;
292     if (p->gravity < 0) {
293         p->gravity = 0;
294     }
295 
296     // validate: integral
297     p->integral = p->integral / 100;
298     if (p->integral < 0) {
299         p->integral = 0;
300     } else if (p->integral > 1) {
301         p->integral = 1;
302     }
303 
304     // validate: cutoff
305     if (p->lower_cut_off == 0)
306         p->lower_cut_off++;
307     if (p->lower_cut_off > p->upper_cut_off) {
308         write_errorf(error,
309                      "lower cutoff frequency can't be higher than higher cutoff frequency\n");
310         return false;
311     }
312 
313     // setting sens
314     p->sens = p->sens / 100;
315 
316     return true;
317 }
318 
load_colors(struct config_params * p,dictionary * ini,void * err)319 bool load_colors(struct config_params *p, dictionary *ini, void *err) {
320     struct error_s *error = (struct error_s *)err;
321 
322     free(p->color);
323     free(p->bcolor);
324 
325     p->color = strdup(iniparser_getstring(ini, "color:foreground", "default"));
326     p->bcolor = strdup(iniparser_getstring(ini, "color:background", "default"));
327 
328     p->gradient = iniparser_getint(ini, "color:gradient", 0);
329     if (p->gradient) {
330         for (int i = 0; i < p->gradient_count; ++i) {
331             free(p->gradient_colors[i]);
332         }
333         p->gradient_count = iniparser_getint(ini, "color:gradient_count", 8);
334         if (p->gradient_count < 2) {
335             write_errorf(error, "\nAtleast two colors must be given as gradient!\n");
336             return false;
337         }
338         if (p->gradient_count > 8) {
339             write_errorf(error, "\nMaximum 8 colors can be specified as gradient!\n");
340             return false;
341         }
342         p->gradient_colors = (char **)malloc(sizeof(char *) * p->gradient_count * 9);
343         p->gradient_colors[0] =
344             strdup(iniparser_getstring(ini, "color:gradient_color_1", "#59cc33"));
345         p->gradient_colors[1] =
346             strdup(iniparser_getstring(ini, "color:gradient_color_2", "#80cc33"));
347         p->gradient_colors[2] =
348             strdup(iniparser_getstring(ini, "color:gradient_color_3", "#a6cc33"));
349         p->gradient_colors[3] =
350             strdup(iniparser_getstring(ini, "color:gradient_color_4", "#cccc33"));
351         p->gradient_colors[4] =
352             strdup(iniparser_getstring(ini, "color:gradient_color_5", "#cca633"));
353         p->gradient_colors[5] =
354             strdup(iniparser_getstring(ini, "color:gradient_color_6", "#cc8033"));
355         p->gradient_colors[6] =
356             strdup(iniparser_getstring(ini, "color:gradient_color_7", "#cc5933"));
357         p->gradient_colors[7] =
358             strdup(iniparser_getstring(ini, "color:gradient_color_8", "#cc3333"));
359     }
360     return true;
361 }
362 
load_config(char configPath[PATH_MAX],struct config_params * p,bool colorsOnly,struct error_s * error)363 bool load_config(char configPath[PATH_MAX], struct config_params *p, bool colorsOnly,
364                  struct error_s *error) {
365     FILE *fp;
366 
367     // config: creating path to default config file
368     if (configPath[0] == '\0') {
369         char *configFile = "config";
370         char *configHome = getenv("XDG_CONFIG_HOME");
371         if (configHome != NULL) {
372             sprintf(configPath, "%s/%s/", configHome, PACKAGE);
373         } else {
374             configHome = getenv("HOME");
375             if (configHome != NULL) {
376                 sprintf(configPath, "%s/%s/", configHome, ".config");
377                 mkdir(configPath, 0777);
378                 sprintf(configPath, "%s/%s/%s/", configHome, ".config", PACKAGE);
379             } else {
380                 write_errorf(error, "No HOME found (ERR_HOMELESS), exiting...");
381                 return false;
382             }
383         }
384 
385         // config: create directory
386         mkdir(configPath, 0777);
387 
388         // config: adding default filename file
389         strcat(configPath, configFile);
390 
391         // open file or create file if it does not exist
392         fp = fopen(configPath, "ab+");
393         if (fp) {
394             fclose(fp);
395         } else {
396             // try to open file read only
397             fp = fopen(configPath, "rb");
398             if (fp) {
399                 fclose(fp);
400             } else {
401                 write_errorf(error, "Unable to open or create file '%s', exiting...\n", configPath);
402                 return false;
403             }
404         }
405 
406     } else { // opening specified file
407 
408         fp = fopen(configPath, "rb");
409         if (fp) {
410             fclose(fp);
411         } else {
412             write_errorf(error, "Unable to open file '%s', exiting...\n", configPath);
413             return false;
414         }
415     }
416 
417     // config: parse ini
418     dictionary *ini;
419     ini = iniparser_load(configPath);
420 
421     if (colorsOnly) {
422         if (!load_colors(p, ini, error)) {
423             return false;
424         }
425         return validate_colors(p, error);
426     }
427 
428 #ifdef NCURSES
429     outputMethod = (char *)iniparser_getstring(ini, "output:method", "ncurses");
430 #endif
431 #ifndef NCURSES
432     outputMethod = (char *)iniparser_getstring(ini, "output:method", "noncurses");
433 #endif
434 
435     xaxisScale = (char *)iniparser_getstring(ini, "output:xaxis", "none");
436     p->monstercat = 1.5 * iniparser_getdouble(ini, "smoothing:monstercat", 0);
437     p->waves = iniparser_getint(ini, "smoothing:waves", 0);
438     p->integral = iniparser_getdouble(ini, "smoothing:integral", 77);
439     p->gravity = iniparser_getdouble(ini, "smoothing:gravity", 100);
440     p->ignore = iniparser_getdouble(ini, "smoothing:ignore", 0);
441 
442     if (!load_colors(p, ini, error)) {
443         return false;
444     }
445 
446     p->fixedbars = iniparser_getint(ini, "general:bars", 0);
447     p->bar_width = iniparser_getint(ini, "general:bar_width", 2);
448     p->bar_spacing = iniparser_getint(ini, "general:bar_spacing", 1);
449     p->framerate = iniparser_getint(ini, "general:framerate", 60);
450     p->sens = iniparser_getint(ini, "general:sensitivity", 100);
451     p->autosens = iniparser_getint(ini, "general:autosens", 1);
452     p->overshoot = iniparser_getint(ini, "general:overshoot", 20);
453     p->lower_cut_off = iniparser_getint(ini, "general:lower_cutoff_freq", 50);
454     p->upper_cut_off = iniparser_getint(ini, "general:higher_cutoff_freq", 10000);
455     p->sleep_timer = iniparser_getint(ini, "general:sleep_timer", 0);
456 
457     // config: output
458     free(channels);
459     free(p->mono_option);
460     free(p->raw_target);
461     free(p->data_format);
462 
463     channels = strdup(iniparser_getstring(ini, "output:channels", "stereo"));
464     p->mono_option = strdup(iniparser_getstring(ini, "output:mono_option", "average"));
465     p->raw_target = strdup(iniparser_getstring(ini, "output:raw_target", "/dev/stdout"));
466     p->data_format = strdup(iniparser_getstring(ini, "output:data_format", "binary"));
467     p->bar_delim = (char)iniparser_getint(ini, "output:bar_delimiter", 59);
468     p->frame_delim = (char)iniparser_getint(ini, "output:frame_delimiter", 10);
469     p->ascii_range = iniparser_getint(ini, "output:ascii_max_range", 1000);
470     p->bit_format = iniparser_getint(ini, "output:bit_format", 16);
471 
472     // read & validate: eq
473     p->userEQ_keys = iniparser_getsecnkeys(ini, "eq");
474     if (p->userEQ_keys > 0) {
475         p->userEQ_enabled = 1;
476         p->userEQ = (double *)calloc(p->userEQ_keys + 1, sizeof(double));
477 #ifndef LEGACYINIPARSER
478         const char *keys[p->userEQ_keys];
479         iniparser_getseckeys(ini, "eq", keys);
480 #endif
481 #ifdef LEGACYINIPARSER
482         char **keys = iniparser_getseckeys(ini, "eq");
483 #endif
484         for (int sk = 0; sk < p->userEQ_keys; sk++) {
485             p->userEQ[sk] = iniparser_getdouble(ini, keys[sk], 1);
486         }
487     } else {
488         p->userEQ_enabled = 0;
489     }
490 
491     free(p->audio_source);
492 
493     char *input_method_name;
494     for (size_t i = 0; i < ARRAY_SIZE(default_methods); i++) {
495         enum input_method method = default_methods[i];
496         if (has_input_method[method]) {
497             input_method_name =
498                 (char *)iniparser_getstring(ini, "input:method", input_method_names[method]);
499         }
500     }
501 
502     p->input = input_method_by_name(input_method_name);
503     switch (p->input) {
504 #ifdef ALSA
505     case INPUT_ALSA:
506         p->audio_source = strdup(iniparser_getstring(ini, "input:source", "hw:Loopback,1"));
507         break;
508 #endif
509     case INPUT_FIFO:
510         p->audio_source = strdup(iniparser_getstring(ini, "input:source", "/tmp/mpd.fifo"));
511         p->fifoSample = iniparser_getint(ini, "input:sample_rate", 44100);
512         p->fifoSampleBits = iniparser_getint(ini, "input:sample_bits", 16);
513         break;
514 #ifdef PULSE
515     case INPUT_PULSE:
516         p->audio_source = strdup(iniparser_getstring(ini, "input:source", "auto"));
517         break;
518 #endif
519 #ifdef SNDIO
520     case INPUT_SNDIO:
521         p->audio_source = strdup(iniparser_getstring(ini, "input:source", SIO_DEVANY));
522         break;
523 #endif
524     case INPUT_SHMEM:
525         p->audio_source =
526             strdup(iniparser_getstring(ini, "input:source", "/squeezelite-00:00:00:00:00:00"));
527         break;
528 #ifdef PORTAUDIO
529     case INPUT_PORTAUDIO:
530         p->audio_source = strdup(iniparser_getstring(ini, "input:source", "auto"));
531         break;
532 #endif
533     case INPUT_MAX: {
534         char supported_methods[255] = "";
535         for (int i = 0; i < INPUT_MAX; i++) {
536             if (has_input_method[i]) {
537                 strcat(supported_methods, "'");
538                 strcat(supported_methods, input_method_names[i]);
539                 strcat(supported_methods, "' ");
540             }
541         }
542         write_errorf(error, "input method '%s' is not supported, supported methods are: %s\n",
543                      input_method_name, supported_methods);
544         return false;
545     }
546     default:
547         write_errorf(error, "cava was built without '%s' input support\n",
548                      input_method_names[p->input]);
549         return false;
550     }
551 
552     bool result = validate_config(p, error);
553     iniparser_freedict(ini);
554     return result;
555 }
556