Portfolio: Lead Sheets

By

View an example: Example Lead Sheet

About the App

Lead sheets help guitarists and other instrumentalists play music. They consist of song lyrics with chord symbols written above the words. For accurate formatting, I prefer to store my lead sheets as plain text files, viewed using a monospaced font. This project was borne out of a desire to automate the process of transposing songs from one key to another, as when using a capo.

This tool first parses a plain text lead sheet file and separates the chords from the lyrics. It then enables transposition, as well as uses a stylesheet that supports easy printing.

Selected Source Files

index.ts

import { LeadSheet } from "./lead_sheets/classes/LeadSheet";
import { globals } from "./lead_sheets/globals";

declare var interlace: boolean;
declare var intro_score: string;

document.addEventListener("DOMContentLoaded", () => {
  // Boot the application
  globals.lead_sheet = new LeadSheet(interlace, intro_score);
});

LeadSheet.ts

import type { Maybe, Nullable } from "../../lib/types";
import { crEl, get_style, qs, qsNullable, qsa } from "../../lib/util";
import { find_transposition_factor, get_css_rule } from "../functions";
import { circle_of_fifths, globals } from "../globals";
import type { IntervalListItem, LineInfo } from "../types";
import { ChordsLine } from "./ChordsLine";
import { LineGroup } from "./LineGroup";
import { LyricsLine } from "./LyricsLine";
import { LyricsOnlyLine } from "./LyricsOnlyLine";
import { LyricsOnlyLineGroup } from "./LyricsOnlyLineGroup";
import { Score } from "./Score";

/**
 * Represents a lead sheet.
 */
export class LeadSheet {
    key_elem: Element;
    interlace: boolean;
    intro_score: string;
    hide_chords_text = "Hide chords (lyrics only)";
    show_chords_text = "Show chords";

    /**
     * Constructs a new instance of the LeadSheet class.
     * @param interlace - A boolean indicating whether to interlace the lyrics.
     * @param intro_score - The intro score for the lead sheet.
     */
    constructor(interlace = false, intro_score = "") {
      // Fix the meta typography
      let key_elem = qs("#key");
      this.key_elem = key_elem;
      this.interlace = interlace;
      this.intro_score = intro_score;
      this.#fix_meta();

      // Initialization
      globals.interlaced_lines = new LyricsOnlyLineGroup();
      let outer_container = qs("#song");
      let lines = outer_container.innerHTML.split("\n");
      const line_groups: LineGroup[] = [];
      const prev_line: LineInfo = {
        blank: true,
        group: new LineGroup(0),
      };
      let sequence = 0;

      // Loop through the lines and group them
      lines.forEach((line, line_index) => {
        const blank_line = globals.re.blank.test(line);
        let error = false;
        if (blank_line) {
          let no_next_line = line_index >= lines.length - 1;
          let next_line = no_next_line ? false : lines[line_index + 1];
          let non_blank_next_line =
            next_line !== false && !globals.re.blank.test(next_line);
          if (no_next_line || non_blank_next_line) {
            prev_line.blank = true;
            if (this.interlace) {
              globals.interlaced_lines.add(new LyricsOnlyLine(line));
            }
            return;
          } else {
            line = "";
          }
        }
        sequence++;
        if (this.is_chord_line(line)) {
          let group = new LineGroup(sequence);
          prev_line.group = group;
          line_groups.push(group);
          let chords = new ChordsLine(line);
          chords.error = error;
          group.add_line(chords);
          prev_line.blank = false;
        } else {
          // lyrics line
          if (prev_line.blank) {
            prev_line.group = new LineGroup(sequence);
            line_groups.push(prev_line.group);
          }
          prev_line.group.add_line(new LyricsLine(line));
          prev_line.blank = false;
          if (interlace) {
            globals.interlaced_lines.add(new LyricsOnlyLine(line));
          }
        }
      });
      outer_container.innerHTML = "";
      line_groups.forEach((group) =>
        outer_container?.appendChild(group.toHTML())
      );

      // Remove blank cells at the start of some lines
      qsa<HTMLTableSectionElement>("tbody", outer_container).forEach((tbody) => {
        let tr_chords = qsNullable<HTMLTableRowElement>("tr.chords", tbody);
        let tr_lyrics = qsa<HTMLTableRowElement>("tr.lyrics", tbody);
        let blank_lyrics: HTMLElement[] = [];
        let blank_chord: Maybe<Nullable<HTMLElement>>;
        try {
          if (tr_chords === null) {
            throw new TypeError("No chord line found");
          }
          blank_chord = qsNullable("td", tr_chords);
        } catch (e) {
          if (e instanceof TypeError) {
            // Sometimes there's no chord line.
            console.debug(
              "Removing blank cells: There's no chord line here. This isn't a problem."
            );
            console.debug(tbody);
            console.debug(e);
            return;
          } else {
            throw e;
          }
        }
        if (blank_chord && blank_chord.innerHTML.trim().length == 0) {
          let blank_start: boolean = true;
          tr_lyrics.forEach((tr) => {
            let td = qs<HTMLTableCellElement>("td", tr);
            let span = qsNullable<HTMLSpanElement>(".with-chords", td);
            if (span && span.innerHTML.trim().length > 0) {
              blank_start = false;
            } else if (!span && td && td.innerHTML.trim().length > 0) {
              blank_start = false;
            } else if (td) {
              blank_lyrics.push(td);
            }
          });
          if (blank_start) {
            blank_chord?.parentElement?.removeChild(blank_chord);
            blank_lyrics.forEach((td) => {
              td.parentElement?.removeChild(td);
            });
          }
        }
      });

      this.init_events();
      this.resize_tables();

      if (this.intro_score) {
        const line = qs(".line-group");
        let method = "insert";
        let with_chords = qs(".with-chords", line);
        if (with_chords.innerHTML.startsWith("Intro")) {
          method = "replace";
        }
        globals.score = new Score(intro_score, line, method);
      }

      // Insert interlaced lyrics
      if (this.interlace) {
        let lyrics = document.createElement("div");
        lyrics.id = "lyrics";
        lyrics.classList.add("hide");
        lyrics.innerHTML = globals.interlaced_lines.to_html();
        outer_container.appendChild(lyrics);
      }

      // Show lyrics only if requested
      if (globals.query_string.has("lyrics")) {
        this.handle_toggle_chords();
      }

      // Add a song number if requested
      if (globals.query_string.has("number")) {
        let num = globals.query_string.get("number");
        if (num?.match(/^[0-9.-]+$/)) {
          // prevent query string code injection/XSS
          let title = qs("#title");
          title.innerHTML = `<span class="sequence-number">${num}</span> ${title.innerHTML}`;
        }
      }
    }

    /**
     * Fixes the meta information of the lead sheet.
     * Replaces '#' with '♯' and 'b' with '♭' in the key element's inner HTML.
     * Updates the initial key value in the globals object.
     */
    #fix_meta(): void {
      if (this.key_elem !== null) {
        this.key_elem.innerHTML = this.key_elem.innerHTML
          .replace(/#/g, "♯")
          .replace(/b/g, "♭");
        globals.initial_key = this.key_elem.innerHTML.split(" ")[0];
      }
    }

    /**
     * Changes the key of the lead sheet to the specified index.
     * 
     * @param index - The index of the new key in the circle of fifths.
     * @throws Error if no current key is set.
     */
    change_to_key(index: number): void {
      if (globals.current_key === null) {
        throw new Error("No current key set");
      }
      let minor = globals.current_key.match(/m$/) ? true : false;
      let new_key = minor
        ? circle_of_fifths[index].name_minor
        : circle_of_fifths[index].name_major;
      let old_key = globals.current_key;
      globals.current_key = new_key;
      let old_index = circle_of_fifths.findIndex(value => value.name_major == old_key || value.name_minor == old_key);
      let notes = [
        ...Array.from(qsa("#song .chord .note-name")),
        ...Array.from(qsa("#song .chord .bass-note")),
      ];
      notes.forEach((note) => {
        let text = note.innerHTML;
        let scale_index = circle_of_fifths[old_index].scale.indexOf(text);
        let new_note = circle_of_fifths[index].scale[scale_index];
        note.innerHTML = new_note;
      });

      if (globals.score) {
        // We have to use the initial key instead of the old key because ABCJS
        // throws an error if it tries to transpose multiple times.
        let old_key_score = (/(F♯|G♭)m?/.test(globals.initial_key || 'C')) ? "F♯/G♭" : <IntervalListItem>globals.initial_key?.replace(/m$/, "");
        let new_key_score = (/(F♯|G♭)m?/.test(new_key)) ? "F♯/G♭" : <IntervalListItem>new_key.replace(/m$/, "");
        if(new_key_score == "C♯") new_key_score = "D♭";
        if(new_key_score == "C♭") new_key_score = "B";
        globals.score.generate(find_transposition_factor(old_key_score, new_key_score));
      }
    }

    /**
     * Handles the toggle of chords visibility.
     */
    handle_toggle_chords(): void {
      this.toggle_chords();
      let toggle_elem = qs("#toggle-chords .click");
      if (globals.chords_hidden) {
        toggle_elem.innerHTML = this.show_chords_text;
      } else {
        toggle_elem.innerHTML = this.hide_chords_text;
      }
      try {
        qs("#toggle-tab").classList.toggle("hide");
      } catch (e) {
        console.debug("No tab element found");
      }
    }

    /**
     * Initializes the chords toggle functionality.
     * Adds a toggle link to the specified box element and attaches a click
     * event listener to it.
     */
    init_chords_toggle(): void {
      let box = qs("#meta .column-2");
      let link = crEl("P");
      link.id = "toggle-chords";
      link.innerHTML =
        `<span class="click no-print">${this.hide_chords_text}</span>`;
      box.appendChild(link);
      link.addEventListener("click", () => this.handle_toggle_chords());
    }

    /**
     * Initializes the events for the LeadSheet class.
     * This method initializes the key change and chords toggle events,
     * and sets up a resize event listener to dynamically resize the lyrics.
     */
    init_events(): void {
      this.init_key_change();
      this.init_chords_toggle();
      let resizing = false;
      window.addEventListener("resize", () => (resizing = true));
      setInterval(() => {
        if (resizing) {
          resizing = false;
          this.resize_tables();
        }
      }, 1000);
    }

    /**
     * Initializes the key change functionality. The method extracts the key
     * from the key element's innerHTML and updates the global `current_key`
     * variable. It also updates the key element's innerHTML with the extracted
     * key and adds a click event listener to trigger the key change controls.
     * 
     * If the extracted key is invalid, it displays an error message indicating
     * that the key cannot be changed. If the key element is unset, it returns
     * early.
     */
    init_key_change(): void {
      if (this.key_elem === null) {
        return;
      }
      let key = this.key_elem.innerHTML.split(globals.re.white_space, 1)[0];
      let key_additional = this.key_elem.innerHTML;
      key_additional = key_additional.slice(key.length, key_additional.length);

      if (key.match(/^[A-G](?:#|b|bb|x|♯|♭|𝄫|𝄪)?m?$/)) {
        // (?:something) non-capturing group; doesn't get included in \1, \2, etc.
        globals.current_key = key;
        this.key_elem.innerHTML = `<span class="key">${key}</span>${key_additional} <span class="click no-print">(click to change)</span>`;
        qs("#key .click").addEventListener("click", () =>
          this.key_change_controls()
        );
      } else {
        this.key_elem.innerHTML = `<span class="key">${key}</span>${key_additional} <span class="no-print>(unable to change key: invalid key given)`;
      }
    }

    /**
     * Checks if a given line is a chord line.
     * A chord line is a line that contains chords or chord symbols.
     *
     * @param line - The line to check.
     * @returns `true` if the line is a chord line, `false` otherwise.
     */
    is_chord_line(line: string): boolean {
      if (line.length == 0) return false;
      if (line.match(/[.,;:?!'"]\s*$/)) return false;
      let num_chords_list = line
        .replace(/&nbsp;/g, " ")
        .replace(/(\S)$/, "$1 ")
        .match(globals.re.chord_symbol);
      let num_words_list = line.match(globals.re.word);
      let num_chords = num_chords_list === null ? 0 : num_chords_list.length;
      if (num_chords == 0) {
        return false;
      }
      let num_words = num_words_list === null ? 0 : num_words_list.length;
      if (num_chords >= num_words - num_chords) {
        return true;
      } else {
        if (num_chords > 0) {
          console.debug(
            `${num_chords} chord(s) found on this non-chord line: ${line}`
          );
        }
      }
      return false;
    }

    /**
     * Displays the key change controls for selecting a new key.
     * Updates the UI to reflect the selected key and applies the key change
     * when the "Apply" button is clicked.
     */
    key_change_controls(): void {
      let options: string[] = [];

      circle_of_fifths.forEach(key => {
        let option = `<option value="${key.name_major}"`;
        switch (globals.current_key) {
          case key.name_major:
          case key.name_minor:
            option += " selected";
        }
        option += `>${key.name_major} / ${key.name_minor} (${key.signature})</option>`;
        options.push(option);
      });
      let form = crEl("form");
      let label = crEl("label");
      label.setAttribute("for", "key-select");
      label.innerHTML = "Choose a new key: ";
      let select = crEl("select");
      select.id = "key-select";
      select.innerHTML = options.join("");
      let button = crEl("button");
      button.innerHTML = "Apply";
      form.appendChild(label);
      form.appendChild(select);
      form.appendChild(button);
      this.key_elem.innerHTML = "";
      this.key_elem.appendChild(form);
      const button_click_handler_reference = (evt: Event) => {
        evt.preventDefault();
        let key_select = qs("#key-select") as HTMLSelectElement;
        this.change_to_key(key_select.selectedIndex);
        button.removeEventListener("click", button_click_handler_reference);
        this.key_elem.innerHTML =
          '<span class="key">' +
          globals.current_key +
          '</span> <span class="click no-print">(click to change)</span>';
        setTimeout(() => {
          let click = qs("#key .click");
          click.addEventListener("click", () => this.key_change_controls());
        }, 200);
      };
      button.addEventListener("click", button_click_handler_reference);
    }

    /**
     * Resizes the tables in the lead sheet to fit within a maximum width.
     * The method adjusts the font size of the tables, chords, and verse numbers
     * to ensure that the widest table fits within the parent container.
     * 
     * @throws {Error} If the CSS rules for the tables, chords, or verse numbers
     *                 are not found.
     * @throws {Error} If a parent element is not found.
     */
    resize_tables(): void {
      const max_width = 400;
      const parse_value = (val: string): number => {
        return parseInt(val.replace(/[^0-9.]+/g, ""));
      };
      let table_css = get_css_rule("#song .line-group");
      let chord_css = get_css_rule("#song .chord");
      let verse_css = get_css_rule("#song .verse-number, #lyrics .verse-number");
      if (table_css === null) {
        throw new Error("No CSS for #song .line-group found");
      }
      if (chord_css === null) {
        throw new Error("No CSS for #song .chord found");
      }
      if (verse_css === null) {
        throw new Error(
          "No CSS for #song .verse-number, #lyrics .verse-number found"
        );
      }
      if (globals.base_font_size) {
        table_css.style.fontSize = `${globals.base_font_size}px`;
        chord_css.style.fontSize = `${globals.chords_font_size}px`;
        verse_css.style.fontSize = `${globals.verse_font_size}px`;
      }
      const tables = qsa<HTMLTableElement>(".line-group");
      const parent = tables[0].parentElement;
      if (parent === null) {
        throw new Error("No parent element found");
      }
      let parent_width = Math.min(
        max_width,
        Math.floor(parse_value(get_style(parent, "width")))
      );
      let widest_table_width = 0;
      let widest_table_elem = tables[0]; // assigned only to silence warning about the element being used before being assigned
      tables.forEach((table) => {
        let table_width = Math.ceil(parse_value(get_style(table, "width")));
        widest_table_width = Math.max(widest_table_width, table_width);
        if (table_width == widest_table_width) {
          widest_table_elem = table;
        }
      });
      if (!globals.base_font_size) {
        globals.base_font_size = parse_value(table_css.style.fontSize);
        globals.chords_font_size = parse_value(chord_css.style.fontSize);
        globals.verse_font_size = parse_value(verse_css.style.fontSize);
      }
      let counter = 0;
      const current_font_size = (elem: CSSStyleRule): number =>
        parse_value(elem.style.fontSize);
      while (
        counter < 100 &&
        current_font_size(table_css) > 9 &&
        Math.ceil(parse_value(get_style(widest_table_elem, "width"))) >
          parent_width
      ) {
        table_css.style.fontSize = `${current_font_size(table_css) - 1}px`;
        chord_css.style.fontSize = `${current_font_size(chord_css) - 1}px`;
        verse_css.style.fontSize = `${current_font_size(verse_css) - 1}px`;
        counter++;
      }
    }

    /**
     * Toggles the visibility of chords in the lead sheet. If a score is present
     * and interlace is disabled, it toggles the visibility of the score.
     */
    toggle_chords(): void {
      globals.chords_hidden = !globals.chords_hidden;
      if (this.interlace) {
        qsa("#song .line-group").forEach((c) => c.classList.toggle("hide"));
        let lyrics = qs("#lyrics");
        lyrics.classList.toggle("hide");
      } else {
        qsa("#song .chords").forEach((c) => c.classList.toggle("hide"));
        qsa("#song .lyrics td").forEach((c) => c.classList.toggle("lyrics-only"));
      }
      if (globals.score && !this.interlace) {
        globals.score.toggle();
      }
    }
  }

LyricsOnlyLine.ts

import type { Maybe } from "../../lib/types";
import { globals } from "../globals";
import { LyricsLine } from "./LyricsLine";

/**
 * Represents a line of lyrics without any chords or annotations, such as when
 * interlacing is enabled.
 */
export class LyricsOnlyLine extends LyricsLine {
  verse: Maybe<number>;
  indented = false;
  first_line = false;
  group_number: number = -1;

  /**
   * Constructs a new instance of the LyricsOnlyLine class.
   * 
   * @param line - The text of the lyrics line.
   * @param verse - The verse number of the lyrics line.
   * @param replace_dashes - Whether to replace dashes in the lyrics line with hyphens.
   */
  constructor(line: string, verse?: number, replace_dashes = false) {
    super(line, replace_dashes);
    if (/^\s\s\s\s/.test(this.text)) this.indented = true;
    this.text = this.text
      .replace(/^\s+/, "")
      .replace(/\s+$/, "")
      .replace(/\s\s+/g, " ")
      .replace(/–(?!\s)/g, "--") // unfortunately, we can't preserve en dashes because Jekyll is overly aggressive
      .replace(/(?<=\s)–(?=\s)/g, "--") // replace an en dash with two hyphens if it's preceeded and followed by a whitespace character
      .replace(/(?<=\s)–(?=\s)/g, "—") // replace an en dash with an em dash if it's followed by a whitespace character but not preceeded by a word character
      .replace(/\s+(?=-)/g, "") // one or more spaces followed by a hyphen
      .replace(/(?<=-)\s+/g, "") // one or more spaces preceeded by a hyphen
      .replace(/--+/g, "-");

    // hunt for compound words
    Array.from(this.text.matchAll(/(?:\w+-)+\w+/g)).forEach((compound_obj) => {
      let compound = compound_obj[0];
      console.debug(`Found possible compound word: ${compound}`);
      if (!globals.hyphenated_compounds.includes(compound)) {
        this.text = this.text.replace(compound, compound.replace(/-/g, ""));
      }
    });

    this.verse = verse;
  }

  /**
   * Sets the verse number of the lyrics line and returns the current instance.
   * @param num - The verse number to set.
   * @returns The current instance of the LyricsOnlyLine class.
   */
  set_verse_return_this(num: number): this {
    this.verse = num;
    return this;
  }

  /**
   * Converts the lyrics line to an HTML string.
   * @returns The HTML representation of the lyrics line.
   */
  to_html(): string {
    let out = document.createElement("p");
    out.innerHTML = this.text;
    if (this.verse) {
      out.setAttribute("verse", this.verse.toString());
    }
    if (this.indented) out.classList.add("indented");
    if (this.first_line) out.classList.add("first-of-verse");
    return out.outerHTML;
  }
}

Score.ts

import ABCJS, { type AbcVisualParams, type Tablature, type TuneObject } from "abcjs";
import { crEl, qs, qsa } from "../../lib/util";

/**
 * Represents a musical score.
 */
export class Score {
  abc: string;
  container: HTMLElement;
  tablature = false;
  tab_options: Tablature[] = [{ instrument: "guitar" }];
  wrapper: HTMLElement;
  abc_options: AbcVisualParams = {
    responsive: "resize",
    add_classes: true,
    format: {
      gchordfont: "Ubuntu",
      vocalfont: "Ubuntu",
    },
    clickListener: () => qsa(".abcjs-note_selected").forEach((note) => {
      note.classList.remove("abcjs-note_selected");
      note.setAttribute("fill", "currentColor");
    }),
  };

  constructor(abc: string, insert_into: HTMLElement, method: string) {
    this.abc = abc;
    this.wrapper = crEl("div");
    this.wrapper.classList.add("score-wrapper");
    const label = crEl("div");
    label.classList.add("score-label");
    label.innerHTML = "Intro";
    this.container = crEl("div");
    this.container.classList.add("score");
    this.wrapper.appendChild(label);
    this.wrapper.appendChild(this.container);
    this.generate();
    if (method == "replace") {
      insert_into.parentElement?.replaceChild(this.wrapper, insert_into);
    } else {
      insert_into.parentElement?.insertBefore(this.wrapper, insert_into.nextSibling);
    }

    // initialize tablature toggling
    let meta = qs("#meta .column-2");
    let tab_toggle_button_p = crEl("p");
    tab_toggle_button_p.id = "toggle-tab";
    let tab_toggle_button = crEl("span");
    tab_toggle_button.classList.add("click", "no-print");
    tab_toggle_button_p.appendChild(tab_toggle_button);
    meta.appendChild(tab_toggle_button_p);
    tab_toggle_button.innerHTML = "Show tablature";
    tab_toggle_button.addEventListener("click", () => {
      this.tablature = !this.tablature;
      tab_toggle_button.innerHTML =
        tab_toggle_button.innerHTML == "Show tablature"
          ? "Hide tablature"
          : "Show tablature";
      this.generate();
    });
  }

  /**
   * Generates the score based on the provided ABC notation.
   * 
   * @param transposition - The amount of transposition to apply to the score (optional).
   * @returns void
   */
  generate(transposition?: number): void {
    if (!this.abc) {
      return;
    }
    if (this.tablature) {
      this.abc_options.tablature = this.tab_options;
    } else {
      delete this.abc_options.tablature;
    }
    let score = ABCJS.renderAbc(this.container, this.abc, this.abc_options);
    if (transposition !== undefined) {
      try {
        ABCJS.renderAbc(
          this.container,
          // strTranspose is typed incorrectly. The second argument should be a TuneObjectArray. Hence, the weird cast.
          ABCJS.strTranspose(this.abc, score as unknown as TuneObject, <number>transposition),
          this.abc_options
        );
      } catch (e) {
        console.error(e);
        setTimeout(
          () => alert(
            "An error impacted the rendering of the intro score. Debugging details are on the console."
          ),
          10
        );
      }
    }
  }

  /**
   * Toggles the visibility of the score wrapper element.
   */
  toggle(): void {
    this.wrapper.classList.toggle("hide");
  }
}

Example Lead Sheet (Joy_to_the_World.txt)

---
genre: christmas
layout: lead_sheet
title: "Joy to the World"
key: "A (C in <i>SDA Hymnal</i>)"
sda_hymnal: 125
permalink: /music/lead_sheets/songs/joy_to_the_world.html
no_ads: true
interlace: true
other_meta: "<span class=\"type\">Sheet music:</span><ul><li><a href=\"christmas/joy_to_the_world-Violin_1.pdf\">Violin 1</a></li><li><a href=\"christmas/joy_to_the_world-Violin_2.pdf\">Violin 2</a></li><li><a href=\"christmas/joy_to_the_world-Violins.pdf\">Both violins</a></li><li><a href=\"christmas/joy_to_the_world.pdf\">Full score</a></li><li><a href=\"christmas/joy_to_the_world.mscz\">MuseScore source</a></li></ul>"
score: |
    X:1
    M:2/4
    L:1/8
    K:A
    V:1 clef=treble octave=-1
    c     | "F#m"cc c(c/d/)       | "C#m"e3(d/c/) | "G"BB B(B/c/)       |
    w:And | heaven and na-ture_   | sing, And_    | heaven and na-ture_ |
      "E7"d3"^┍ Opt. intro"(c/B/) | "D"(Aa2)f    | "A"(e>d c)d    | "E"c2B2 | "A"A4"^┑"x ||
    w:sing, And_                  | heaven,_ and | heaven__ and   | na-ture | sing.
---
Intro:

   A                       E         F#m
1. Joy to the world, the   Lord is   come!
2. Joy to the earth, the   Savior    reigns!
3. No more let sin and     sorrow    grow,
4. He rules the world with truth and grace,

    D         E         F#m
Let earth   receive her King;
Let men their songs   employ;
Nor thorns  infest the  ground;
And makes the nations   prove

      D          A              D          A
Let   every      heart       prepare Him   room,
While fields and floods, rocks, hills, and plains,
He    comes to   make His       blessings  flow
The   glories    of His         righteous--ness,

    F#m               C#m
And heaven and nature sing,
Re--peat the sounding joy,
Far as the curse is   found,
And wonders of His    love,

    G                 E7
And heaven and nature sing,
Re--peat the sounding joy,
Far as the curse is   found,
And wonders of His    love,

    D            A          E        A
And heaven, and  heaven and nature   sing.
Re--peat,      repeat the   sounding joy.
Far as, far      as the     curse is found.
And wonders, and wonders    of His   love.