lorid

convert chordpro to pdf
git clone git://git.relim.de/lorid.git
Log | Files | Refs | README | LICENSE

commit 79a5a9893f502100d29ea2dac399a4115a543b91
parent f5cf40d7f52df07ce54f7cb9c1b4bac77732a7d6
Author: nibo <nibo@relim.de>
Date:   Tue,  6 Aug 2024 08:13:56 +0200

Parse chords

Not finished.

Diffstat:
Mchordpro.c | 211+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mchordpro.h | 15++++++++++++++-
Mconfig.c | 231++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mconfig.h | 25+++++++++++++++++++++++++
Mlorid.c | 4++--
Mout_pdf.c | 4++--
Mtodo | 1-
Mutil.c | 14++++++++++++++
Mutil.h | 1+
9 files changed, 476 insertions(+), 30 deletions(-)

diff --git a/chordpro.c b/chordpro.c @@ -65,6 +65,78 @@ struct StyleProperty default_style_properties[18] = { { SF_TITLE, SPT_COLOR, { .foreground_color = NULL } }, }; +static const char *chord_extensions_major[] = { + "2", "3", "4", "5", "6", "69", "7", "7-5", + "7#5", "7#9", "7#9#5", "7#9b5", "7#9#11", + "7b5", "7b9", "7b9#5", "7b9#9", "7b9#11", "7b9b13", "7b9b5", "7b9sus", "7b13", "7b13sus", + "7-9", "7-9#11", "7-9#5", "7-9#9", "7-9-13", "7-9-5", "7-9sus", + "711", + "7#11", + "7-13", "7-13sus", + "7sus", "7susadd3", + "7+", + "7alt", + "9", + "9+", + "9#5", + "9b5", + "9-5", + "9sus", + "9add6", + "maj7", "maj711", "maj7#11", "maj13", "maj7#5", "maj7sus2", "maj7sus4", + "^7", "^711", "^7#11", "^7#5", "^7sus2", "^7sus4", + "maj9", "maj911", + "^9", "^911", + "^13", + "^9#11", + "11", + "911", + "9#11", + "13", + "13#11", + "13#9", + "13b9", + "alt", + "add2", "add4", "add9", + "sus2", "sus4", "sus9", + "6sus2", "6sus4", + "7sus2", "7sus4", + "13sus2", "13sus4" +}; + +static const char *chord_extensions_minor[] = { + "#5", + "#5", + "m11", + "-11", + "m6", + "-6", + "m69", + "-69", + "m7b5", // same as 'h7' + "-7b5", + "m7-5", + "-7-5", + "mmaj7", + "-maj7", + "mmaj9", + "-maj9", + "m9maj7", + "-9maj7", + "m9^7", + "-9^7", + "madd9", + "-add9", + "mb6", + "-b6", + "m#7", + "-#7", + "msus4", "msus9", + "-sus4", "-sus9", + "m7sus4", + "-7sus4" +}; + static enum SongFragmentType g_current_ftype = SF_TEXT; static enum SongFragmentType g_prev_ftype = SF_TEXT; static struct Config *g_config = NULL; @@ -1374,7 +1446,134 @@ static struct ChoChord *cho_chord_new(void) { struct ChoChord *chord = malloc(sizeof(struct ChoChord)); chord->style = cho_style_new_default(); - chord->chord = NULL; + chord->name = NULL; + chord->is_canonical = false; + chord->root = NULL; + chord->qual = CQ_EMPTY; + chord->ext = NULL; + chord->bass = NULL; + return chord; +} + +/* returns how many bytes make up the root; returns 0 if no root was found */ +static int cho_chord_root_parse(const char *str, struct ChoChord *chord) +{ + const char *note = NULL; + const char *sharp = NULL; + const char *flat = NULL; + int i; + for (i = 0; i<7; i++) { + sharp = g_config->chords->notes[i]->sharp; + if (sharp && str_starts_with(str, sharp)) { + chord->root = strdup(sharp); + return strlen(sharp); + } + } + for (i = 0; i<7; i++) { + flat = g_config->chords->notes[i]->flat; + if (flat && str_starts_with(str, flat)) { + chord->root = strdup(flat); + return strlen(flat); + } + } + for (i = 0; i<7; i++) { + note = g_config->chords->notes[i]->note; + if (str_starts_with(str, note)) { + chord->root = strdup(note); + return strlen(note); + } + } + return 0; +} + +static char *cho_chord_qualifier_strip(const char *str) +{ + if (str_starts_with(str, "m") || str_starts_with(str, "-")) { + return strdup((char *)&str[1]); + } + return strdup(str); +} + +static int cho_chord_qualifier_and_extension_parse(const char *str, struct ChoChord *chord) +{ + int i; + for (i = 0; chord_extensions_major[i]; i++) { + if (str_starts_with(str, chord_extensions_major[i])) { + chord->ext = strdup(chord_extensions_major[i]); + chord->qual = CQ_MAJ; + return strlen(chord_extensions_major[i]); + } + } + for (i = 0; chord_extensions_minor[i]; i++) { + if (str_starts_with(str, chord_extensions_minor[i])) { + chord->ext = cho_chord_qualifier_strip(chord_extensions_minor[i]); + chord->qual = CQ_MIN; + return strlen(chord_extensions_minor[i]); + } + } + if (str_starts_with(str, "aug")) { + chord->qual = CQ_AUG; + return 3; + } + if (str_starts_with(str, "+")) { + chord->qual = CQ_AUG; + return 1; + } + if (str_starts_with(str, "dim")) { + chord->qual = CQ_DIM; + return 3; + } + if (str_starts_with(str, "0")) { + chord->qual = CQ_DIM; + return 1; + } + // TODO: What about 'ΓΈ', 'h', 'h7' and 'h9'? + // TODO: What about extensions after 'aug', '+', 'dim', '0'? + return 0; +} + +static int cho_chord_bass_parse(const char *str, struct ChoChord *chord) +{ + if (str[0] == '/') { + int i; + for (i = 0; i<7; i++) { + if (strcmp((char *)&str[1], g_config->chords->notes[i]->note) == 0) { + chord->bass = strdup((char *)&str[1]); + return strlen((char *)&str[1]); + } + } + } + return 0; +} + +static struct ChoChord *cho_chord_parse(const char *str) +{ + struct ChoChord *chord = cho_chord_new(); + size_t str_len = strlen(str); + size_t bytes_parsed = 0; + int ret; + chord->name = strdup(str); + ret = cho_chord_root_parse(str, chord); + if (ret == 0) { + return chord; + } + bytes_parsed += ret; + if (bytes_parsed == str_len) { + chord->is_canonical = true; + return chord; + } + ret = cho_chord_qualifier_and_extension_parse((const char *)&str[bytes_parsed], chord); + bytes_parsed += ret; + if (bytes_parsed == str_len) { + chord->is_canonical = true; + return chord; + } + ret = cho_chord_bass_parse((const char *)&str[bytes_parsed], chord); + bytes_parsed += ret; + if (bytes_parsed == str_len) { + chord->is_canonical = true; + return chord; + } return chord; } @@ -1493,7 +1692,10 @@ static void cho_song_free(struct ChoSong *song) while (song->sections[i]->lines[k]->text_above[c] != NULL) { if (song->sections[i]->lines[k]->text_above[c]->is_chord) { cho_style_free(song->sections[i]->lines[k]->text_above[c]->u.chord->style); - free(song->sections[i]->lines[k]->text_above[c]->u.chord->chord); + free(song->sections[i]->lines[k]->text_above[c]->u.chord->name); + free(song->sections[i]->lines[k]->text_above[c]->u.chord->root); + free(song->sections[i]->lines[k]->text_above[c]->u.chord->ext); + free(song->sections[i]->lines[k]->text_above[c]->u.chord->bass); free(song->sections[i]->lines[k]->text_above[c]->u.chord); } else { cho_style_free(song->sections[i]->lines[k]->text_above[c]->u.annot->style); @@ -2027,7 +2229,7 @@ struct ChoSong **cho_songs_parse(FILE *fp, struct Config *config) state = STATE_LYRICS; break; } - if (buf == ':') { + if (buf == ':' || buf == ' ') { directive_name[dn] = 0; dn = 0; state = STATE_DIRECTIVE_VALUE; @@ -2188,8 +2390,7 @@ struct ChoSong **cho_songs_parse(FILE *fp, struct Config *config) songs[so]->sections[se]->lines[li]->text_above[c]->is_chord = true; chord_pos = cho_line_compute_chord_position(songs[so]->sections[se]->lines[li], ly, te); songs[so]->sections[se]->lines[li]->text_above[c]->position = chord_pos; - songs[so]->sections[se]->lines[li]->text_above[c]->u.chord = cho_chord_new(); - songs[so]->sections[se]->lines[li]->text_above[c]->u.chord->chord = strdup(chord); + songs[so]->sections[se]->lines[li]->text_above[c]->u.chord = cho_chord_parse(chord); memset(chord, 0, strlen(chord)); c++; g_current_ftype = g_prev_ftype; diff --git a/chordpro.h b/chordpro.h @@ -165,6 +165,14 @@ enum Position { POS_END }; +enum ChordQualifier { + CQ_EMPTY = -1, + CQ_MIN, + CQ_MAJ, + CQ_AUG, + CQ_DIM +}; + struct ChoDirective { enum DirectiveType dtype; enum SectionType stype; @@ -181,7 +189,12 @@ struct ChoMetadata { struct ChoChord { struct Style *style; - char *chord; + bool is_canonical; + char *name; + char *root; + enum ChordQualifier qual; + char *ext; + char *bass; }; struct ChoAnnotation { diff --git a/config.c b/config.c @@ -27,6 +27,46 @@ static const char *g_valid_styles[] = { "chordfingers" // TODO }; +static struct Note notes_common[] = { + { .note = "C", .sharp = "C#", .flat = NULL }, + { .note = "D", .sharp = "D#", .flat = "Db" }, + { .note = "E", .sharp = NULL, .flat = "Eb" }, + { .note = "F", .sharp = "F#", .flat = NULL }, + { .note = "G", .sharp = "G#", .flat = "Gb" }, + { .note = "A", .sharp = "A#", .flat = "Ab" }, + { .note = "B", .sharp = NULL, .flat = "Bb" } +}; + +static struct Note notes_german[] = { + { .note = "C", .sharp = "Cis", .flat = NULL }, + { .note = "D", .sharp = "Dis", .flat = "Des" }, + { .note = "E", .sharp = NULL, .flat = "Es" }, + { .note = "F", .sharp = "Fis", .flat = NULL }, + { .note = "G", .sharp = "Gis", .flat = "Ges" }, + { .note = "A", .sharp = "Ais", .flat = "As" }, + { .note = "H", .sharp = NULL, .flat = "B"}, +}; + +static struct Note notes_scandinavian[] = { + { .note = "C", .sharp = "C#", .flat = NULL }, + { .note = "D", .sharp = "D#", .flat = "Db"}, + { .note = "E", .sharp = NULL, .flat = "Eb"}, + { .note = "F", .sharp = "F#", .flat = NULL }, + { .note = "G", .sharp = "G#", .flat = "Gb"}, + { .note = "A", .sharp = "A#", .flat = "Ab"}, + { .note = "H", .sharp = NULL, .flat = "B"}, +}; + +static struct Note notes_latin[] = { + { .note = "Do", .sharp = "Do#", .flat = NULL }, + { .note = "Re", .sharp = "Re#", .flat = "Reb"}, + { .note = "Mi", .sharp = NULL, .flat = "Mib"}, + { .note = "Fa", .sharp = "Fa#", .flat = NULL }, + { .note = "Sol", .sharp = "Sol#", .flat = "Solb"}, + { .note = "La", .sharp = "La#", .flat = "Lab"}, + { .note = "Si", .sharp = NULL, .flat = "Sib"}, +}; + static struct PrintableItem *config_printable_item_new(const char *name) { struct PrintableItem *item = malloc(sizeof(struct PrintableItem)); @@ -54,6 +94,108 @@ struct PrintableItem *config_printable_item_get(struct PrintableItem **items, co return NULL; } +static struct Note *config_note_new(void) +{ + struct Note *note = malloc(sizeof(struct Note)); + note->note = NULL; + note->sharp = NULL; + note->flat = NULL; + return note; +} + +static void config_note_free(struct Note *note) +{ + free(note->note); + free(note->sharp); + free(note->flat); + free(note); +} + +static struct Note **config_notes_new_default(enum NamingSystem system) +{ + struct Note **notes_default = malloc(8 * sizeof(struct Note *)); + struct Note *notes; + switch (system) { + case NS_COMMON: + notes = (struct Note *)&notes_common; + break; + case NS_GERMAN: + notes = (struct Note *)&notes_german; + break; + case NS_SCANDINAVIAN: + notes = (struct Note *)&notes_scandinavian; + break; + case NS_LATIN: + notes = (struct Note *)&notes_latin; + break; + } + int i; + for (i = 0; i<7; i++) { + notes_default[i] = config_note_new(); + if (notes[i].note) { + notes_default[i]->note = strdup(notes[i].note); + } + if (notes[i].sharp) { + notes_default[i]->sharp = strdup(notes[i].sharp); + } + if (notes[i].flat) { + notes_default[i]->flat = strdup(notes[i].flat); + } + } + notes_default[7] = NULL; + return notes_default; +} + +static struct Note **config_notes_load(toml_table_t *notes, const char *system) +{ + struct Note **custom_notes = malloc(8 * sizeof(struct Note *)); + toml_array_t *arr = toml_table_array(notes, system); + int arr_len = toml_array_len(arr); + if (arr_len != 7) { + fprintf(stderr, "INFO: Custom naming system '%s' in [chords.notes] has to have exactly 7 items.\n", system); + fprintf(stderr, "INFO: For an example see `lorid --print-default-config`\n"); + free(notes); + return NULL; + } + toml_table_t *note; + toml_value_t value; + int i; + for (i = 0; i<arr_len; i++) { + note = toml_array_table(arr, i); + if (note) { + custom_notes[i] = config_note_new(); + value = toml_table_string(note, "note"); + if (value.ok) { + custom_notes[i]->note = value.u.s; + } + value = toml_table_string(note, "sharp"); + if (value.ok) { + custom_notes[i]->sharp = value.u.s; + if (i == 2 || i == 6) { + fprintf(stderr, "INFO: Custom naming system '%s' in [chords.notes] can't have sharp value at array index '%d'.\n", system, i); + goto CLEAN; + } + } + value = toml_table_string(note, "flat"); + if (value.ok) { + custom_notes[i]->flat = value.u.s; + if (i == 0 || i == 3) { + fprintf(stderr, "INFO: Custom naming system '%s' in [chords.notes] can't have flat value at array index '%d'.\n", system, i); + goto CLEAN; + } + } + } + } + custom_notes[7] = NULL; + return custom_notes; + CLEAN: + for (int k=i; k>=0; k--) { + config_note_free(custom_notes[k]); + } + free(custom_notes); + return NULL; +} + static struct Config *config_load_default(void) { struct Config *config = malloc(sizeof(struct Config)); @@ -89,6 +231,10 @@ static struct Config *config_load_default(void) config->printable_items[10]->style->font->name = strdup("Inter"); config->printable_items[10]->style->font->style = FS_ITALIC; config->printable_items[11] = NULL; + config->chords = malloc(sizeof(struct ConfigChords)); + config->chords->system = NS_COMMON; + config->chords->mode = PM_STRICT; + config->chords->notes = NULL; return config; } @@ -302,12 +448,12 @@ struct Config *config_load(const char *filepath) struct Config *config = config_load_default(); char *home = getenv("HOME"); char path[26+strlen(home)+1]; - if (filepath == NULL) { + if (!filepath) { sprintf(path, "%s/.config/lorid/config.toml", home); filepath = path; } FILE *fp = fopen(filepath, "r"); - if (fp == NULL) { + if (!fp) { fprintf(stderr, "INFO: Couldn't open config file '%s'. Using default configuration.\n", filepath); return config; } @@ -319,28 +465,68 @@ struct Config *config_load(const char *filepath) return NULL; } toml_table_t *styles = toml_table_table(table, "styles"); - if (!styles) - goto END; - int unused; - const char *key_name; - toml_table_t *key; - struct PrintableItem *item; - for (int i=0; i<toml_table_len(styles); i++) { - key_name = toml_table_key(styles, i, &unused); - if (config_is_style(key_name)) { - key = toml_table_table(styles, key_name); - if (key) { - item = config_printable_item_get(config->printable_items, key_name); - if (item) { - if (!config_load_style(item->style, key, key_name)) { - fprintf(stderr, "config_load_style failed.\n"); - return NULL; + if (styles) { + int unused; + const char *key_name; + toml_table_t *key; + struct PrintableItem *item; + for (int i=0; i<toml_table_len(styles); i++) { + key_name = toml_table_key(styles, i, &unused); + if (config_is_style(key_name)) { + key = toml_table_table(styles, key_name); + if (key) { + item = config_printable_item_get(config->printable_items, key_name); + if (item) { + if (!config_load_style(item->style, key, key_name)) { + fprintf(stderr, "config_load_style failed.\n"); + return NULL; + } } } } } } - END: + toml_table_t *chords = toml_table_table(table, "chords"); + struct Note **notes_custom = NULL; + if (chords) { + toml_value_t value; + value = toml_table_string(chords, "system"); + if (value.ok) { + if (strcasecmp(value.u.s, "common") == 0 || strcasecmp(value.u.s, "dutch") == 0) { + config->chords->system = NS_COMMON; + } else if (strcasecmp(value.u.s, "german") == 0) { + config->chords->system = NS_GERMAN; + } else if (strcasecmp(value.u.s, "scandinavian") == 0) { + config->chords->system = NS_SCANDINAVIAN; + } else if (strcasecmp(value.u.s, "latin") == 0) { + config->chords->system = NS_LATIN; + } else { + toml_table_t *notes = toml_table_table(chords, "notes"); + if (notes) { + notes_custom = config_notes_load(notes, value.u.s); + if (!notes_custom) { + fprintf(stderr, "config_notes_load failed.\n"); + fprintf(stderr, "INFO: Continuing with default naming system 'common'.\n"); + } + } + } + free(value.u.s); + } + value = toml_table_string(chords, "mode"); + if (value.ok) { + if (strcasecmp(value.u.s, "strict") == 0) { + config->chords->mode = PM_STRICT; + } else if (strcasecmp(value.u.s, "relaxed") == 0) { + config->chords->mode = PM_RELAXED; + } + free(value.u.s); + } + } + if (notes_custom) { + config->chords->notes = notes_custom; + } else { + config->chords->notes = config_notes_new_default(config->chords->system); + } toml_free(table); fclose(fp); return config; @@ -354,5 +540,12 @@ void config_free(struct Config *config) i++; } free(config->printable_items); + i = 0; + while (config->chords->notes[i] != NULL) { + config_note_free(config->chords->notes[i]); + i++; + } + free(config->chords->notes); + free(config->chords); free(config); } diff --git a/config.h b/config.h @@ -6,8 +6,33 @@ struct PrintableItem { struct Style *style; }; +enum NamingSystem { + NS_COMMON, + NS_GERMAN, + NS_SCANDINAVIAN, + NS_LATIN +}; + +enum ParseMode { + PM_STRICT, + PM_RELAXED +}; + +struct Note { + char *note; + char *sharp; + char *flat; +}; + +struct ConfigChords { + enum NamingSystem system; + enum ParseMode mode; + struct Note **notes; +}; + struct Config { struct PrintableItem **printable_items; + struct ConfigChords *chords; }; struct Config *config_load(const char *filepath); diff --git a/lorid.c b/lorid.c @@ -45,7 +45,7 @@ int main(int argc, char *argv[]) } } struct Config *config = config_load(config_filepath); - if (config == NULL) { + if (!config) { fprintf(stderr, "config_load failed.\n"); return 1; } @@ -63,7 +63,7 @@ int main(int argc, char *argv[]) return 1; } struct ChoSong **songs = cho_songs_parse(fp, config); - if (songs == NULL) { + if (!songs) { fprintf(stderr, "cho_parse failed.\n"); return 1; } diff --git a/out_pdf.c b/out_pdf.c @@ -568,7 +568,7 @@ static enum Bool out_pdf_text_above_is_enough_space(struct ChoLine *line, struct return B_ERROR; } free(name); - prev_text_above_width = pdfioContentTextMeasure(font_obj, prev_text_above->u.chord->chord, prev_text_above->u.chord->style->font->size); + prev_text_above_width = pdfioContentTextMeasure(font_obj, prev_text_above->u.chord->name, prev_text_above->u.chord->style->font->size); } else { name = out_pdf_fnt_name_create(prev_text_above->u.annot->style->font); font_obj = out_pdf_fnt_obj_get_by_name(name); @@ -730,7 +730,7 @@ static struct Text **text_create(struct ChoSong **songs, struct Config *config) } if (text_above[ch]->is_chord) { text[t]->lines[tl]->items[tli]->style = cho_style_duplicate(text_above[ch]->u.chord->style); - text[t]->lines[tl]->items[tli]->text = strdup(text_above[ch]->u.chord->chord); + text[t]->lines[tl]->items[tli]->text = strdup(text_above[ch]->u.chord->name); } else { text[t]->lines[tl]->items[tli]->style = cho_style_duplicate(text_above[ch]->u.annot->style); text[t]->lines[tl]->items[tli]->text = strdup(text_above[ch]->u.annot->text); diff --git a/todo b/todo @@ -16,4 +16,3 @@ introduce parse errors still very unclear to me when the parser should warn and continue execution and when it should fail and stop execution -continue line if it ends with a backslash diff --git a/util.c b/util.c @@ -1,9 +1,23 @@ #include <stdlib.h> +#include <stdbool.h> #include <ctype.h> #include <string.h> #include <sys/stat.h> #include "util.h" +bool str_starts_with(const char *str, const char *part) +{ + unsigned int i; + size_t part_len = strlen(part); + if (part_len > strlen(str)) + return false; + for (i=0; i<part_len; i++) { + if (str[i] != part[i]) + return false; + } + return true; +} + char *str_normalize(const char *str) { char *normalized = NULL; diff --git a/util.h b/util.h @@ -5,6 +5,7 @@ enum FileType { F_OTHER }; +bool str_starts_with(const char *str, const char *part); char *str_normalize(const char *str); char *str_trim(const char *str); char *str_remove_leading_whitespace(const char *str);