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,6 +1,17 @@
# 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)
#!/usr/bin/env python3
##
# @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
# for each shape, generate finger position tab, like so:
# B x24442
@ -8,4 +19,3 @@
# Amaj7 x02120
# F#m 244222
# Am6 x04555
#

View File

@ -1,8 +1,24 @@
# !/usr/bin/python
# This file hosts the classes for storing song data
#!/usr/bin/env python3
##
# @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
# 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):
nonEmptyLines = ""
lines = inputString.split("\n")
@ -10,20 +26,28 @@ def stripEmptyLines(inputString):
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
"""!@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):
with open(inputFile, 'r') as file:
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:
return
#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):
#print("'{}' is a CHORD line, since it contains a number".format(inputString))
return True
# Assume CHORD line if any character {/, #, (, ), }
# Assume tablature 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))
@ -41,10 +65,11 @@ def isChordType(inputString):
#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))
#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
"""!@brief Class containing Section specific data
"""
class Section:
def __init__(self):
# List of lines of lyrics strings
@ -58,6 +83,9 @@ class Section:
# Flag for succesfully parsed
self.isParsed = False
"""!@brief Converts raw buffered data into separate Lyric and tablature lines
@return None
"""
# Parses self.rawData into lyrics and chord strings
def parseMe(self):
isFirstLine = True
@ -66,7 +94,7 @@ class Section:
lines = self.rawData.split('\r\n')
for line in lines:
# Determine lyric or chord line
currentIsChord = isChordType(line)
currentIsChord = isTablatureData(line)
# Initially just fill in the first line correctly
if isFirstLine:
isFirstLine = False
@ -91,20 +119,21 @@ class Section:
self.chords.append(line)
else:
self.lyrics.append(line)
# move on to next line, save current type
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:
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
# Add a trailing 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
"""!@brief Class containing Song specific data
"""
class Song:
def __init__(self):
# Src file
@ -122,14 +151,16 @@ class Song:
# Flag for succesfully parsed
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):
# Fill raw data
# Get 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 []).
# While not EOF: build sections untill new section found.
delimiterIndex = parseData.find("[")
if delimiterIndex == -1:
print("Cannot parse input file, since it is not delimited by '[<sectionName>]' entries")
@ -140,20 +171,21 @@ class Song:
parseData = parseData[delimiterIndex:]
# We are now at the start of the first section, at the '[' character
while parseData:
# Init new Section object
thisSection = Section()
# Get first line
# Get header on the 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
# Skip the ']\r\n' characters
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
# Set thisSection's data to remaining buffer
thisSection.rawData = parseData
parseData = ""
else:

View File

@ -1,11 +1,34 @@
# !/usr/bin/python
# Iterate through input folders and create a list of Song objects
#!/usr/bin/env python3
##
# @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 os
# For now manually whitelist folders to convert
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):
thisSong = lib.dataStructures.Song()
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))
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():
# path to song folders, which MAY contain a .txt source file
txtFileLocations = []
# list of Song objects
songList = []
# get all subdirectories
# go through all input locations. find .txt files.
for inputFolder in whitelist:
for root, dirs, files in os.walk(inputFolder):
for name in files:
@ -34,7 +61,7 @@ def getSongObjects():
#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
# create list of Song objects
while(txtFileLocations):
filePath = txtFileLocations.pop()
if (filePath != ""):

View File

@ -1,10 +1,21 @@
# 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:
#!/usr/bin/env python3
##
# @file transpose.py
#
# @brief This file takes a string corresponding to chord data and transposes it
#
# @section description Description
# -
#
# @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
# This program converts all songs in a given directory to a printable format
#!/usr/bin/env python3
##
# @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.dataStructures
import lib.initSongs

View File

@ -1,5 +1,21 @@
# !/usr/bin/python
# This program converts Song objects to imgs printable on A4 paper
#!/usr/bin/env python3
##
# @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 lib.dataStructures
from PIL import Image, ImageDraw, ImageFont
@ -23,25 +39,38 @@ fontColour = ()
topMargin = 10
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):
lineIterator = 0
amountOfLines = len(section.lyrics)
heightSum = 0
# add section title
# consider section title
headerWidth, headerHeight = fontChords.getsize(section.header)
heightSum += headerHeight
while lineIterator < amountOfLines:
# Get chord&lyric line
# Get chord&lyric line dimensions
lyricTextWidth, lyricTextHeight = fontLyrics.getsize(section.lyrics[lineIterator])
chordTextWidth, chordTextHeight = fontChords.getsize(section.chords[lineIterator])
heightSum += lyricTextHeight + chordTextHeight
lineIterator += 1
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):
# Create target Directory if don't exist
# Create target Directory if doesn't exist
if not os.path.exists(folderLocation):
os.mkdir(folderLocation)
print("Directory " , folderLocation , " Created ")
@ -58,7 +87,7 @@ def outputToImage(folderLocation, songObj):
# Write metadata
for line in songObj.metadata.split('\n'):
# remove any unwanted characters from metadat
# remove any unwanted characters from metadata
line = line.rstrip()
if not line:
continue
@ -66,16 +95,19 @@ def outputToImage(folderLocation, songObj):
metadataTextWidth, metadataTextHeight = fontMetadata.getsize(line)
draw.text((leftMargin,currentHeight), line, fill=(128, 128, 128), font=fontMetadata)
currentHeight += metadataTextHeight
# Margin between metadata and the first section
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:
# Reset section specific variables
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))
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
# 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:
#print("overflow! starting with a new image")
outputLocation = folderLocation + "/" + str(imageNumber) + ".png"
@ -84,11 +116,11 @@ def outputToImage(folderLocation, songObj):
currentHeight = topMargin
a4image = Image.new('RGB',(imageWidth, imageHeight),(background))
draw = ImageDraw.Draw(a4image)
# add section title
# write section title
headerWidth, headerHeight = fontChords.getsize(section.header)
draw.text((leftMargin,currentHeight), section.header, fill=(0, 0, 0), font=fontChords)
currentHeight += headerHeight
# Write each line tablature&lyric data
while lineIterator < amountOfLines:
#print("Printing chord line {} and lyrics line {}".format(section.chords[lineIterator], section.lyrics[lineIterator]))
# Get chord&lyric line
@ -101,9 +133,9 @@ def outputToImage(folderLocation, songObj):
currentHeight += lyricTextHeight
lineIterator += 1
#print("currentheight={}".format(currentHeight))
# Margin between each section
currentHeight += topMargin
# Write remaining image to file as well
# No more sections left, so the current buffered image is ready to be written to file
outputLocation = folderLocation + "/" + str(imageNumber) + ".png"
a4image.save(outputLocation)