mirror of
https://github.com/stronk-dev/Guitar-Sheet-Parser.git
synced 2025-07-05 08:25:09 +02:00
parent
aea24b5eb9
commit
5be0f83c0d
@ -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
|
||||
#
|
||||
|
@ -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:
|
||||
|
@ -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 != ""):
|
||||
|
@ -1,10 +1,21 @@
|
||||
# This file takes a string corresponding to chord data and transposes it
|
||||
|
||||
#!/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']
|
||||
# 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
24
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 [<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
|
||||
|
@ -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)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user