mirror of
https://github.com/stronk-dev/Guitar-Sheet-Parser.git
synced 2025-07-04 16:25:08 +02:00
Initial Commit of prototype
This commit is contained in:
commit
2e29859e1c
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
__pycache__/
|
BIN
fonts/CourierPrime-Bold.ttf
Normal file
BIN
fonts/CourierPrime-Bold.ttf
Normal file
Binary file not shown.
BIN
fonts/CourierPrime-BoldItalic.ttf
Normal file
BIN
fonts/CourierPrime-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
fonts/CourierPrime-Italic.ttf
Normal file
BIN
fonts/CourierPrime-Italic.ttf
Normal file
Binary file not shown.
BIN
fonts/CourierPrime-Regular.ttf
Normal file
BIN
fonts/CourierPrime-Regular.ttf
Normal file
Binary file not shown.
0
lib/__init__.py
Normal file
0
lib/__init__.py
Normal file
11
lib/chordFinder.py
Normal file
11
lib/chordFinder.py
Normal 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
183
lib/dataStructures.py
Normal 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
42
lib/initSongs.py
Normal 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
10
lib/transpose.py
Normal 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
24
main.py
Normal 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
122
output2img.py
Normal 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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user