Created
September 13, 2019 04:24
-
-
Save bdice/5a3646d21d06ff662b8a39686c7021eb to your computer and use it in GitHub Desktop.
This script generates song files for the LaTeX package "songs" from charts in the common format copy-pasted from Ultimate Guitar and similar websites, where chords are on one line, aligned by whitespace with the lyrics on the following line.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import click | |
import re | |
"""This script generates song files for the LaTeX package "songs" from chord | |
charts in the common format copy-pasted from Ultimate Guitar and similar | |
websites, where chords are on one line, aligned by whitespace with the lyrics | |
on the following line (after dropping lines with only whitespace). | |
This script will accomplish most of the task automatically but outputs still | |
need to be reviewed and manually fixed before inclusion in a LaTeX songbook. | |
Sample input: | |
C Am F G | |
This is a sample lyric from a four chord song. | |
Sample output: | |
\[C]This is a sam\[Am]ple lyric from a \[F]four chord \[G]song. | |
Notes: | |
Lines containing non-chord, non-lyric formatting content like "[Verse]" should | |
be manually removed before using this script. ASCII formatting of musical | |
notation like ||: repeating measures :|| is similarly unsupported. Lines with | |
only chords (e.g. for intros or solo sections) may be mishandled. Some | |
combinations of chord lines and lyric lines may cause the final lyric line to | |
be dropped. | |
Songs LaTeX package: http://songs.sourceforge.net/ | |
Author: Bradley Dice (@bdice) | |
""" | |
CHORD_SYMBOLS = 'ABCDEFGmajsusx0123456789b#/' | |
def is_chord(line, whitespace=True, threshold=0.85): | |
"""Determines if a string (line) of text is mostly chord symbols.""" | |
symbols = CHORD_SYMBOLS + (' ' if whitespace else '') | |
chord_like = sum(map(lambda c: c in symbols, line)) | |
total = len(line) | |
return (chord_like / total) > threshold | |
def combine_chords(chord_line, line): | |
"""Combines chord symbols into a lyric line. | |
Given a line of chord text and a line of lyric text, combine the chords | |
with the lyrics such that the chords are integrated into the line at the | |
appropriate position, given by whitespace in the chord line aligning it | |
with the lyrics below. | |
""" | |
symbols = list(filter(len, re.split('(\s+)', chord_line))) | |
index = 0 | |
for s in symbols: | |
if is_chord(s, whitespace=False, threshold=0.6): | |
s_length = len(s) | |
s = chordize(s) | |
line = line[:index] + s + line[index:] | |
index += s_length | |
index += len(s) | |
return line | |
def chordize(line): | |
"""Transforms an ASCII chord symbol into the LaTeX form used by songs.""" | |
return ' '.join([ | |
r'\[' + chord.replace('b', '&') + ']' for chord in line.split()]) | |
@click.command() | |
@click.argument('input', type=click.File('r')) | |
def parse_song(input): | |
"""Generates LaTeX song files from an input text file. | |
Given a song's chord chart and lyrics as a text file, print its LaTeX form | |
to stdout. | |
""" | |
lines = input.readlines() | |
lines = list(map(str.rstrip, lines)) | |
lines = list(filter(len, lines)) | |
chords = list(map(is_chord, lines)) | |
song_lines = [] | |
song_lines.append(r'\beginsong{Song Title}[by={Artist}]') | |
song_lines.append('') | |
for i in range(len(chords)): | |
next_i = i + 1 | |
if chords[i] and next_i < len(chords): | |
if chords[next_i]: | |
song_lines.append(chordize(lines[i])) | |
else: | |
# This chord line should be combined with the following line | |
song_lines.append(combine_chords(lines[i], lines[next_i])) | |
elif not chords[i] and (next_i >= len(chords) or not chords[next_i]): | |
song_lines.append(lines[i]) | |
song_lines.append('') | |
song_lines.append(r'\endsong') | |
for line in song_lines: | |
print(line) | |
if __name__ == '__main__': | |
parse_song() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment