Add comments to start of files and functions
This commit is contained in:
Marco van Dijk 2021-07-07 23:01:52 +02:00
parent aea24b5eb9
commit 5be0f83c0d
6 changed files with 192 additions and 60 deletions

View File

@ -1,11 +1,21 @@
# This file returns tablature for chords in different positions and voicings #!/usr/bin/env python3
##
# 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) # @file chordFinder.py
#
# @brief This file returns tablature for chords in different positions and voicings
#
# @section description Description
# -
#
# @section notes Notes
# - File might never be created in the first place, since we might create a lookup table using data from existing available API's
#
# @section todo 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 # then for each chord, get location for each (C A G E D) shape
# for each shape, generate finger position tab, like so: # for each shape, generate finger position tab, like so:
# B x24442 # B x24442
# C#m x46654 # C#m x46654
# Amaj7 x02120 # Amaj7 x02120
# F#m 244222 # F#m 244222
# Am6 x04555 # Am6 x04555
#

View File

@ -1,8 +1,24 @@
# !/usr/bin/python #!/usr/bin/env python3
# This file hosts the classes for storing song data ##
# @file dataStructures.py
#
# @brief This file contains the internal data structures required for each tablature file
#
# @section description Description
# -
#
# @section notes Notes
#
# @section todo TODO
# - Move helper functions like stripEmptyLines to a separate file for
# - Move read functions to separate input functions (also to support more types of inputs)
import re import re
# TODO: move to separate file with helper functions like this """!@brief Removes empty lines and makes sure every line ends with \r\n
@param inputString raw txt input
@return string of parsed input
"""
def stripEmptyLines(inputString): def stripEmptyLines(inputString):
nonEmptyLines = "" nonEmptyLines = ""
lines = inputString.split("\n") lines = inputString.split("\n")
@ -10,20 +26,28 @@ def stripEmptyLines(inputString):
if line.strip() != "": if line.strip() != "":
nonEmptyLines += line + "\r\n" nonEmptyLines += line + "\r\n"
return nonEmptyLines 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
"""!@brief Opens a .txt file and loads it's contents into buffer
@param inputFile path to .txt file
@return .txt file raw contents
"""
def readSourceFile(inputFile): def readSourceFile(inputFile):
with open(inputFile, 'r') as file: with open(inputFile, 'r') as file:
return file.read() return file.read()
def isChordType(inputString): """!@brief Returns whether the string is a line of lyrics or a line of tablature data
@param inputString single line of text
@return True if it is tablature data, False if it is lyric data
"""
def isTablatureData(inputString):
if not inputString: if not inputString:
return return
#print("Checking '{}' for line type".format(inputString)) #print("Checking '{}' for line type".format(inputString))
# Assume CHORD line if any NUMBER character # Assume tablature line if any digit
if any(char.isdigit() for char in inputString): if any(char.isdigit() for char in inputString):
#print("'{}' is a CHORD line, since it contains a number".format(inputString)) #print("'{}' is a CHORD line, since it contains a number".format(inputString))
return True return True
# Assume CHORD line if any character {/, #, (, ), } # Assume tablature line if any character {/, #, (, ), }
chordSpecificCharacterString = r"/#" chordSpecificCharacterString = r"/#"
if any(elem in inputString for elem in chordSpecificCharacterString): if any(elem in inputString for elem in chordSpecificCharacterString):
#print("'{}' is a CHORD line, since it contains a chord specific character".format(inputString)) #print("'{}' is a CHORD line, since it contains a chord specific character".format(inputString))
@ -41,10 +65,11 @@ def isChordType(inputString):
#print("'{}' is a LYRIC line, since it contains lyric specific special characters".format(inputString)) #print("'{}' is a LYRIC line, since it contains lyric specific special characters".format(inputString))
return False return False
# Else warn and assume chord line # 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)) #print("Unable to identify if '{}' is a lyric or tablature line. Assuming it is a tablature line. Please improve the isTablatureData function".format(inputString))
return True return True
"""!@brief Class containing Section specific data
"""
class Section: class Section:
def __init__(self): def __init__(self):
# List of lines of lyrics strings # List of lines of lyrics strings
@ -58,6 +83,9 @@ class Section:
# Flag for succesfully parsed # Flag for succesfully parsed
self.isParsed = False self.isParsed = False
"""!@brief Converts raw buffered data into separate Lyric and tablature lines
@return None
"""
# Parses self.rawData into lyrics and chord strings # Parses self.rawData into lyrics and chord strings
def parseMe(self): def parseMe(self):
isFirstLine = True isFirstLine = True
@ -66,7 +94,7 @@ class Section:
lines = self.rawData.split('\r\n') lines = self.rawData.split('\r\n')
for line in lines: for line in lines:
# Determine lyric or chord line # Determine lyric or chord line
currentIsChord = isChordType(line) currentIsChord = isTablatureData(line)
# Initially just fill in the first line correctly # Initially just fill in the first line correctly
if isFirstLine: if isFirstLine:
isFirstLine = False isFirstLine = False
@ -91,20 +119,21 @@ class Section:
self.chords.append(line) self.chords.append(line)
else: else:
self.lyrics.append(line) self.lyrics.append(line)
# move on to next line, save current type
prevWasChord = currentIsChord prevWasChord = currentIsChord
# Simple check to see if it worked # Simple check to see if it probably exported correctly
if abs(len(self.lyrics) - len(self.chords)) > 1: 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.") print("Unable to parse section, since there is a mismatch between the amount of chord and lyric lines.")
return return
# Add a final empty line if necessary # Add a trailing empty line if necessary
elif len(self.lyrics) > len(self.chords): elif len(self.lyrics) > len(self.chords):
self.chords.append("") self.chords.append("")
elif len(self.lyrics) < len(self.chords): elif len(self.lyrics) < len(self.chords):
self.lyrics.append("") self.lyrics.append("")
self.isParsed = True self.isParsed = True
"""!@brief Class containing Song specific data
"""
class Song: class Song:
def __init__(self): def __init__(self):
# Src file # Src file
@ -122,14 +151,16 @@ class Song:
# Flag for succesfully parsed # Flag for succesfully parsed
self.isParsed = False self.isParsed = False
# Parses self.rawData into Section objects and metadata """!@brief Parses self.rawData into Section objects and metadata
@return None
"""
def parseMe(self): def parseMe(self):
# Fill raw data # Get raw data
self.rawData = readSourceFile(self.inputFile) self.rawData = readSourceFile(self.inputFile)
# Clean up input # Clean up input
parseData = stripEmptyLines(self.rawData) parseData = stripEmptyLines(self.rawData)
#print("Clean data='{}'\n".format(parseData)) #print("Clean data='{}'\n".format(parseData))
# While !EOF: build sections (untill []). # While not EOF: build sections untill new section found.
delimiterIndex = parseData.find("[") delimiterIndex = parseData.find("[")
if delimiterIndex == -1: if delimiterIndex == -1:
print("Cannot parse input file, since it is not delimited by '[<sectionName>]' entries") print("Cannot parse input file, since it is not delimited by '[<sectionName>]' entries")
@ -140,20 +171,21 @@ class Song:
parseData = parseData[delimiterIndex:] parseData = parseData[delimiterIndex:]
# We are now at the start of the first section, at the '[' character # We are now at the start of the first section, at the '[' character
while parseData: while parseData:
# Init new Section object
thisSection = Section() thisSection = Section()
# Get first line # Get header on the first line
delimiterIndex = parseData.find("]\r\n") delimiterIndex = parseData.find("]\r\n")
if delimiterIndex == -1: if delimiterIndex == -1:
print("Cannot parse input file, delimitor did not match '[<sectionName>]'") print("Cannot parse input file, delimitor did not match '[<sectionName>]'")
return return
# Set header to first line # Skip the ']\r\n' characters
thisSection.header = parseData[:delimiterIndex+3] thisSection.header = parseData[:delimiterIndex+3]
parseData = parseData[delimiterIndex+3:] parseData = parseData[delimiterIndex+3:]
# Find next section # Find next section
delimiterIndex = parseData.find("[") delimiterIndex = parseData.find("[")
# If EOF, current buffer is final section # If EOF, current buffer is final section
if delimiterIndex == -1: if delimiterIndex == -1:
# Set current section data to remaining buffer # Set thisSection's data to remaining buffer
thisSection.rawData = parseData thisSection.rawData = parseData
parseData = "" parseData = ""
else: else:

View File

@ -1,11 +1,34 @@
# !/usr/bin/python #!/usr/bin/env python3
# Iterate through input folders and create a list of Song objects ##
# @file initSongs.py
#
# @brief Iterate through input folders and create a list of Song objects
#
# @section description Description
# Initializes the Song objects for each supported input file found
# Currently only supports .txt files, which are read as-is into a string
#
# @section notes Notes
# -
#
# @section todo TODO
# - Set a max recursion depth on the os.walk function
# - Support both paths to folders (like now) and to files directly
# When the input is a file, check if it is .txt and init it
# - Input locations should be set in a config file (init to CWD, overwrite by CMD arguments)
import lib.dataStructures import lib.dataStructures
import os import os
# For now manually whitelist folders to convert # For now manually whitelist folders to convert
whitelist = ["/mnt/koios/Band/1-sugmesties", "/mnt/koios/Band/2-oefenen", "/mnt/koios/Band/3-uitgewerkt"] whitelist = ["/mnt/koios/Band/1-sugmesties", "/mnt/koios/Band/2-oefenen", "/mnt/koios/Band/3-uitgewerkt"]
"""!@brief Creates and inits a Song object
This function creates a new Song object and sets the internal variables correctly
Output folder name is derived from the name of the input file
@param filePath path to the input file
@return intialised Song object
"""
def initSong(filePath): def initSong(filePath):
thisSong = lib.dataStructures.Song() thisSong = lib.dataStructures.Song()
thisSong.inputFile = filePath thisSong.inputFile = filePath
@ -16,14 +39,18 @@ def initSong(filePath):
#print("Finished init for input file '{}'.\nBase output folder is '{}'\nSong title is '{}'\n".format(thisSong.inputFile, thisSong.outputLocation, thisSong.title)) #print("Finished init for input file '{}'.\nBase output folder is '{}'\nSong title is '{}'\n".format(thisSong.inputFile, thisSong.outputLocation, thisSong.title))
return thisSong return thisSong
"""!@brief Returns the list of all Song objects created
This function gets all supported input files in the specified input location(s)
For each of these files it creates a Song object, ready to be read and then parsed
@return list of intialised Song objects
"""
def getSongObjects(): def getSongObjects():
# path to song folders, which MAY contain a .txt source file # path to song folders, which MAY contain a .txt source file
txtFileLocations = [] txtFileLocations = []
# list of Song objects # list of Song objects
songList = [] songList = []
# get all subdirectories # go through all input locations. find .txt files.
for inputFolder in whitelist: for inputFolder in whitelist:
for root, dirs, files in os.walk(inputFolder): for root, dirs, files in os.walk(inputFolder):
for name in files: for name in files:
@ -34,7 +61,7 @@ def getSongObjects():
#else: #else:
#print("Skipping file '{}' for it is not a .txt file".format(name)) #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 # create list of Song objects
while(txtFileLocations): while(txtFileLocations):
filePath = txtFileLocations.pop() filePath = txtFileLocations.pop()
if (filePath != ""): if (filePath != ""):

View File

@ -1,10 +1,21 @@
# This file takes a string corresponding to chord data and transposes it #!/usr/bin/env python3
##
slider = ['E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B', 'C', 'Db', 'D', 'Eb'] # @file transpose.py
# 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 # @brief This file takes a string corresponding to chord data and transposes it
# then get its index in the slider #
# then add/subtract transposition amount and loop around the slider if it goes over # @section description Description
# 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 # @section notes Notes
# -
#
# @section todo 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
slider = ['E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B', 'C', 'Db', 'D', 'Eb']

24
main.py
View File

@ -1,5 +1,25 @@
# !/usr/bin/python #!/usr/bin/env python3
# This program converts all songs in a given directory to a printable format ##
# @file main.py
#
# @brief This program converts supported tablature source files to a printable format
#
# @section description Description
# Creates Song objects of all tablatures it can find in a given directory or its subdirectories
# Supported inputs currently: Any .txt file, as long as each section has a corresponding [<sectionName>] delimiter
# Supported outputs currently: PNG format
# Song objects are then parsed into separate metadata information and sections
# Sections contain lines of lyric and corresponding tablature data
# The program then tries to fit these sections within the chosen output dimensions (currently A4)
# as best as it can, shrinking or growing sections to fit the remaining space
#
# @section notes Notes
# - Splitting raw text into lyric and tablature info is very basic at the moment.
# We need a better way to classify & split the various channels (raw tab, lyrics, chords, more?) that can be expected in tablature
#
# @section todo TODO
# - Various prints should be printed at specific log levels, to easily switch between debug, info or warnings only
import lib.chordFinder import lib.chordFinder
import lib.dataStructures import lib.dataStructures
import lib.initSongs import lib.initSongs

View File

@ -1,5 +1,21 @@
# !/usr/bin/python #!/usr/bin/env python3
# This program converts Song objects to imgs printable on A4 paper ##
# @file output2img.py
#
# @brief This program converts the internal data structure to an image file
#
# @section description Description
# Generates PNG images of a specific dimension (currently A4) of tablature data
# Dynamically resizes specific sections to maximize using the entire paper (and avoid awkward page flips)
#
# @section notes Notes
# -
#
# @section todo TODO
# - A lot of this stuff is hardcoded. We want to write default fonts, sizes, colours, margins, dimensions, wanted amount of pages
# to a config file on first boot. Overwrite these if they get passed via CMD arguments (or manually by user)
# - Various prints should be printed at specific log levels, to easily switch between debug, info or warnings only
import os import os
import lib.dataStructures import lib.dataStructures
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
@ -23,25 +39,38 @@ fontColour = ()
topMargin = 10 topMargin = 10
leftMargin = 25 leftMargin = 25
# return expected height of rendering the complete current section """!@brief Calculates the height of rendered text
This function calculates the dimensions of each line of text
the section contains and returns the sum
@param section lib.dataStructures.Section object
@return the total height of the section
"""
def calcSectionHeight(section): def calcSectionHeight(section):
lineIterator = 0 lineIterator = 0
amountOfLines = len(section.lyrics) amountOfLines = len(section.lyrics)
heightSum = 0 heightSum = 0
# add section title # consider section title
headerWidth, headerHeight = fontChords.getsize(section.header) headerWidth, headerHeight = fontChords.getsize(section.header)
heightSum += headerHeight heightSum += headerHeight
while lineIterator < amountOfLines: while lineIterator < amountOfLines:
# Get chord&lyric line # Get chord&lyric line dimensions
lyricTextWidth, lyricTextHeight = fontLyrics.getsize(section.lyrics[lineIterator]) lyricTextWidth, lyricTextHeight = fontLyrics.getsize(section.lyrics[lineIterator])
chordTextWidth, chordTextHeight = fontChords.getsize(section.chords[lineIterator]) chordTextWidth, chordTextHeight = fontChords.getsize(section.chords[lineIterator])
heightSum += lyricTextHeight + chordTextHeight heightSum += lyricTextHeight + chordTextHeight
lineIterator += 1 lineIterator += 1
return heightSum return heightSum
"""!@brief Exports the song object to images
This function renders the metadata and sections
of a given Song object, and exports it as PNG to the destination folder.
It will create the folder if it does not exist yet.
It will overwrite existing images, but will not clear old images
@param folderLocation path to where we want the images
@param songObj lib.dataStructures.Song object
@return None
"""
def outputToImage(folderLocation, songObj): def outputToImage(folderLocation, songObj):
# Create target Directory if don't exist # Create target Directory if doesn't exist
if not os.path.exists(folderLocation): if not os.path.exists(folderLocation):
os.mkdir(folderLocation) os.mkdir(folderLocation)
print("Directory " , folderLocation , " Created ") print("Directory " , folderLocation , " Created ")
@ -58,7 +87,7 @@ def outputToImage(folderLocation, songObj):
# Write metadata # Write metadata
for line in songObj.metadata.split('\n'): for line in songObj.metadata.split('\n'):
# remove any unwanted characters from metadat # remove any unwanted characters from metadata
line = line.rstrip() line = line.rstrip()
if not line: if not line:
continue continue
@ -66,16 +95,19 @@ def outputToImage(folderLocation, songObj):
metadataTextWidth, metadataTextHeight = fontMetadata.getsize(line) metadataTextWidth, metadataTextHeight = fontMetadata.getsize(line)
draw.text((leftMargin,currentHeight), line, fill=(128, 128, 128), font=fontMetadata) draw.text((leftMargin,currentHeight), line, fill=(128, 128, 128), font=fontMetadata)
currentHeight += metadataTextHeight currentHeight += metadataTextHeight
# Margin between metadata and the first section
currentHeight += topMargin currentHeight += topMargin
# Iterate over each section
# NOTE: sections might be split into lists of pages containing a list of sections
# This change will occur when we add an arranger which resizes sections to fit pages better
for section in songObj.sections: for section in songObj.sections:
# Reset section specific variables
lineIterator = 0 lineIterator = 0
amountOfLines = len(section.lyrics) amountOfLines = len(section.lyrics)
if (amountOfLines != len(section.chords)): 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)) print("Cannot write this section to file, since it was not processed correctly. There are {} tablature lines and {} lyric lines. Aborting...".format(len(section.chords), amountOfLines))
return return
# See if it can fit on the current page - if it does not, write & reset # See if the section would fit on the current page - if it does not, write current buffered image & make the next image ready
if currentHeight + calcSectionHeight(section) > imageHeight: if currentHeight + calcSectionHeight(section) > imageHeight:
#print("overflow! starting with a new image") #print("overflow! starting with a new image")
outputLocation = folderLocation + "/" + str(imageNumber) + ".png" outputLocation = folderLocation + "/" + str(imageNumber) + ".png"
@ -84,11 +116,11 @@ def outputToImage(folderLocation, songObj):
currentHeight = topMargin currentHeight = topMargin
a4image = Image.new('RGB',(imageWidth, imageHeight),(background)) a4image = Image.new('RGB',(imageWidth, imageHeight),(background))
draw = ImageDraw.Draw(a4image) draw = ImageDraw.Draw(a4image)
# write section title
# add section title
headerWidth, headerHeight = fontChords.getsize(section.header) headerWidth, headerHeight = fontChords.getsize(section.header)
draw.text((leftMargin,currentHeight), section.header, fill=(0, 0, 0), font=fontChords) draw.text((leftMargin,currentHeight), section.header, fill=(0, 0, 0), font=fontChords)
currentHeight += headerHeight currentHeight += headerHeight
# Write each line tablature&lyric data
while lineIterator < amountOfLines: while lineIterator < amountOfLines:
#print("Printing chord line {} and lyrics line {}".format(section.chords[lineIterator], section.lyrics[lineIterator])) #print("Printing chord line {} and lyrics line {}".format(section.chords[lineIterator], section.lyrics[lineIterator]))
# Get chord&lyric line # Get chord&lyric line
@ -101,9 +133,9 @@ def outputToImage(folderLocation, songObj):
currentHeight += lyricTextHeight currentHeight += lyricTextHeight
lineIterator += 1 lineIterator += 1
#print("currentheight={}".format(currentHeight)) #print("currentheight={}".format(currentHeight))
# Margin between each section
currentHeight += topMargin currentHeight += topMargin
# No more sections left, so the current buffered image is ready to be written to file
# Write remaining image to file as well
outputLocation = folderLocation + "/" + str(imageNumber) + ".png" outputLocation = folderLocation + "/" + str(imageNumber) + ".png"
a4image.save(outputLocation) a4image.save(outputLocation)