Initial Commit of prototype

This commit is contained in:
Marco van Dijk 2021-07-07 18:38:45 +02:00
commit 2e29859e1c
12 changed files with 393 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
__pycache__/

BIN
fonts/CourierPrime-Bold.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

0
lib/__init__.py Normal file
View File

11
lib/chordFinder.py Normal file
View File

@ -0,0 +1,11 @@
# This file returns tablature for chords in different positions and voicings
# TODO: we need to itemize all chords in the song and major/minor/diminshed/augmented/dom7/maj7/etc (we can support more voicings as we go)
# then for each chord, get location for each (C A G E D) shape
# for each shape, generate finger position tab, like so:
# B x24442
# C#m x46654
# Amaj7 x02120
# F#m 244222
# Am6 x04555
#

183
lib/dataStructures.py Normal file
View File

@ -0,0 +1,183 @@
# !/usr/bin/python
# This file hosts the classes for storing song data
import re
# TODO: move to separate file with helper functions like this
def stripEmptyLines(inputString):
nonEmptyLines = ""
lines = inputString.split("\n")
for line in lines:
if line.strip() != "":
nonEmptyLines += line + "\r\n"
return nonEmptyLines
# read .txt input TODO: move to separate input functions if we want to support multiple types of inputs some day, like web or PDF
def readSourceFile(inputFile):
with open(inputFile, 'r') as file:
return file.read()
def isChordType(inputString):
if not inputString:
return
#print("Checking '{}' for line type".format(inputString))
# Assume CHORD line if any NUMBER character
if any(char.isdigit() for char in inputString):
#print("'{}' is a CHORD line, since it contains a number".format(inputString))
return True
# Assume CHORD line if any character {/, #, (, ), }
chordSpecificCharacterString = r"/#"
if any(elem in inputString for elem in chordSpecificCharacterString):
#print("'{}' is a CHORD line, since it contains a chord specific character".format(inputString))
return True
# Assume LYRIC line if any TEXT character OTHER THAN {a, b, c, d, e, f, g, h, b, x, m}
lyricSpecificCharacterString = r"abcdefghbxm"
for char in inputString:
if char.isalpha():
if not char.lower() in lyricSpecificCharacterString:
#print("'{}' is a LYRIC line, since it contains lyric specific text characters".format(inputString))
return False
# Assume LYRIC line if any character {.}
lyricSpecialChars = r"."
if any(elem in inputString for elem in lyricSpecialChars):
#print("'{}' is a LYRIC line, since it contains lyric specific special characters".format(inputString))
return False
# Else warn and assume chord line
#print("Unable to identify if '{}' is a lyric or chord line. Assuming it is a chord line. Please improve the isChordType function".format(inputString))
return True
class Section:
def __init__(self):
# List of lines of lyrics strings
self.lyrics = []
# List of lines of chord strings
self.chords = []
# section type string
self.header = ""
# string of chord and lyric data
self.rawData = ""
# Flag for succesfully parsed
self.isParsed = False
# Parses self.rawData into lyrics and chord strings
def parseMe(self):
isFirstLine = True
# Input sections may have chord-only or lyric-only sections
# So we have to insert empty lines if we have subsequent chord or lyric lines
lines = self.rawData.split('\r\n')
for line in lines:
# Determine lyric or chord line
currentIsChord = isChordType(line)
# Initially just fill in the first line correctly
if isFirstLine:
isFirstLine = False
if currentIsChord:
self.chords.append(line)
else:
self.lyrics.append(line)
# We want alternating lines, so if the prev is of the same type
# we need to insert an empty line of the other type
elif currentIsChord == prevWasChord:
if currentIsChord:
#print("Inserting empty Lyric line")
self.chords.append(line)
self.lyrics.append("")
else:
#print("Inserting empty Chord line")
self.lyrics.append(line)
self.chords.append("")
# also insert the current line
elif currentIsChord:
#print("Inserting empty Lyric line")
self.chords.append(line)
else:
self.lyrics.append(line)
prevWasChord = currentIsChord
# Simple check to see if it worked
if abs(len(self.lyrics) - len(self.chords)) > 1:
print("Unable to parse section, since there is a mismatch between the amount of chord and lyric lines.")
return
# Add a final empty line if necessary
elif len(self.lyrics) > len(self.chords):
self.chords.append("")
elif len(self.lyrics) < len(self.chords):
self.lyrics.append("")
self.isParsed = True
class Song:
def __init__(self):
# Src file
self.inputFile = ""
# Path to folder
self.outputLocation = ""
# Title - based on input file
self.title = ""
# List of Section objects
self.sections = []
# Meta info: the text before the first section
self.metadata = ""
# String of entire input
self.rawData = ""
# Flag for succesfully parsed
self.isParsed = False
# Parses self.rawData into Section objects and metadata
def parseMe(self):
# Fill raw data
self.rawData = readSourceFile(self.inputFile)
# Clean up input
parseData = stripEmptyLines(self.rawData)
#print("Clean data='{}'\n".format(parseData))
# While !EOF: build sections (untill []).
delimiterIndex = parseData.find("[")
if delimiterIndex == -1:
print("Cannot parse input file, since it is not delimited by '[<sectionName>]' entries")
return
# Start with metadata
self.metadata = parseData[:delimiterIndex]
#print("Set '{}' as metadata".format(self.metadata))
parseData = parseData[delimiterIndex:]
# We are now at the start of the first section, at the '[' character
while parseData:
thisSection = Section()
# Get first line
delimiterIndex = parseData.find("]\r\n")
if delimiterIndex == -1:
print("Cannot parse input file, delimitor did not match '[<sectionName>]'")
return
# Set header to first line
thisSection.header = parseData[:delimiterIndex+3]
parseData = parseData[delimiterIndex+3:]
# Find next section
delimiterIndex = parseData.find("[")
# If EOF, current buffer is final section
if delimiterIndex == -1:
# Set current section data to remaining buffer
thisSection.rawData = parseData
parseData = ""
else:
# Set thisSection's data and remove it from the buffer
thisSection.rawData = parseData[:delimiterIndex]
#print("set rawData of '{}' to this section".format(thisSection.rawData))
parseData = parseData[delimiterIndex:]
# Finally parse section data
thisSection.parseMe()
if thisSection.isParsed:
self.sections.append(thisSection)
else:
print("Aborting parse due to section not being parseable.")
return
self.isParsed = True

42
lib/initSongs.py Normal file
View File

@ -0,0 +1,42 @@
# !/usr/bin/python
# Iterate through input folders and create a list of Song objects
import lib.dataStructures
import os
# For now manually whitelist folders to convert
whitelist = ["/mnt/koios/Band/1-sugmesties", "/mnt/koios/Band/2-oefenen", "/mnt/koios/Band/3-uitgewerkt"]
def initSong(filePath):
thisSong = lib.dataStructures.Song()
thisSong.inputFile = filePath
# set base folder name - depending on selected outputs the output folder name changes
thisSong.outputLocation = filePath[:filePath.rfind('.')]
# title is just the name of the .txt file
thisSong.title = thisSong.outputLocation[filePath.rfind('/')+1:]
#print("Finished init for input file '{}'.\nBase output folder is '{}'\nSong title is '{}'\n".format(thisSong.inputFile, thisSong.outputLocation, thisSong.title))
return thisSong
def getSongObjects():
# path to song folders, which MAY contain a .txt source file
txtFileLocations = []
# list of Song objects
songList = []
# get all subdirectories
for inputFolder in whitelist:
for root, dirs, files in os.walk(inputFolder):
for name in files:
if(name[name.rfind('.'):] == ".txt"):
filePath = os.path.join(root, name)
#print("Found .txt file '{}'".format(filePath))
txtFileLocations.append(filePath)
#else:
#print("Skipping file '{}' for it is not a .txt file".format(name))
# go through all input locations. find .txt files. for each .txt file initSong. return list
while(txtFileLocations):
filePath = txtFileLocations.pop()
if (filePath != ""):
songList.append(initSong(filePath))
return songList

10
lib/transpose.py Normal file
View File

@ -0,0 +1,10 @@
# This file takes a string corresponding to chord data and transposes it
slider = ['E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B', 'C', 'Db', 'D', 'Eb']
# TODO: take a line of chord data, for each string enclosed in whitespace or tabs:
# ignore if its in major or minor, just take the ROOT of the chord
# then get its index in the slider
# then add/subtract transposition amount and loop around the slider if it goes over
# make sure to keep line width persistent:
# if from E to Eb for example, remove a whitespace
# if from Eb to D for example, add a whitespace

24
main.py Normal file
View File

@ -0,0 +1,24 @@
# !/usr/bin/python
# This program converts all songs in a given directory to a printable format
import lib.chordFinder
import lib.dataStructures
import lib.initSongs
import lib.transpose
import output2img
def main():
# Init Song objects for all songs with compatible inputs
songs = lib.initSongs.getSongObjects()
# Convert all songs into sections
for song in songs:
song.parseMe()
# Parse as PNG a4
if song.isParsed:
# Create subdirectory where we will output our images
targetDirectory = song.outputLocation + "-a4-png"
print("Successfully parsed '{}' file. Writing output to '{}'".format(song.inputFile, targetDirectory))
# Write out metadata and sections, as many as can fit on one page
output2img.outputToImage(targetDirectory, song)
if __name__ == "__main__":
main()

122
output2img.py Normal file
View File

@ -0,0 +1,122 @@
# !/usr/bin/python
# This program converts Song objects to imgs printable on A4 paper
import os
import lib.dataStructures
from PIL import Image, ImageDraw, ImageFont
# size and font of metadata
metaFontFamily = 'fonts/CourierPrime-Regular.ttf'
metaFontWeight = 8
# size and font of chord and lyric text
lyricFontFamily = 'fonts/CourierPrime-Regular.ttf'
chordFontFamily = 'fonts/CourierPrime-Bold.ttf'
songFontWeight = 14
# image properties
imageWidth, imageHeight = (595, 842) # A4 at 72dpi
background = (255, 255, 255)
fontMetadata = ImageFont.truetype(metaFontFamily, metaFontWeight)
fontLyrics = ImageFont.truetype(lyricFontFamily, songFontWeight)
fontChords = ImageFont.truetype(chordFontFamily, songFontWeight)
fontColour = ()
topMargin = 10
leftMargin = 25
# return expected height of rendering the complete current section
def calcSectionHeight(section):
lineIterator = 0
amountOfLines = len(section.lyrics)
heightSum = 0
# add section title
headerWidth, headerHeight = fontChords.getsize(section.header)
heightSum += headerHeight
while lineIterator < amountOfLines:
# Get chord&lyric line
lyricTextWidth, lyricTextHeight = fontLyrics.getsize(section.lyrics[lineIterator])
chordTextWidth, chordTextHeight = fontChords.getsize(section.chords[lineIterator])
heightSum += lyricTextHeight + chordTextHeight
lineIterator += 1
return heightSum
def outputToImage(folderLocation, songObj):
# Create target Directory if don't exist
if not os.path.exists(folderLocation):
os.mkdir(folderLocation)
print("Directory " , folderLocation , " Created ")
#else:
#print("Directory " , folderLocation , " already exists")
# Init image info
imageNumber = 1
currentHeight = topMargin
# New Image
a4image = Image.new('RGB',(imageWidth, imageHeight),(background))
draw = ImageDraw.Draw(a4image)
# Write metadata
for line in songObj.metadata.split('\n'):
# remove any unwanted characters from metadat
line = line.rstrip()
if not line:
continue
#print("meta line '{}'".format(line))
metadataTextWidth, metadataTextHeight = fontMetadata.getsize(line)
draw.text((leftMargin,currentHeight), line, fill=(128, 128, 128), font=fontMetadata)
currentHeight += metadataTextHeight
currentHeight += topMargin
for section in songObj.sections:
lineIterator = 0
amountOfLines = len(section.lyrics)
if (amountOfLines != len(section.chords)):
print("Cannot write this section to file, since it was not processed correctly. There are {} chord lines and {} lyric lines. Aborting...".format(len(section.chords), amountOfLines))
return
# See if it can fit on the current page - if it does not, write & reset
if currentHeight + calcSectionHeight(section) > imageHeight:
#print("overflow! starting with a new image")
outputLocation = folderLocation + "/" + str(imageNumber) + ".png"
imageNumber += 1
a4image.save(outputLocation)
currentHeight = topMargin
a4image = Image.new('RGB',(imageWidth, imageHeight),(background))
draw = ImageDraw.Draw(a4image)
# add section title
headerWidth, headerHeight = fontChords.getsize(section.header)
draw.text((leftMargin,currentHeight), section.header, fill=(0, 0, 0), font=fontChords)
currentHeight += headerHeight
while lineIterator < amountOfLines:
#print("Printing chord line {} and lyrics line {}".format(section.chords[lineIterator], section.lyrics[lineIterator]))
# Get chord&lyric line
lyricTextWidth, lyricTextHeight = fontLyrics.getsize(section.lyrics[lineIterator])
chordTextWidth, chordTextHeight = fontChords.getsize(section.chords[lineIterator])
# add to image file
draw.text((leftMargin,currentHeight), section.chords[lineIterator], fill=(0, 0, 0), font=fontChords)
currentHeight += chordTextHeight
draw.text((leftMargin,currentHeight), section.lyrics[lineIterator], fill=(0, 0, 0), font=fontLyrics)
currentHeight += lyricTextHeight
lineIterator += 1
#print("currentheight={}".format(currentHeight))
currentHeight += topMargin
# Write remaining image to file as well
outputLocation = folderLocation + "/" + str(imageNumber) + ".png"
a4image.save(outputLocation)