From 5be0f83c0db634d22c73a14c5c86c02d535470f3 Mon Sep 17 00:00:00 2001 From: Marco van Dijk Date: Wed, 7 Jul 2021 23:01:52 +0200 Subject: [PATCH] fixes #9 Add comments to start of files and functions --- lib/chordFinder.py | 20 +++++++++--- lib/dataStructures.py | 76 ++++++++++++++++++++++++++++++------------- lib/initSongs.py | 37 ++++++++++++++++++--- lib/transpose.py | 31 ++++++++++++------ main.py | 24 ++++++++++++-- output2img.py | 64 +++++++++++++++++++++++++++--------- 6 files changed, 192 insertions(+), 60 deletions(-) diff --git a/lib/chordFinder.py b/lib/chordFinder.py index b0bc480..3a85021 100644 --- a/lib/chordFinder.py +++ b/lib/chordFinder.py @@ -1,11 +1,21 @@ -# 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: +# for each shape, generate finger position tab, like so: # B x24442 # C#m x46654 # Amaj7 x02120 # F#m 244222 # Am6 x04555 -# diff --git a/lib/dataStructures.py b/lib/dataStructures.py index 38e3435..687b5dd 100644 --- a/lib/dataStructures.py +++ b/lib/dataStructures.py @@ -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 '[]' 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 '[]'") 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: diff --git a/lib/initSongs.py b/lib/initSongs.py index 500948d..6db37c3 100644 --- a/lib/initSongs.py +++ b/lib/initSongs.py @@ -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 != ""): diff --git a/lib/transpose.py b/lib/transpose.py index 518d8f4..17c6312 100644 --- a/lib/transpose.py +++ b/lib/transpose.py @@ -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: - # 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 +#!/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'] \ No newline at end of file diff --git a/main.py b/main.py index 8f2c756..1758d61 100644 --- a/main.py +++ b/main.py @@ -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 [] 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 diff --git a/output2img.py b/output2img.py index 9846d00..00dcc2b 100644 --- a/output2img.py +++ b/output2img.py @@ -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)