From ade9aa3db8d706fadbc161694cdd2579a3ef4382 Mon Sep 17 00:00:00 2001 From: Albert Graef Date: Wed, 15 Aug 2018 00:06:08 +0200 Subject: [PATCH] Optimize memory usage of the translation tables, using sorted arrays and binary search. Still runs reasonably fast, and memory usage is much lower know (in the KB range). --- midizap.c | 184 +++++++++++++++++++++++++++++++++++----------- midizap.h | 34 +++++---- readconfig.c | 204 +++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 320 insertions(+), 102 deletions(-) diff --git a/midizap.c b/midizap.c index 248a5b3..8c09620 100644 --- a/midizap.c +++ b/midizap.c @@ -151,26 +151,112 @@ send_midi(uint8_t portno, int status, int data, int step, int incr, int index, i queue_midi(&seq, msg, portno); } +static int stroke_data_cmp(const void *a, const void *b) +{ + const stroke_data *ad = (const stroke_data*)a; + const stroke_data *bd = (const stroke_data*)b; + if (ad->chan == bd->chan) + return ad->data - bd->data; + else + return ad->chan - bd->chan; +} + +static stroke *find_stroke_data(stroke_data *sd, + int chan, int data, int index, + int *step, int *incr, + uint16_t n) +{ + if (n < 16) { + // Linear search is presumably faster for small arrays, and we also avoid + // function calls for doing the comparisons here. Not sure where it breaks + // even with glibc's bsearch(), though (TODO: measure). + uint16_t i; + for (i = 0; i < n; i++) { + if (sd[i].chan == chan && sd[i].data == data) { + if (step) *step = sd[i].step[index]; + if (incr) *incr = sd[i].is_incr; + return sd[i].s[index]; + } else if (sd[i].chan > chan || + (sd[i].chan == chan && sd[i].data > data)) + return NULL; + } + return NULL; + } else { + // binary search from libc + stroke_data *ret, key; + key.chan = chan; key.data = data; + ret = bsearch(&key, sd, n, sizeof(stroke_data), stroke_data_cmp); + if (ret) { + if (step) *step = ret->step[index]; + if (incr) *incr = ret->is_incr; + return ret->s[index]; + } else + return NULL; + } +} + +static stroke *find_note(translation *tr, int shift, + int chan, int data, int index) +{ + return find_stroke_data(tr->note[shift], chan, data, index, 0, 0, + tr->n_note[shift]); +} + +static stroke *find_pc(translation *tr, int shift, + int chan, int data, int index) +{ + return find_stroke_data(tr->pc[shift], chan, data, index, 0, 0, + tr->n_pc[shift]); +} + +static stroke *find_cc(translation *tr, int shift, + int chan, int data, int index) +{ + return find_stroke_data(tr->cc[shift], chan, data, index, 0, 0, + tr->n_cc[shift]); +} + +static stroke *find_ccs(translation *tr, int shift, + int chan, int data, int index, int *step, int *incr) +{ + return find_stroke_data(tr->ccs[shift], chan, data, index, step, incr, + tr->n_ccs[shift]); +} + +static stroke *find_pb(translation *tr, int shift, + int chan, int index) +{ + return find_stroke_data(tr->pb[shift], chan, 0, index, 0, 0, + tr->n_pb[shift]); +} + +static stroke *find_pbs(translation *tr, int shift, + int chan, int index, int *step) +{ + return find_stroke_data(tr->pbs[shift], chan, 0, index, step, 0, + tr->n_pbs[shift]); +} + stroke * fetch_stroke(translation *tr, uint8_t portno, int status, int chan, int data, - int index, int dir) + int index, int dir, int *step, int *incr) { if (tr && tr->portno == portno) { switch (status) { case 0x90: - return tr->note[shift][chan][data][index]; + return find_note(tr, shift, chan, data, index); case 0xc0: - return tr->pc[shift][chan][data][index]; + return find_pc(tr, shift, chan, data, index); case 0xb0: if (dir) - return tr->ccs[shift][chan][data][dir>0]; + return find_ccs(tr, shift, chan, data, dir>0, step, incr); else - return tr->cc[shift][chan][data][index]; + return find_cc(tr, shift, chan, data, index); case 0xe0: if (dir) - return tr->pbs[shift][chan][dir>0]; + return find_pbs(tr, shift, chan, dir>0, step); else - return tr->pb[shift][chan][index]; + return find_pb(tr, shift, chan, index); default: return NULL; } @@ -221,10 +307,11 @@ static char *debug_key(translation *tr, char *name, data / 12 + midi_octave, chan+1); break; case 0xb0: { - int step = tr->cc_step[shift][chan][data][dir>0]; + int step = 1, is_incr = 0; + if (tr) (void)find_ccs(tr, shift, chan, data, dir>0, &step, &is_incr); if (!dir) suffix = ""; - else if (tr->is_incr[shift][chan][data]) + else if (is_incr) suffix = (dir<0)?"<":">"; else suffix = (dir<0)?"-":"+"; @@ -238,7 +325,8 @@ static char *debug_key(translation *tr, char *name, sprintf(name, "%sPC%d-%d", prefix, data, chan+1); break; case 0xe0: { - int step = tr->pb_step[shift][chan][dir>0]; + int step = 1; + if (tr) (void)find_pbs(tr, shift, chan, dir>0, &step); if (!dir) suffix = ""; else @@ -255,7 +343,7 @@ static char *debug_key(translation *tr, char *name, return name; } -static void debug_input(translation *tr, int portno, +static void debug_input(int portno, int status, int chan, int data, int data2) { char name[100]; @@ -264,10 +352,10 @@ static void debug_input(translation *tr, int portno, data2 = ((data2 << 7) | data) - 8192; if (status == 0xc0) printf("[%d] %s\n", portno, - debug_key(tr, name, status, chan, data, 0)); + debug_key(0, name, status, chan, data, 0)); else printf("[%d] %s value = %d\n", portno, - debug_key(tr, name, status, chan, data, 0), data2); + debug_key(0, name, status, chan, data, 0), data2); } // Some machinery to handle the debugging of section matches. This is @@ -300,21 +388,23 @@ void send_strokes(translation *tr, uint8_t portno, int status, int chan, int data, int index, int dir) { - int nkeys = 0; - stroke *s = fetch_stroke(tr, portno, status, chan, data, index, dir); + int nkeys = 0, step = 0, is_incr = 0; + stroke *s = fetch_stroke(tr, portno, status, chan, data, index, dir, + &step, &is_incr); // If there's no press/release translation, check whether we have got at // least the corresponding release/press translation, in order to prevent // spurious error messages if either the press or release translation just // happens to be empty. int chk = s || - (!dir && fetch_stroke(tr, portno, status, chan, data, !index, dir)); + (!dir && fetch_stroke(tr, portno, status, chan, data, !index, dir, 0, 0)); if (!s && jack_num_outputs) { // fall back to default MIDI translation tr = default_midi_translation[portno]; - s = fetch_stroke(tr, portno, status, chan, data, index, dir); + s = fetch_stroke(tr, portno, status, chan, data, index, dir, + &step, &is_incr); chk = chk || s || - (!dir && fetch_stroke(tr, portno, status, chan, data, !index, dir)); + (!dir && fetch_stroke(tr, portno, status, chan, data, !index, dir, 0, 0)); // Ignore all MIDI input on the second port if no translation was found in // the [MIDI2] section (or the section is missing altogether). if (portno && !s) return; @@ -323,9 +413,10 @@ send_strokes(translation *tr, uint8_t portno, int status, int chan, int data, if (!s) { // fall back to the default translation tr = default_translation; - s = fetch_stroke(tr, portno, status, chan, data, index, dir); + s = fetch_stroke(tr, portno, status, chan, data, index, dir, + &step, &is_incr); chk = chk || s || - (!dir && fetch_stroke(tr, portno, status, chan, data, !index, dir)); + (!dir && fetch_stroke(tr, portno, status, chan, data, !index, dir, 0, 0)); } if (debug_regex) { @@ -486,34 +577,39 @@ static uint8_t inpbdown[2][16]; int check_incr(translation *tr, uint8_t portno, int chan, int data) { + int is_incr; if (tr && tr->portno == portno && - (tr->ccs[shift][chan][data][0] || tr->ccs[shift][chan][data][1])) - return tr->is_incr[shift][chan][data]; + (find_ccs(tr, shift, chan, data, 0, 0, &is_incr) || + find_ccs(tr, shift, chan, data, 1, 0, &is_incr))) + return is_incr; tr = default_midi_translation[portno]; if (tr && tr->portno == portno && - (tr->ccs[shift][chan][data][0] || tr->ccs[shift][chan][data][1])) - return tr->is_incr[shift][chan][data]; + (find_ccs(tr, shift, chan, data, 0, 0, &is_incr) || + find_ccs(tr, shift, chan, data, 1, 0, &is_incr))) + return is_incr; tr = default_translation; if (tr && tr->portno == portno && - (tr->ccs[shift][chan][data][0] || tr->ccs[shift][chan][data][1])) - return tr->is_incr[shift][chan][data]; + (find_ccs(tr, shift, chan, data, 0, 0, &is_incr) || + find_ccs(tr, shift, chan, data, 1, 0, &is_incr))) + return is_incr; return 0; } int get_cc_step(translation *tr, uint8_t portno, int chan, int data, int dir) { + int step; if (tr && tr->portno == portno && - tr->ccs[shift][chan][data][dir>0]) - return tr->cc_step[shift][chan][data][dir>0]; + find_ccs(tr, shift, chan, data, dir>0, &step, 0)) + return step; tr = default_midi_translation[portno]; if (tr && tr->portno == portno && - tr->ccs[shift][chan][data][dir>0]) - return tr->cc_step[shift][chan][data][dir>0]; + find_ccs(tr, shift, chan, data, dir>0, &step, 0)) + return step; tr = default_translation; if (tr && tr->portno == portno && - tr->ccs[shift][chan][data][dir>0]) - return tr->cc_step[shift][chan][data][dir>0]; + find_ccs(tr, shift, chan, data, dir>0, &step, 0)) + return step; return 1; } @@ -521,15 +617,18 @@ int check_pbs(translation *tr, uint8_t portno, int chan) { if (tr && tr->portno == portno && - (tr->pbs[shift][chan][0] || tr->pbs[shift][chan][1])) + (find_pbs(tr, shift, chan, 0, 0) || + find_pbs(tr, shift, chan, 1, 0))) return 1; tr = default_midi_translation[portno]; if (tr && tr->portno == portno && - (tr->pbs[shift][chan][0] || tr->pbs[shift][chan][1])) + (find_pbs(tr, shift, chan, 0, 0) || + find_pbs(tr, shift, chan, 1, 0))) return 1; tr = default_translation; if (tr && tr->portno == portno && - (tr->pbs[shift][chan][0] || tr->pbs[shift][chan][1])) + (find_pbs(tr, shift, chan, 0, 0) || + find_pbs(tr, shift, chan, 1, 0))) return 1; return 0; } @@ -537,17 +636,18 @@ check_pbs(translation *tr, uint8_t portno, int chan) int get_pb_step(translation *tr, uint8_t portno, int chan, int dir) { + int step; if (tr && tr->portno == portno && - tr->pbs[shift][chan][dir>0]) - return tr->pb_step[shift][chan][dir>0]; + find_pbs(tr, shift, chan, dir>0, &step)) + return step; tr = default_midi_translation[portno]; if (tr && tr->portno == portno && - tr->pbs[shift][chan][dir>0]) - return tr->pb_step[shift][chan][dir>0]; + find_pbs(tr, shift, chan, dir>0, &step)) + return step; tr = default_translation; if (tr && tr->portno == portno && - tr->pbs[shift][chan][dir>0]) - return tr->pb_step[shift][chan][dir>0]; + find_pbs(tr, shift, chan, dir>0, &step)) + return step; return 1; } @@ -563,7 +663,7 @@ handle_event(uint8_t *msg, uint8_t portno) msg[0] = status | chan; msg[2] = 0; } - if (debug_midi) debug_input(tr, portno, status, chan, msg[1], msg[2]); + if (debug_midi) debug_input(portno, status, chan, msg[1], msg[2]); switch (status) { case 0xc0: start_debug(); diff --git a/midizap.h b/midizap.h index a0c486c..59ced22 100644 --- a/midizap.h +++ b/midizap.h @@ -64,25 +64,33 @@ typedef struct _stroke { #define NUM_KEYS 128 #define NUM_CHAN 16 +typedef struct _stroke_data { + // key (MIDI channel and, for note/CC/PB, data byte) + uint8_t chan, data; + // stroke data, indexed by press/release or up/down index + stroke *s[2]; + // step size (CC and PB only) + int step[2]; + // incr flag (CC only) + uint8_t is_incr; +} stroke_data; + typedef struct _translation { struct _translation *next; char *name; int is_default; regex_t regex; uint8_t portno; - // XXXFIXME: This way of storing the translation tables is easy to - // construct, but wastes quite a lot of memory. We should rather use some - // kind of dictionary here. - uint8_t is_incr[2][NUM_CHAN][NUM_KEYS]; - stroke *pc[2][NUM_CHAN][NUM_KEYS][2]; - stroke *note[2][NUM_CHAN][NUM_KEYS][2]; - stroke *cc[2][NUM_CHAN][NUM_KEYS][2]; - stroke *ccs[2][NUM_CHAN][NUM_KEYS][2]; - stroke *pb[2][NUM_CHAN][2]; - stroke *pbs[2][NUM_CHAN][2]; - // step size for control changes and pitch bend (1 by default) - int cc_step[2][NUM_CHAN][NUM_KEYS][2]; - int pb_step[2][NUM_CHAN][2]; + // these are indexed by shift status + stroke_data *note[2]; + stroke_data *pc[2]; + stroke_data *cc[2]; + stroke_data *ccs[2]; + stroke_data *pb[2]; + stroke_data *pbs[2]; + // actual and allocated sizes (can be at most 16*128) + uint16_t n_note[2], n_pc[2], n_cc[2], n_ccs[2], n_pb[2], n_pbs[2]; + uint16_t a_note[2], a_pc[2], a_cc[2], a_ccs[2], a_pb[2], a_pbs[2]; } translation; extern void reload_callback(void); diff --git a/readconfig.c b/readconfig.c index 4abc4f9..a15f7d1 100644 --- a/readconfig.c +++ b/readconfig.c @@ -371,10 +371,129 @@ free_strokes(stroke *s) } } +static int stroke_data_cmp(const void *a, const void *b) +{ + const stroke_data *ad = (const stroke_data*)a; + const stroke_data *bd = (const stroke_data*)b; + if (ad->chan == bd->chan) + return ad->data - bd->data; + else + return ad->chan - bd->chan; +} + +static void finish_stroke_data(stroke_data **sd, + uint16_t *n, uint16_t *a) +{ + if (*a && *a > *n) { + // realloc to needed size + *sd = realloc(*sd, (*n)*sizeof(stroke_data)); + *a = *n; + } + // sort by chan/data for faster access + qsort(*sd, *n, sizeof(stroke_data), stroke_data_cmp); +} + +static void free_stroke_data(stroke_data *sd, uint16_t n) +{ + uint16_t i; + for (i = 0; i < n; i++) { + free_strokes(sd[i].s[0]); + free_strokes(sd[i].s[1]); + } + free(sd); +} + +static stroke **find_stroke_data(stroke_data **sd, + int chan, int data, int index, + int step, int incr, + uint16_t *n, uint16_t *a) +{ + uint16_t i; + for (i = 0; i < *n; i++) { + if ((*sd)[i].chan == chan && (*sd)[i].data == data) { + // existing entry + (*sd)[i].step[index] = step; + (*sd)[i].is_incr = incr; + return &(*sd)[i].s[index]; + } + } + // add a new entry + if (*n >= *a) { + // make some room + *a = (*a)?2*(*a):8; + *sd = realloc(*sd, (*a)*sizeof(stroke_data)); + } + memset(&(*sd)[*n], 0, sizeof(stroke_data)); + (*sd)[*n].chan = chan; + (*sd)[*n].data = data; + (*sd)[*n].step[index] = step; + (*sd)[*n].is_incr = incr; + return &(*sd)[(*n)++].s[index]; +} + +static stroke **find_note(translation *tr, int shift, + int chan, int data, int index) +{ + return find_stroke_data(&tr->note[shift], chan, data, index, 0, 0, + &tr->n_note[shift], &tr->a_note[shift]); +} + +static stroke **find_pc(translation *tr, int shift, + int chan, int data, int index) +{ + return find_stroke_data(&tr->pc[shift], chan, data, index, 0, 0, + &tr->n_pc[shift], &tr->a_pc[shift]); +} + +static stroke **find_cc(translation *tr, int shift, + int chan, int data, int index) +{ + return find_stroke_data(&tr->cc[shift], chan, data, index, 0, 0, + &tr->n_cc[shift], &tr->a_cc[shift]); +} + +static stroke **find_ccs(translation *tr, int shift, + int chan, int data, int index, int step, int incr) +{ + return find_stroke_data(&tr->ccs[shift], chan, data, index, step, incr, + &tr->n_ccs[shift], &tr->a_ccs[shift]); +} + +static stroke **find_pb(translation *tr, int shift, + int chan, int index) +{ + return find_stroke_data(&tr->pb[shift], chan, 0, index, 0, 0, + &tr->n_pb[shift], &tr->a_pb[shift]); +} + +static stroke **find_pbs(translation *tr, int shift, + int chan, int index, int step) +{ + return find_stroke_data(&tr->pbs[shift], chan, 0, index, step, 0, + &tr->n_pbs[shift], &tr->a_pbs[shift]); +} + +void +finish_translation_section(translation *tr) +{ + int k; + + if (tr) { + for (k=0; k<2; k++) { + finish_stroke_data(&tr->pc[k], &tr->n_pc[k], &tr->a_pc[k]); + finish_stroke_data(&tr->note[k], &tr->n_note[k], &tr->a_note[k]); + finish_stroke_data(&tr->cc[k], &tr->n_cc[k], &tr->a_cc[k]); + finish_stroke_data(&tr->ccs[k], &tr->n_ccs[k], &tr->a_ccs[k]); + finish_stroke_data(&tr->pb[k], &tr->n_pb[k], &tr->a_pb[k]); + finish_stroke_data(&tr->pbs[k], &tr->n_pbs[k], &tr->a_pbs[k]); + } + } +} + void free_translation_section(translation *tr) { - int i, j, k; + int k; if (tr != NULL) { free(tr->name); @@ -382,22 +501,12 @@ free_translation_section(translation *tr) regfree(&tr->regex); } for (k=0; k<2; k++) { - for (i=0; ipc[k][i][j][0]); - free_strokes(tr->pc[k][i][j][1]); - free_strokes(tr->note[k][i][j][0]); - free_strokes(tr->note[k][i][j][1]); - free_strokes(tr->cc[k][i][j][0]); - free_strokes(tr->cc[k][i][j][1]); - free_strokes(tr->ccs[k][i][j][0]); - free_strokes(tr->ccs[k][i][j][1]); - } - free_strokes(tr->pb[k][i][0]); - free_strokes(tr->pb[k][i][1]); - free_strokes(tr->pbs[k][i][0]); - free_strokes(tr->pbs[k][i][1]); - } + free_stroke_data(tr->pc[k], tr->n_pc[k]); + free_stroke_data(tr->note[k], tr->n_note[k]); + free_stroke_data(tr->cc[k], tr->n_cc[k]); + free_stroke_data(tr->ccs[k], tr->n_ccs[k]); + free_stroke_data(tr->pb[k], tr->n_pb[k]); + free_stroke_data(tr->pbs[k], tr->n_pbs[k]); } free(tr); } @@ -934,11 +1043,11 @@ start_translation(translation *tr, char *which_key) switch (status & 0xf0) { case 0x90: // note on/off - first_stroke = &(tr->note[k][chan][data][0]); - release_first_stroke = &(tr->note[k][chan][data][1]); + first_stroke = find_note(tr, k, chan, data, 0); + release_first_stroke = find_note(tr, k, chan, data, 1); if (is_anyshift) { - alt_press_stroke = &(tr->note[0][chan][data][0]); - alt_release_stroke = &(tr->note[0][chan][data][1]); + alt_press_stroke = find_note(tr, 0, chan, data, 0); + alt_release_stroke = find_note(tr, 0, chan, data, 1); } is_keystroke = 1; break; @@ -948,32 +1057,34 @@ start_translation(translation *tr, char *which_key) // this message has no off state. Thus, when we receive a pc, it's // supposed to be treated as a "press" sequence immediately followed by // the corresponding "release" sequence. - first_stroke = &(tr->pc[k][chan][data][0]); - release_first_stroke = &(tr->pc[k][chan][data][1]); + first_stroke = find_pc(tr, k, chan, data, 0); + release_first_stroke = find_pc(tr, k, chan, data, 1); if (is_anyshift) { - alt_press_stroke = &(tr->pc[0][chan][data][0]); - alt_release_stroke = &(tr->pc[0][chan][data][1]); + alt_press_stroke = find_pc(tr, 0, chan, data, 0); + alt_release_stroke = find_pc(tr, 0, chan, data, 1); } is_keystroke = 1; break; case 0xb0: if (!incr) { // cc on/off - first_stroke = &(tr->cc[k][chan][data][0]); - release_first_stroke = &(tr->cc[k][chan][data][1]); + first_stroke = find_cc(tr, k, chan, data, 0); + release_first_stroke = find_cc(tr, k, chan, data, 1); if (is_anyshift) { - alt_press_stroke = &(tr->cc[0][chan][data][0]); - alt_release_stroke = &(tr->cc[0][chan][data][1]); + alt_press_stroke = find_cc(tr, 0, chan, data, 0); + alt_release_stroke = find_cc(tr, 0, chan, data, 1); } is_keystroke = 1; } else { // cc (step up, down) - tr->is_incr[k][chan][data] = incr>1; - first_stroke = &(tr->ccs[k][chan][data][dir>0]); - if (is_anyshift) { - alt_press_stroke = &(tr->ccs[0][chan][data][0]); + if (step <= 0) { + fprintf(stderr, "zero or negative step size not permitted here: [%s]%s\n", current_translation, which_key); + return 1; + } + first_stroke = find_ccs(tr, k, chan, data, dir>0, step, incr>1); + if (is_anyshift) { + alt_press_stroke = find_ccs(tr, 0, chan, data, dir>0, step, incr>1); } - tr->cc_step[k][chan][data][dir>0] = step; if (!dir) { // This is a bidirectional translation (=, ~). We first fill in the // "down" part (pointed to by first_stroke). When finishing off the @@ -983,22 +1094,21 @@ start_translation(translation *tr, char *which_key) // translation, here to remember the "up" part of the translation, // so that we can fill in that part later. is_bidirectional = 1; - release_first_stroke = &(tr->ccs[k][chan][data][1]); + release_first_stroke = find_ccs(tr, k, chan, data, 1, step, incr>1); if (is_anyshift) { - alt_release_stroke = &(tr->ccs[0][chan][data][1]); + alt_release_stroke = find_ccs(tr, 0, chan, data, 1, step, incr>1); } - tr->cc_step[k][chan][data][1] = step; } } break; case 0xe0: if (!incr) { // pb on/off - first_stroke = &(tr->pb[k][chan][0]); - release_first_stroke = &(tr->pb[k][chan][1]); + first_stroke = find_pb(tr, k, chan, 0); + release_first_stroke = find_pb(tr, k, chan, 1); if (is_anyshift) { - alt_press_stroke = &(tr->pb[0][chan][0]); - alt_release_stroke = &(tr->pb[0][chan][1]); + alt_press_stroke = find_pb(tr, 0, chan, 0); + alt_release_stroke = find_pb(tr, 0, chan, 1); } is_keystroke = 1; } else { @@ -1007,18 +1117,16 @@ start_translation(translation *tr, char *which_key) fprintf(stderr, "zero or negative step size not permitted here: [%s]%s\n", current_translation, which_key); return 1; } - first_stroke = &(tr->pbs[k][chan][dir>0]); + first_stroke = find_pbs(tr, k, chan, dir>0, step); if (is_anyshift) { - alt_press_stroke = &(tr->pbs[0][chan][0]); + alt_press_stroke = find_pbs(tr, 0, chan, dir>0, step); } - tr->pb_step[k][chan][dir>0] = step; if (!dir) { is_bidirectional = 1; - release_first_stroke = &(tr->pbs[k][chan][1]); + release_first_stroke = find_pbs(tr, k, chan, 1, step); if (is_anyshift) { - alt_release_stroke = &(tr->pbs[0][chan][1]); + alt_release_stroke = find_pbs(tr, 0, chan, 1, step); } - tr->pb_step[k][chan][1] = step; } } break; @@ -1324,6 +1432,7 @@ read_config_file(void) } s[1] = '\0'; } + finish_translation_section(tr); tr = new_translation_section(name, regex); continue; } @@ -1432,6 +1541,7 @@ read_config_file(void) } finish_translation(); } + finish_translation_section(tr); fclose(f); return 1;