Skip to content

Instantly share code, notes, and snippets.

@bdice
Created September 13, 2019 04:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bdice/5a3646d21d06ff662b8a39686c7021eb to your computer and use it in GitHub Desktop.
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.
#!/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