commit 79a5a9893f502100d29ea2dac399a4115a543b91
parent f5cf40d7f52df07ce54f7cb9c1b4bac77732a7d6
Author: nibo <nibo@relim.de>
Date: Tue, 6 Aug 2024 08:13:56 +0200
Parse chords
Not finished.
Diffstat:
| M | chordpro.c | | | 211 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- |
| M | chordpro.h | | | 15 | ++++++++++++++- |
| M | config.c | | | 231 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------- |
| M | config.h | | | 25 | +++++++++++++++++++++++++ |
| M | lorid.c | | | 4 | ++-- |
| M | out_pdf.c | | | 4 | ++-- |
| M | todo | | | 1 | - |
| M | util.c | | | 14 | ++++++++++++++ |
| M | util.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 *)¬es_common;
+ break;
+ case NS_GERMAN:
+ notes = (struct Note *)¬es_german;
+ break;
+ case NS_SCANDINAVIAN:
+ notes = (struct Note *)¬es_scandinavian;
+ break;
+ case NS_LATIN:
+ notes = (struct Note *)¬es_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);