Jan-01-2022, 12:19 PM
For a long time I am developing a music notation app called PianoScript and after finishing the first app which was like lilypond(editing the savefile directly inside tkinter text widget) I decided to build a more graphical app where you can point and click the notes on the page. What I have done is inventing my own save file format:
.
I am searching for a tkinter code example that does exactly this:
* inserting a oval/circle on left-mouseclick
* saving the tkinter ovals on the canvas inside a file and being able to open it again
* editing the position of every oval in the file by click and drag on it
Following these three points you need to connect every instance of an oval to the data in the save file and that is exactly the problem I want to solve/understand. What are possible ways to do this?
My code:
#---------------------
# save file structure
#---------------------
'''
File structure:
A *.pnoscript file consists of a python list which contains
two parts(in nested lists):
* score setup;
* time signatures
* page layout(like margins, measures each line etc)
* titles(like title, composer etc)
* cursor
* musical data;
* notes
* text; to describe the music/how to play the sequense
'''
file = [
# score setup part:
[
# t_sig_map; inside list because the order of the t_sig messages defines the time changes/score.
[
{'type':'time_signature','amount':150, 'numerator':4, 'denominator':4, 'grid':4, 'visible':1},
#{'type':'time_signature','amount':4, 'numerator':6, 'denominator':8, 'grid':2, 'visible':1},
],
# mp_line
{'type':'mp_line', 'string':'5'},
# titles
{'type':'title', 'text':'Test_Version'},
{'type':'composer', 'text':'PianoScript'},
{'type':'copyright', 'text':'copyrights reserved 2022'},
# scale; global scale
{'type':'scale', 'value':1},
# page margins
{'type':'margin', 'value':50},
# space under systems / in between
{'type':'system_space', 'value':60},
# cursor
{'type':'cursor', 'time':0, 'duration':256, 'note':40}
],
# musical data part:
[
# notes
{'type':'note', 'time':0, 'duration':16384, 'note':52, 'hand':0, 'beam':0, 'slur':0},
{'type':'note', 'time':0, 'duration':256, 'note':28, 'hand':1, 'beam':0, 'slur':0},
{'type':'note', 'time':0, 'duration':512, 'note':40, 'hand':0, 'beam':0, 'slur':0},
# text
{'type':'text', 'time':0, 'text':'play', 'bold':0, 'italic':1, 'underline':0}
]
]The current application can import midi-files basically which is really cool(!) but now I need to make the editor that can add notes(in the above dictionary structure), edit notes, and remove notes. So the problem is that I need to find a way to connect the drawn notes on the tkinter canvas to the corresponding note message/dictionary. In the lilypond like application the user had to search for the right position in the file but now I have to program this
. I am searching for a tkinter code example that does exactly this:
* inserting a oval/circle on left-mouseclick
* saving the tkinter ovals on the canvas inside a file and being able to open it again
* editing the position of every oval in the file by click and drag on it
Following these three points you need to connect every instance of an oval to the data in the save file and that is exactly the problem I want to solve/understand. What are possible ways to do this?
My code:
#--------------------
# IMPORTS
#--------------------
from tkinter import Tk,Canvas,Menu,Scrollbar,messagebox,PanedWindow,Listbox
from tkinter import simpledialog,filedialog,Frame,Button,Entry,Label,Spinbox
import time,ast,platform,subprocess,os,sys,errno,math
from mido import MidiFile
from shutil import which
import tkinter.ttk as ttk
#--------------------
# GUI
#--------------------
_bg = '#333333' #d9d9d9
# root
root = Tk()
root.title('PianoScript')
ttk.Style(root).theme_use("alt")
scrwidth = root.winfo_screenwidth()
scrheight = root.winfo_screenheight()
root.geometry("%sx%s+0+0" % (int(scrwidth), int(scrheight)))
# PanedWindow
orient = 'h'
# master
panedmaster = PanedWindow(root, orient='v', sashwidth=20, relief='flat', bg=_bg, sashcursor='arrow')
panedmaster.place(relwidth=1, relheight=1)
uppanel = PanedWindow(panedmaster, height=10000, relief='flat', bg=_bg)
panedmaster.add(uppanel)
downpanel = PanedWindow(panedmaster, relief='flat', bg=_bg)
panedmaster.add(downpanel)
# editor panel
paned = PanedWindow(uppanel, relief='flat', sashwidth=20, sashcursor='arrow', orient='h', bg=_bg)
uppanel.add(paned)
# left panel
leftpanel = PanedWindow(paned, relief='flat', bg=_bg)
paned.add(leftpanel)
# right panel
rightpanel = PanedWindow(paned, sashwidth=15, sashcursor='arrow', relief='flat', bg=_bg)
paned.add(rightpanel)
# editor --> leftpanel
editor = Canvas(rightpanel, bg='black', relief='flat')
editor.place(relwidth=1, relheight=1)
vbar = Scrollbar(editor, orient='vertical', width=20, relief='flat', bg=_bg)
vbar.pack(side='right', fill='y')
vbar.config(command=editor.yview)
editor.configure(yscrollcommand=vbar.set)
hbar = Scrollbar(editor, orient='horizontal', width=20, relief='flat', bg=_bg)
hbar.pack(side='bottom', fill='x')
hbar.config(command=editor.xview)
editor.configure(xscrollcommand=hbar.set)
editor.create_window(0,0, width=100,height=20, window=Button(editor, text='<').place())
# piano-keyboard-editor
# piano = Canvas(downpanel, bg='black', relief='flat')
# downpanel.add(piano)
def scrollD(event):
editor.yview('scroll', int(event.y/200), 'units')
#editor.configure(scrollregion=bbox_offset(editor.bbox("all")))
def scrollU(event):
editor.yview('scroll', -abs(int(event.y/200)), 'units')
# linux scroll
if platform.system() == 'Linux':
root.bind("<5>", scrollD)
root.bind("<4>", scrollU)
# mac scroll
if platform.system() == 'Darwin':
def _on_mousewheel(event):
editor.yview_scroll(-1*(event.delta), "units")
editor.bind("<MouseWheel>", _on_mousewheel)
# windows scroll
if platform.system() == 'Windows':
def _on_mousewheel(event):
editor.yview_scroll(int(-1*(event.delta)/120), "units")
editor.bind("<MouseWheel>", _on_mousewheel)
# score setup --> rightpanel
separator1 = ttk.Separator(leftpanel, orient='horizontal').pack(fill='x')
fill_label1 = Label(leftpanel, text='TITLES',bg=_bg,fg='white',anchor='w')
fill_label1.pack(fill='x')
title_label = Label(leftpanel, text='Title: ',bg=_bg,fg='white',anchor='w')
title_label.pack(fill='x')
title_entry = Entry(leftpanel)
title_entry.pack(fill='x')
composer_label = Label(leftpanel, text='Composer: ',bg=_bg,fg='white',anchor='w')
composer_label.pack(fill='x')
composer_entry = Entry(leftpanel)
composer_entry.pack(fill='x')
copyright_label = Label(leftpanel, text='Copyright: ',bg=_bg,fg='white',anchor='w')
copyright_label.pack(fill='x')
copyright_entry = Entry(leftpanel)
copyright_entry.pack(fill='x')
fill_label2 = Label(leftpanel, text='',bg=_bg,fg='white',anchor='w')
fill_label2.pack(fill='x')
separator2 = ttk.Separator(leftpanel, orient='horizontal').pack(fill='x')
fill_label3 = Label(leftpanel, text='LAYOUT',bg=_bg,fg='white',anchor='w')
fill_label3.pack(fill='x')
mpline_label = Label(leftpanel, text='Measures each line: ',bg=_bg,fg='white',anchor='w')
mpline_label.pack(fill='x')
mpline_entry = Entry(leftpanel)
mpline_entry.pack(fill='x')
scale_label = Label(leftpanel, text='Global scale: ',bg=_bg,fg='white',anchor='w')
scale_label.pack(fill='x')
scale_entry = Entry(leftpanel)
scale_entry.pack(fill='x')
margin_label = Label(leftpanel, text='Margin: ',bg=_bg,fg='white',anchor='w')
margin_label.pack(fill='x')
margin_entry = Entry(leftpanel)
margin_entry.pack(fill='x')
system_label = Label(leftpanel, text='Space under system: ',bg=_bg,fg='white',anchor='w')
system_label.pack(fill='x')
system_entry = Entry(leftpanel)
system_entry.pack(fill='x')
apply_label = Label(leftpanel, text='',bg=_bg,fg='white',anchor='w')
apply_label.pack(fill='x')
apply_button = Button(leftpanel, text='Apply to score')
apply_button.pack(fill='x')
# fill_label4 = Label(leftpanel, text='',bg=_bg,fg='white',anchor='w')
# fill_label4.pack(fill='x')
# separator2 = ttk.Separator(leftpanel, orient='horizontal').pack(fill='x')
# noteinput_label = Label(leftpanel, text='NOTE INPUT',bg=_bg,fg='white',anchor='w')
# noteinput_label.pack(fill='x')
# length_label = Label(leftpanel, text='Note length:', anchor='w', bg=_bg, fg='white')
# length_label.pack(fill='x')
# list_dur = Listbox(leftpanel, height=8)
# list_dur.pack(fill='x')
# list_dur.insert(0, "1 whole")
# list_dur.insert(1, "2 half")
# list_dur.insert(2, "4 quarter")
# list_dur.insert(3, "8 eight")
# list_dur.insert(4, "16 sixteenth")
# list_dur.insert(5, "32 ...")
# list_dur.insert(6, "64 ...")
# list_dur.insert(7, "128 ...")
# divide_label = Label(leftpanel, text='รท', font=("courier", 20, "bold"), bg=_bg, fg='white')
# divide_label.pack(fill='x')
# divide_spin = Spinbox(leftpanel, from_=1, to=20)
# divide_spin.pack(fill='x')
#------------------
# constants
#------------------
QUARTER = 256
MM = root.winfo_fpixels('1m')
PAPER_HEIGHT = MM * 297 # a4 210x297 mm
PAPER_WIDTH = MM * 210
XYPAPER = 30
MARGIN = 30
PRINTEAREA_WIDTH = PAPER_WIDTH - (MARGIN*2)
PRINTEAREA_HEIGHT = PAPER_HEIGHT - (MARGIN*2)
MIDINOTECOLOR = '#b4b4b4'
#--------------------------------------------------------
# TOOLS (notation design, help functions etc...)
#--------------------------------------------------------
def measure_length(tsig):
'''
tsig is a tuple containg; (numerator, denominator)
returns the length in ticks where tpq == ticks per quarter
'''
w = 0
tpq = QUARTER
n = tsig[0]
d = tsig[1]
if d < 4:
w = (n * d) / (d / 2)
if d == 4:
w = (n * d) / d
if d > 4:
w = (n * d) / (d * 2)
return int(tpq * w)
def is_in_range(x, y, z):
'''
returns true if z is in between x and y.
'''
if x < z and y > z:
return True
else:
return False
def barline_pos(t_sig_map):
'''
This functions returns a list of all barline
positions in the score based on the time signatures.
'''
b_lines = []
bln_time = 0
for i in t_sig_map:
meas_len = measure_length((i['numerator'], i['denominator']))
for meas in range(0,i['amount']):
b_lines.append(bln_time)
bln_time += meas_len
return b_lines
def note_split_processor(note, t_sig_map):
'''
Returns a list of notes and if nessesary note split
'''
out = []
# creating a list of barline positions.
b_lines = barline_pos(t_sig_map)
# detecting barline overlapping note.
is_split = False
split_points = []
for i in b_lines:
if is_in_range(note['time'], note['time']+note['duration'], i):
split_points.append(i)
is_split = True
if is_split == False:
out.append(note)
return out
elif is_split == True:
start = note['time']
end = note['time']+note['duration']
for i in range(0,len(split_points)+1):
if i == 0:# if first iteration
out.append({'type':'note', 'note':note['note'], 'time':start, 'duration':split_points[0]-start, 'hand':0, 'beam':0, 'slur':0})
elif i == len(split_points):# if last iteration
out.append({'type':'split', 'note':note['note'], 'time':split_points[i-1], 'duration':end-split_points[i-1], 'hand':0, 'beam':0, 'slur':0})
return out
else:# if not first and not last iteration
out.append({'type':'split', 'note':note['note'], 'time':split_points[i-1], 'duration':split_points[i]-split_points[i-1], 'hand':0, 'beam':0, 'slur':0})
def bbox_offset(bbox, offset):
x1, y1, x2, y2 = bbox
return (x1-offset, y1-offset, x2+offset, y2+offset)
def staff_height(mn, mx, scale):
'''
This function returns the height of a staff based on the
lowest and highest piano-key-number.
'''
staffheight = 0
if mx >= 81:
staffheight = 260
if mx >= 76 and mx <= 80:
staffheight = 220
if mx >= 69 and mx <= 75:
staffheight = 190
if mx >= 64 and mx <= 68:
staffheight = 150
if mx >= 57 and mx <= 63:
staffheight = 120
if mx >= 52 and mx <= 56:
staffheight = 80
if mx >= 45 and mx <= 51:
staffheight = 50
if mx >= 40 and mx <= 44:
staffheight = 10
if mx < 40:
staffheight = 10
if mn >= 33 and mn <= 39:
staffheight += 40
if mn >= 28 and mn <= 32:
staffheight += 70
if mn >= 21 and mn <= 27:
staffheight += 110
if mn >= 16 and mn <= 20:
staffheight += 140
if mn >= 9 and mn <= 15:
staffheight += 180
if mn >= 4 and mn <= 8:
staffheight += 210
if mn >= 1 and mn <= 3:
staffheight += 230
return staffheight * scale
def new_line_pos(t_sig_map, mp_line):
'''
returns a list of the position of every new line of music.
'''
b_pos = barline_pos(t_sig_map)
new_lines = []
count = 0
for bl in range(len(b_pos)):
try:
new_lines.append(b_pos[count])
except IndexError:
new_lines.append(end_bar_tick(t_sig_map))
break
try:
count += mp_line[bl]
except IndexError:
count += mp_line[-1]
return new_lines
def draw_staff_lines(y, mn, mx, scale):
'''
'y' takes the y-position of the uppper line of the staff.
'mn' and 'mx' take the lowest and highest note in the staff
so the function can draw the needed lines.
'scale' prints the staff on a different scale where 1 is
the default/normal size.
'''
def draw3line(y):
x = XYPAPER + MARGIN
editor.create_line(x, y, x+PRINTEAREA_WIDTH, y, width=2, capstyle='round')
editor.create_line(x, y+(10*scale), x+PRINTEAREA_WIDTH, y+(10*scale), width=2, capstyle='round')
editor.create_line(x, y+(20*scale), x+PRINTEAREA_WIDTH, y+(20*scale), width=2, capstyle='round')
def draw2line(y):
x = XYPAPER + MARGIN
editor.create_line(x, y, x+PRINTEAREA_WIDTH, y, width=0.5, capstyle='round')
editor.create_line(x, y+(10*scale), x+PRINTEAREA_WIDTH, y+(10*scale), width=0.5, capstyle='round')
def draw_dash2line(y):
x = XYPAPER + MARGIN
if platform.system() == 'Linux' or platform.system() == 'Darwin':
editor.create_line(x, y, x+PRINTEAREA_WIDTH, y, width=1, dash=(6,6), capstyle='round')
editor.create_line(x, y+(10*scale), x+PRINTEAREA_WIDTH, y+(10*scale), width=1, dash=(6,6), capstyle='round')
elif platform.system() == 'Windows':
editor.create_line(x, y, x+PRINTEAREA_WIDTH, y, width=1, dash=4, capstyle='round')
editor.create_line(x, y+(10*scale), x+PRINTEAREA_WIDTH, y+(10*scale), width=1, dash=4, capstyle='round')
keyline = 0
if mx >= 81:
draw3line(0+y)
draw2line((40*scale)+y)
draw3line((70*scale)+y)
draw2line((110*scale)+y)
draw3line((140*scale)+y)
draw2line((180*scale)+y)
draw3line((210*scale)+y)
keyline = (250*scale)
if mx >= 76 and mx <= 80:
draw2line(0+y)
draw3line((30*scale)+y)
draw2line((70*scale)+y)
draw3line((100*scale)+y)
draw2line((140*scale)+y)
draw3line((170*scale)+y)
keyline = (210*scale)
if mx >= 69 and mx <= 75:
draw3line(0+y)
draw2line((40*scale)+y)
draw3line((70*scale)+y)
draw2line((110*scale)+y)
draw3line((140*scale)+y)
keyline = 180*scale
if mx >= 64 and mx <= 68:
draw2line(0+y)
draw3line((30*scale)+y)
draw2line((70*scale)+y)
draw3line((100*scale)+y)
keyline = 140*scale
if mx >= 57 and mx <= 63:
draw3line(0+y)
draw2line((40*scale)+y)
draw3line((70*scale)+y)
keyline = 110*scale
if mx >= 52 and mx <= 56:
draw2line(0+y)
draw3line((30*scale)+y)
keyline = 70*scale
if mx >= 45 and mx <= 51:
draw3line(0+y)
keyline = 40*scale
draw_dash2line(keyline+y)
if mn >= 33 and mn <= 39:
draw3line(keyline+(30*scale)+y)
if mn >= 28 and mn <= 32:
draw3line(keyline+(30*scale)+y)
draw2line(keyline+(70*scale)+y)
if mn >= 21 and mn <= 27:
draw3line(keyline+(30*scale)+y)
draw2line(keyline+(70*scale)+y)
draw3line(keyline+(100*scale)+y)
if mn >= 16 and mn <= 20:
draw3line(keyline+(30*scale)+y)
draw2line(keyline+(70*scale)+y)
draw3line(keyline+(100*scale)+y)
draw2line(keyline+(140*scale)+y)
if mn >= 9 and mn <= 15:
draw3line(keyline+(30*scale)+y)
draw2line(keyline+(70*scale)+y)
draw3line(keyline+(100*scale)+y)
draw2line(keyline+(140*scale)+y)
draw3line(keyline+(170*scale)+y)
if mn >= 4 and mn <= 8:
draw3line(keyline+(30*scale)+y)
draw2line(keyline+(70*scale)+y)
draw3line(keyline+(100*scale)+y)
draw2line(keyline+(140*scale)+y)
draw3line(keyline+(170*scale)+y)
draw2line(keyline+(210*scale)+y)
if mn >= 1 and mn <= 3:
draw3line(keyline+(30*scale)+y)
draw2line(keyline+(70*scale)+y)
draw3line(keyline+(100*scale)+y)
draw2line(keyline+(140*scale)+y)
draw3line(keyline+(170*scale)+y)
draw2line(keyline+(210*scale)+y)
editor.create_line(XYPAPER + MARGIN, (keyline+(240*scale)+y), XYPAPER + MARGIN + PRINTEAREA_WIDTH, (keyline+(240*scale)+y), width=2)
def get_staff_height(line, scale):
#create linenotelist
linenotelist = []
for note in line:
if note[0] == 'note' or note[0] == 'split' or note[0] == 'invis' or note[0] == 'cursor':
linenotelist.append(note[3])
if linenotelist:
minnote = min(linenotelist)
maxnote = max(linenotelist)
else:
minnote = 40
maxnote = 44
return staff_height(minnote, maxnote, scale), minnote, maxnote
def end_bar_tick(t_sig_map):
'''
Returns the tick of the end-barline.
'''
bln_time = 0
for i in t_sig_map:
meas_len = measure_length((i['numerator'], i['denominator']))
for meas in range(0,i['amount']):
bln_time += meas_len
return bln_time
def event_x_pos(pos, linenr, newlinepos):
'''
returns the x position on the paper based on
position in piano-ticks and line-number.
'''
newlinepos.append(end_bar_tick(t_sig_map))
linelength = newlinepos[linenr] - newlinepos[linenr-1]
factor = PRINTEAREA_WIDTH / linelength
pos = pos - newlinepos[linenr-1]
xpos = XYPAPER + MARGIN + pos * factor
return xpos
def t_sig_start_tick(t_sig_map,n):
out = []
tick = 0
for i in t_sig_map:
out.append(tick)
tick += measure_length((i['numerator'], i['denominator'])) * i['amount']
return out[n]
def process_margin(value):
global QUARTER,MM,PAPER_HEIGHT,PAPER_WIDTH,XYPAPER
global MARGIN,PRINTEAREA_WIDTH,PRINTEAREA_HEIGHT
QUARTER = 256
MM = root.winfo_fpixels('1m')
PAPER_HEIGHT = MM * 297 # a4 210x297 mm
PAPER_WIDTH = MM * 210
XYPAPER = 30
MARGIN = value
PRINTEAREA_WIDTH = PAPER_WIDTH - (MARGIN*2)
PRINTEAREA_HEIGHT = PAPER_HEIGHT - (MARGIN*2)
def note_active_grey(x0, x1, y, linenr, new_line):
'''draws a midi note with a stop sign(vertical line at the end of the midi-note).'''
x0 = event_x_pos(x0, linenr, new_line)
x1 = event_x_pos(x1, linenr, new_line)
editor.create_rectangle(x0, y-5, x1, y+5, fill='#e3e3e3', outline='')#e3e3e3
editor.create_line(x1, y-5, x1, y+5, width=2)
editor.create_line(x0, y-5, x0, y+5, width=2, fill='#e3e3e3')
def note_y_pos(note, mn, mx, cursy, scale):
'''
returns the position of the given note relative to 'cursy'(the y axis staff cursor).
'''
ylist = [495, 490, 485, 475, 470, 465, 460, 455, 445, 440, 435, 430, 425, 420, 415,
405, 400, 395, 390, 385, 375, 370, 365, 360, 355, 350, 345, 335, 330, 325, 320, 315,
305, 300, 295, 290, 285, 280, 275, 265, 260, 255, 250, 245, 235, 230, 225, 220, 215,
210, 205, 195, 190, 185, 180, 175, 165, 160, 155, 150, 145, 140, 135, 125, 120, 115,
110, 105, 95, 90, 85, 80, 75, 70, 65, 55, 50, 45, 40, 35, 25, 20, 15, 10, 5, 0, -5, -15]
sub = 0
if mx >= 81:
sub = 0
if mx >= 76 and mx <= 80:
sub = 40
if mx >= 69 and mx <= 75:
sub = 70
if mx >= 64 and mx <= 68:
sub = 110
if mx >= 57 and mx <= 63:
sub = 140
if mx >= 52 and mx <= 56:
sub = 180
if mx >= 45 and mx <= 51:
sub = 210
if mx <= 44:
sub = 250
return cursy + (ylist[note-1]*scale) - (sub*scale)
def diff(x, y):
if x >= y:
return x - y
else:
return y - x
def note_active_gradient(x0, x1, y, linenr, scale):
'''draws a midi note with gradient'''
width = diff(x0, x1)
if width == 0:
width = 1
(r1,g1,b1) = root.winfo_rgb('white')
(r2,g2,b2) = root.winfo_rgb(MIDINOTECOLOR)
r_ratio = float(r2-r1) / width
g_ratio = float(g2-g1) / width
b_ratio = float(b2-b1) / width
for i in range(math.ceil(width)):
nr = int(r1 + (r_ratio * i))
ng = int(g1 + (g_ratio * i))
nb = int(b1 + (b_ratio * i))
color = "#%4.4x%4.4x%4.4x" % (nr,ng,nb)
editor.create_line(x0+i,y-(5*scale),x0+i,y+(5*scale), fill=color)
editor.create_line(x1, y-(5*scale), x1, y+(5*scale), width=2)
editor.create_line(x0, y-(5*scale), x0, y+(5*scale), width=2, fill='white')
def newpage_linenr(no, lst):
p_counter = 0
l_counter = 0
for page in lst:
if p_counter == no:
return l_counter
p_counter += 1
for line in page:
l_counter += 1
def newpage_barnr(no, lst):
p_counter = 0
b_counter = 0
for page in lst:
if p_counter == no:
return b_counter
p_counter += 1
for line in page:
for bar in line:
if bar[0] == 'barline':
b_counter += 1
#---------------------
# save file structure
#---------------------
'''
File structure:
A *.pnoscript file consists of a python list which contains
two parts(in nested lists):
* score setup;
* time signatures
* page layout(like margins, measures each line etc)
* titles(like title, composer etc)
* cursor
* musical data;
* notes
* text; to describe the music/how to play the sequense
'''
file = [
# score setup part:
[
# t_sig_map; inside list because the order of the t_sig messages defines the time changes/score.
[
{'type':'time_signature','amount':150, 'numerator':4, 'denominator':4, 'grid':4, 'visible':1},
#{'type':'time_signature','amount':4, 'numerator':6, 'denominator':8, 'grid':2, 'visible':1},
],
# mp_line
{'type':'mp_line', 'string':'5'},
# titles
{'type':'title', 'text':'Test_Version'},
{'type':'composer', 'text':'PianoScript'},
{'type':'copyright', 'text':'copyrights reserved 2022'},
# scale; global scale
{'type':'scale', 'value':1},
# page margins
{'type':'margin', 'value':50},
# space under systems / in between
{'type':'system_space', 'value':60},
# cursor
{'type':'cursor', 'time':0, 'duration':256, 'note':40}
],
# musical data part:
[
# notes
#{'type':'note', 'time':0, 'duration':16384, 'note':52, 'hand':0, 'beam':0, 'slur':0},
# {'type':'note', 'time':0, 'duration':256, 'note':28, 'hand':1, 'beam':0, 'slur':0},
# {'type':'note', 'time':0, 'duration':512, 'note':40, 'hand':0, 'beam':0, 'slur':0},
# text
{'type':'text', 'time':0, 'text':'play', 'bold':0, 'italic':1, 'underline':0}
]
]
#------------------
# file management
#------------------
file_changed = 0
def new_file():
if file_changed == 1:
save_quest()
mpline_entry.delete(0,'end')
title_entry.delete(0,'end')
composer_entry.delete(0,'end')
copyright_entry.delete(0,'end')
global file
file = [
# score setup part:
[
# t_sig_map; inside list because the order of the t_sig messages defines the time changes/score.
[
{'type':'time_signature','amount':32, 'numerator':4, 'denominator':4, 'grid':4, 'visible':1},
],
# mp_line
{'type':'mp_line', 'string':'4'},
# titles
{'type':'title', 'text':'Untitled'},
{'type':'composer', 'text':'PianoScript'},
{'type':'copyright', 'text':'copyrights reserved 2022'},
# scale; global scale
{'type':'scale', 'value':1},
# page margins
{'type':'margin', 'value':50},
# space under systems / in between
{'type':'system_space', 'value':60}
],
# musical data part:
[
]
]
render('normal', 0)
def open_file():
print('open_file')
f = filedialog.askopenfile(parent=root, mode='Ur', title='Open', filetypes=[("PianoScript files","*.pnoscript")], initialdir='~/Desktop/')
if f:
mpline_entry.delete(0,'end')
title_entry.delete(0,'end')
composer_entry.delete(0,'end')
copyright_entry.delete(0,'end')
filepath = f.name
root.title('PnoScript - %s' % f.name)
f = open(f.name, 'r', newline=None)
global file
file = ast.literal_eval(f.read())
f.close()
render('normal', 0)
return
def save_file():
save_as()
def save_as():
f = filedialog.asksaveasfile(parent=root, mode='w', filetypes=[("PianoScript files","*.pnoscript")], initialdir='~/Desktop/')
if f:
root.title('PnoScript - %s' % f.name)
f = open(f.name, 'w')
f.write(str(file))
f.close()
return
def save_quest():
if messagebox.askyesno('Wish to save?', 'Do you wish to save the current file?'):
save_file()
else:
return
#--------------
# draw engine
#--------------
t_sig_map = []
mp_line = []
msg = []
page_space = []
new_line = []
title = ''
composer = ''
copyright = ''
system_space = 0
header_space = 50
scale = 1
paper_color = 0
view_page = 0
cursor = (0,40)
grid_step = 128
note_write = 0
def render(render_type, pageno=0):
# check if there is a time_signature in the file.
if not file[0][0]:
print('ERROR: There is no time signature in the file!')
return
# remove all objects from the canvas.
editor.delete('all')
global paper_color, view_page
if render_type == 'normal':
paper_color = '#ffe4de'
elif render_type == 'export':
paper_color = 'white'
def read_score_setup():
'''
Reads and writes the score setup
settings to the file; as well
inserts the score settings to the
score setup entry's.
'''
# insert from file
for i in file[0]:
if isinstance(i,dict):
if i['type'] == 'mp_line':
mpline_entry.delete(0,'end')
mpline_entry.insert(0,i['string'])
if i['type'] == 'scale':
scale_entry.delete(0,'end')
scale_entry.insert(0,i['value'])
if i['type'] == 'title':
title_entry.delete(0,'end')
title_entry.insert(0,i['text'])
if i['type'] == 'composer':
composer_entry.delete(0,'end')
composer_entry.insert(0,i['text'])
if i['type'] == 'copyright':
copyright_entry.delete(0,'end')
copyright_entry.insert(0,i['text'])
if i['type'] == 'margin':
margin_entry.delete(0,'end')
margin_entry.insert(0,i['value'])
if i['type'] == 'system_space':
system_entry.delete(0,'end')
system_entry.insert(0,i['value'])
read_score_setup()
def read():
'''
This function reads the file and translates it
to a msg list; list containing nested lists:
[pages[lines[notes]lines]pages]
and it writes all settings to the right variables.
'''
# init utils lists and variables.
global t_sig_map,mp_line,msg,MARGIN,title,composer,copyright
global system_space,new_line,page_space,cursor,scale
t_sig_map = []
mp_line = []
msg = []
page_space = []
title = ''
composer = ''
copyright = ''
system_space = 0
# score setup part:
# time_signature
bln_time = 0
grd_time = 0
count = 0
for i in file[0][0]:
if i['type'] == 'time_signature':
t_sig_map.append(i)
# barline and grid messages
meas_len = measure_length((i['numerator'], i['denominator']))
grid_len = meas_len / i['grid']
for meas in range(0,i['amount']):
msg.append(['barline', bln_time])
for grid in range(0,i['grid']):
msg.append(['gridline', grd_time])
grd_time += grid_len
bln_time += meas_len
msg.append(['time_signature_text',t_sig_start_tick(t_sig_map,count),
meas_len,str(i['numerator'])+'/'+str(i['denominator'])])
count += 1
# mp_line
for i in file[0]:
if isinstance(i,dict):
if i['type'] == 'mp_line':
try:
mpline = i['string'].split()
for mp in mpline:
mp_line.append(eval(mp))
except:
print('ERROR: mp_line string is not valid! mp_line is set to default value 5.')
mp_line.append(5)
# titles
if i['type'] == 'title':
title = i['text']
if i['type'] == 'composer':
composer = i['text']
if i['type'] == 'copyright':
copyright = i['text']
# scale
if i['type'] == 'scale':
scale = i['value']
# margin
if i['type'] == 'margin':
MARGIN = i['value']
process_margin(MARGIN)
# system_spacing
if i['type'] == 'system_space':
system_space = i['value']
# cursor
if i['type'] == 'cursor':
msg.append(['cursor',i['time'],i['duration'],i['note']])
# musical data part:
for i in file[1]:
# note
if i['type'] == 'note':
for note in note_split_processor(i, t_sig_map):
msg.append([note['type'], note['time'], note['duration'], note['note'], note['hand']])
# invisible note
if i['type'] == 'invis':
msg.append([note['type'], note['time'], note['duration'], note['note']])
# text
if i['type'] == 'text':
msg.append([i['type'], i['time'], i['text'], i['bold'], i['italic'], i['underline']])
# endline
msg.append(['endline', end_bar_tick(t_sig_map)])
# sort on starttime of event.
msg.sort(key=lambda x: x[1])
# placing the events in lists of lines.
new_line = new_line_pos(t_sig_map, mp_line)
msgs = msg
msg = []
count = 0
for ln in new_line[:-1]:
hlplst = []
for evt in msgs:
if evt[1] >= new_line[count] and evt[1] < new_line[count+1]:
hlplst.append(evt)
msg.append(hlplst)
count += 1
# placing the lines in lists of pages.
lineheight = []
for line in msg:
notelst = []
for note in line:
if note[0] == 'note' or note[0] == 'split' or note[0] == 'invis' or note[0] == 'cursor':
notelst.append(note[3])
else:
pass
try:
lineheight.append(staff_height(min(notelst), max(notelst), scale))
except ValueError:
lineheight.append(10*scale)
msgs = msg
msg = []
curs_y = header_space
pagelist = []
icount = 0
resspace = 0
header = header_space
for line, height in zip(msgs, lineheight):
icount += 1
curs_y += height + system_space
if icount == len(lineheight):#if this is the last iteration
if curs_y <= PRINTEAREA_HEIGHT - header:
pagelist.append(line)
msg.append(pagelist)
resspace = PRINTEAREA_HEIGHT - curs_y
page_space.append(resspace)
break
elif curs_y > PRINTEAREA_HEIGHT - header:
msg.append(pagelist)
pagelist = []
pagelist.append(line)
msg.append(pagelist)
page_space.append(resspace)
curs_y = 0
resspace = PRINTEAREA_HEIGHT - curs_y
page_space.append(resspace)
break
else:
pass
else:
if curs_y <= PRINTEAREA_HEIGHT - header:#does fit on paper
pagelist.append(line)
resspace = PRINTEAREA_HEIGHT - curs_y
elif curs_y > PRINTEAREA_HEIGHT - header:#does not fit on paper
msg.append(pagelist)
pagelist = []
pagelist.append(line)
curs_y = 0
curs_y += height + system_space
page_space.append(resspace)
header = 0
else:
pass
if pageno > len(msg)-1:
pageno = 0
view_page = 0
elif pageno < 0:
pageno = len(msg)-1
view_page = len(msg)-1
# for page in msg:
# print('newpage:')
# for line in page:
# print('newline:')
# for note in line:
# if note[0] == 'note' or note[0] == 'split' or note[0] == 'endline':
# print(note)
# print(page_space)
read()
def draw():
def draw_paper():
curs_y = 0
editor.create_rectangle(XYPAPER,
XYPAPER+curs_y,
XYPAPER+PAPER_WIDTH,
XYPAPER+PAPER_HEIGHT+curs_y,
outline='', fill=paper_color)
def draw_staff():
if pageno == 0:
curs_y = XYPAPER + MARGIN + header_space
else:
curs_y = XYPAPER + MARGIN
p_counter = 0
l_counter = 0
for line in msg[pageno]:
staffheight, minnote, maxnote = get_staff_height(line, scale)
draw_staff_lines(curs_y, minnote, maxnote, scale)
if len(msg)-1 == pageno:
curs_y += staffheight + (system_space)
else:
curs_y += staffheight + (system_space) + (page_space[pageno] / (len(msg[pageno])-1))
def draw_barline_grid_stemwhite():
if pageno == 0:
curs_y = XYPAPER + MARGIN + header_space
else:
curs_y = XYPAPER + MARGIN
p_counter = 0
l_counter = newpage_linenr(pageno, msg)
b_counter = newpage_barnr(pageno, msg)
b_lines = barline_pos(t_sig_map)
for line in msg[pageno]:
l_counter += 1
staffheight, minnote, maxnote = get_staff_height(line, scale)
for bl in line:
# draw barline
if bl[0] == 'barline':
b_counter += 1
editor.create_line(event_x_pos(bl[1],l_counter,new_line),
curs_y,
event_x_pos(bl[1],l_counter,new_line),
curs_y+staffheight,
width=2*scale)
editor.create_text(event_x_pos(bl[1],l_counter,new_line)+(5*scale),
curs_y-(10*scale),
text=b_counter,
anchor='w')
# draw time signature indicator
if bl[0] == 'time_signature_text':
editor.create_line(event_x_pos(bl[1],l_counter,new_line),
curs_y+staffheight,
event_x_pos(bl[1],l_counter,new_line),
curs_y+staffheight+30,
width=2*scale)
editor.create_line(event_x_pos(bl[1],l_counter,new_line),
curs_y+staffheight+30,
event_x_pos(bl[1]+bl[2],l_counter,new_line),
curs_y+staffheight+30,
width=2*scale)
editor.create_line(event_x_pos(bl[1]+bl[2],l_counter,new_line),
curs_y+staffheight+30,
event_x_pos(bl[1]+bl[2],l_counter,new_line),
curs_y+staffheight,
width=2*scale)
editor.create_text(event_x_pos(bl[1],l_counter,new_line)+5,
curs_y+staffheight+17,
text=bl[3],
font=("courier", 12, "bold"),
anchor='w')
# endbarline
if bl[0] == 'endbar':
editor.create_line(event_x_pos(bl[1],l_counter,new_line),
curs_y,
event_x_pos(bl[1],l_counter,new_line),
curs_y+staffheight,
width=4*scale)
# draw grid
if bl[0] == 'gridline':
editor.create_line(event_x_pos(bl[1],l_counter,new_line),
curs_y,
event_x_pos(bl[1],l_counter,new_line),
curs_y+staffheight, dash=(6,6))
# draw white space around stems if on barline.
if bl[0] == 'note':
if bl[1] in b_lines:
if bl[4] == 0:
editor.create_line(event_x_pos(bl[1],l_counter,new_line),
note_y_pos(bl[3], minnote, maxnote, curs_y, scale)-(25*scale),
event_x_pos(bl[1],l_counter,new_line),
note_y_pos(bl[3], minnote, maxnote, curs_y, scale)-(30*scale),
width=2*scale,
fill=paper_color)
editor.create_line(event_x_pos(bl[1],l_counter,new_line),
note_y_pos(bl[3], minnote, maxnote, curs_y, scale),
event_x_pos(bl[1],l_counter,new_line),
note_y_pos(bl[3], minnote, maxnote, curs_y, scale)+(10*scale),
width=2*scale,
fill=paper_color)
if bl[4] == 1:
editor.create_line(event_x_pos(bl[1],l_counter,new_line),
note_y_pos(bl[3], minnote, maxnote, curs_y, scale)+(25*scale),
event_x_pos(bl[1],l_counter,new_line),
note_y_pos(bl[3], minnote, maxnote, curs_y, scale)+(30*scale),
width=2*scale,
fill=paper_color)
editor.create_line(event_x_pos(bl[1],l_counter,new_line),
note_y_pos(bl[3], minnote, maxnote, curs_y, scale),
event_x_pos(bl[1],l_counter,new_line),
note_y_pos(bl[3], minnote, maxnote, curs_y, scale)-(10*scale),
width=2*scale,
fill=paper_color)
if bl[0] == 'endline':
editor.create_line(bl[1],curs_y,bl[1],curs_y+staffheight,width=4)
# create barline at the end of each system
editor.create_line(XYPAPER+MARGIN+PRINTEAREA_WIDTH,curs_y,XYPAPER+MARGIN+PRINTEAREA_WIDTH,curs_y+staffheight)
if len(msg)-1 == pageno:
curs_y += staffheight + (system_space)
else:
curs_y += staffheight + (system_space) + (page_space[pageno] / (len(msg[pageno])-1))
def draw_header_and_titles():
if pageno == 0:
# draw title on the first page
editor.create_text(XYPAPER+MARGIN, XYPAPER+MARGIN, text=title, anchor='w', font=("courier", 18, "normal"))
# draw composer on the first page
editor.create_text(XYPAPER+MARGIN+PRINTEAREA_WIDTH, XYPAPER+MARGIN, text=composer, anchor='e', font=("courier", 12, "normal"))
# draw copyright + pagenumbering + title on the bottom of every page
curs_y = XYPAPER
editor.create_text(XYPAPER+MARGIN,curs_y+PAPER_HEIGHT-35,
text='page %s of %s | %s | %s' % (pageno+1, len(msg), title, copyright),
anchor='nw',
font=("courier", 12, "normal"))
def draw_note_active():
if pageno == 0:
curs_y = XYPAPER + MARGIN + header_space
else:
curs_y = XYPAPER + MARGIN
l_counter = newpage_linenr(pageno, msg)
for line in msg[pageno]:
l_counter += 1
staffheight, minnote, maxnote = get_staff_height(line, scale)
for note in line:
if render_type == 'normal':
if note[0] == 'note':
x0 = event_x_pos(note[1], l_counter, new_line)
x1 = event_x_pos(note[1]+note[2], l_counter, new_line)
y = note_y_pos(note[3], minnote, maxnote, curs_y, scale)
editor.create_rectangle(x0,y-(5*scale),x1,y+(5*scale),fill=MIDINOTECOLOR,outline='')
editor.create_line(x1,y-(5*scale),x1,y+(5*scale),width=2*scale)
if note[0] == 'split':
x0 = event_x_pos(note[1], l_counter, new_line)
x1 = event_x_pos(note[1]+note[2], l_counter, new_line)
y = note_y_pos(note[3], minnote, maxnote, curs_y, scale)
editor.create_rectangle(x0,y-(5*scale),x1,y+(5*scale),fill=MIDINOTECOLOR,outline='')
editor.create_oval(x0+(5*scale),y-(2.5*scale),x0+(10*scale),y+(2.5*scale),fill='black',outline='')
editor.create_line(x1,y-(5*scale),x1,y+(5*scale),width=2*scale)
elif render_type == 'export':
if note[0] == 'note':
x0 = event_x_pos(note[1], l_counter, new_line)
x1 = event_x_pos(note[1]+note[2], l_counter, new_line)
y = note_y_pos(note[3], minnote, maxnote, curs_y, scale)
note_active_gradient(x0,x1,y, l_counter, scale)
editor.create_line(x1,y-(5*scale),x1,y+(5*scale),width=2*scale)
if note[0] == 'split':
x0 = event_x_pos(note[1], l_counter, new_line)
x1 = event_x_pos(note[1]+note[2], l_counter, new_line)
y = note_y_pos(note[3], minnote, maxnote, curs_y, scale)
note_active_gradient(x0,x1,y, l_counter, scale)
editor.create_oval(x0+(5*scale),y-(2.5*scale),x0+(10*scale),y+(2.5*scale),fill='black',outline='')
editor.create_line(x1,y-(5*scale),x1,y+(5*scale),width=2*scale)
if len(msg)-1 == pageno:
curs_y += staffheight + (system_space)
else:
curs_y += staffheight + (system_space) + (page_space[pageno] / (len(msg[pageno])-1))
def draw_note_start_and_cursor():
black = [2, 5, 7, 10, 12, 14, 17, 19, 22, 24, 26, 29, 31, 34, 36, 38, 41, 43, 46,
48, 50, 53, 55, 58, 60, 62, 65, 67, 70, 72, 74, 77, 79, 82, 84, 86]
if pageno == 0:
curs_y = XYPAPER + MARGIN + header_space
else:
curs_y = XYPAPER + MARGIN
l_counter = newpage_linenr(pageno, msg)
p_counter = 0
b_lines = barline_pos(t_sig_map)
for line in msg[pageno]:
l_counter += 1
staffheight, minnote, maxnote = get_staff_height(line, scale)
for note in line:
if note[0] == 'note':
x = event_x_pos(note[1], l_counter, new_line)
y = note_y_pos(note[3], minnote, maxnote, curs_y, scale)
if note[4] == 0:
if note[3] in black:
editor.create_oval(x,
y-(5*scale),
x+(5*scale),
y+(5*scale),
width=2*scale,
fill='black')
else:
editor.create_oval(x,
y-(5*scale),
x+(10*scale),
y+(5*scale),
width=2*scale,
fill=paper_color)
editor.create_line(x,y-(25*scale),x,y,width=2*scale)
if note[4] == 1:
if note[3] in black:
editor.create_oval(x,
y-(5*scale),
x+(5*scale),
y+(5*scale),
width=2*scale,
fill='black')
else:
editor.create_oval(x,
y-(5*scale),
x+(10*scale),
y+(5*scale),
width=2*scale,
fill=paper_color)
editor.create_line(x,y+(25*scale),x,y,width=2*scale)
# cursor
if note[0] == 'cursor' and render_type == 'normal':
x = event_x_pos(note[1], l_counter, new_line)
y = note_y_pos(note[3], minnote, maxnote, curs_y, scale)
editor.create_line(x,y-(5*scale),x,y+(5*scale),fill='blue',tag='cursor',width=5)
if len(msg)-1 == pageno:
curs_y += staffheight + (system_space)
else:
curs_y += staffheight + (system_space) + (page_space[pageno] / (len(msg[pageno])-1))
# drawing order
draw_paper()
draw_note_active()
draw_staff()
draw_barline_grid_stemwhite()
draw_note_start_and_cursor()
draw_header_and_titles()
editor.tag_raise('cursor')
draw()
if not render_type == 'export':
editor.scale("all", XYPAPER, XYPAPER, 1.5, 1.5)
editor.configure(scrollregion=bbox_offset(editor.bbox("all"), XYPAPER))
return len(msg)
#------------------
# editor functions
#------------------
def score_setup():
pass
def midi_import():
global file
file = [
# score setup part:
[
# t_sig_map; inside list because the order of the t_sig messages defines the time changes/score.
[
],
# mp_line
{'type':'mp_line', 'string':'5 4'},
# titles
{'type':'title', 'text':'Test_Version'},
{'type':'composer', 'text':'PianoScript'},
{'type':'copyright', 'text':'copyrights reserved 2022'},
# scale; global scale
{'type':'scale', 'value':0.75},
# page margins
{'type':'margin', 'value':30},
# space under systems / in between
{'type':'system_space', 'value':40}
],
# musical data part:
[
]
]
# ---------------------------------------------
# translate midi data to note messages with
# the right start and stop (piano)ticks.
# ---------------------------------------------
midifile = filedialog.askopenfile(parent=root,
mode='Ur',
title='Open midi (experimental)...',
filetypes=[("MIDI files","*.mid")]).name
mesgs = []
mid = MidiFile(midifile)
tpb = mid.ticks_per_beat
msperbeat = 1
for i in mid:
mesgs.append(i.dict())
''' convert time to pianotick '''
for i in mesgs:
i['time'] = tpb * (1 / msperbeat) * 1000000 * i['time'] * (256 / tpb)
if i['type'] == 'set_tempo':
msperbeat = i['tempo']
''' change time values from delta to relative time. '''
memory = 0
for i in mesgs:
i['time'] += memory
memory = i['time']
# change every note_on with 0 velocity to note_off.
if i['type'] == 'note_on' and i['velocity'] == 0:
i['type'] = 'note_off'
''' get note_on, note_off, time_signature durations. '''
index = 0
for i in mesgs:
if i['type'] == 'note_on':
for n in mesgs[index:]:
if n['type'] == 'note_off' and i['note'] == n['note']:
i['duration'] = n['time'] - i['time']
break
if i['type'] == 'time_signature':
for t in mesgs[index+1:]:
if t['type'] == 'time_signature' or t['type'] == 'end_of_track':
i['duration'] = t['time'] - i['time']
break
index += 1
# round time to piano-tick
for i in mesgs:
if i['type'] == 'note_on' or i['type'] == 'time_signature':
i['time'] = round(i['time'],0)
i['duration'] = round(i['duration'],0)
# for debugging purposes print every midi message.
# for i in mesgs:
# print(i)
# write time_signatures:
count = 0
for i in mesgs:
if i['type'] == 'time_signature':
tsig = (i['numerator'], i['denominator'])
amount = int(round(i['duration'] / measure_length(tsig),0))
gridno = i['numerator']
if tsig == '6/8':
gridno = 2
if tsig == '12/8':
gridno = 4
file[0][0].append({'type':i['type'], 'amount':amount, 'numerator':i['numerator'], 'denominator':i['denominator'], 'grid':gridno, 'visible':1})
count += 1
# write notes
for i in mesgs:
if i['type'] == 'note_on' and i['channel'] == 0:
file[1].append({'type':'note', 'time':i['time'], 'duration':i['duration'], 'note':i['note']-20, 'hand':0, 'beam':0, 'slur':0})
if i['type'] == 'note_on' and i['channel'] >= 1:
file[1].append({'type':'note', 'time':i['time'], 'duration':i['duration'], 'note':i['note']-20, 'hand':1, 'beam':0, 'slur':0})
# insert cursor
file[0].append({'type':'cursor', 'time':0, 'duration':256, 'note':40})
render('normal')
def move_cursor(event):
global file
if event.keysym == 'Up':
for i in file[0]:
if isinstance(i,dict):
if i['type'] == 'cursor':
i['note'] += 1
if i['note'] > 88:
i['note'] = 88
elif event.keysym == 'Down':
for i in file[0]:
if isinstance(i,dict):
if i['type'] == 'cursor':
i['note'] -= 1
if i['note'] < 1:
i['note'] = 1
elif event.keysym == 'Left':
for i in file[0]:
if isinstance(i,dict):
if i['type'] == 'cursor':
i['time'] -= i['duration']
if i['time'] < 0:
i['time'] = 0
elif event.keysym == 'Right':
for i in file[0]:
if isinstance(i,dict):
if i['type'] == 'cursor':
i['time'] += i['duration']
render('normal', view_page)
def change_length(event):
try: dur = list_dur.curselection()[0]
except: return
if dur == 0:
for i in file[0]:
if isinstance(i,dict):
if i['type'] == 'cursor':
i['duration'] = 1024 / eval(divide_spin.get())
if dur == 1:
for i in file[0]:
if isinstance(i,dict):
if i['type'] == 'cursor':
i['duration'] = 512 / eval(divide_spin.get())
if dur == 2:
for i in file[0]:
if isinstance(i,dict):
if i['type'] == 'cursor':
i['duration'] = 256 / eval(divide_spin.get())
if dur == 3:
for i in file[0]:
if isinstance(i,dict):
if i['type'] == 'cursor':
i['duration'] = 128 / eval(divide_spin.get())
if dur == 4:
for i in file[0]:
if isinstance(i,dict):
if i['type'] == 'cursor':
i['duration'] = 64 / eval(divide_spin.get())
if dur == 5:
for i in file[0]:
if isinstance(i,dict):
if i['type'] == 'cursor':
i['duration'] = 32 / eval(divide_spin.get())
if dur == 6:
for i in file[0]:
if isinstance(i,dict):
if i['type'] == 'cursor':
i['duration'] = 16 / eval(divide_spin.get())
if dur == 7:
for i in file[0]:
if isinstance(i,dict):
if i['type'] == 'cursor':
i['duration'] = 8 / eval(divide_spin.get())
def apply_score_setup():
for i in file[0]:
if isinstance(i,dict):
if i['type'] == 'mp_line':
i['string'] = mpline_entry.get()
if i['type'] == 'scale':
i['value'] = eval(scale_entry.get())
if i['type'] == 'title':
i['text'] = title_entry.get()
if i['type'] == 'composer':
i['text'] = composer_entry.get()
if i['type'] == 'copyright':
i['text'] = copyright_entry.get()
if i['type'] == 'margin':
i['value'] = eval(margin_entry.get())
if i['type'] == 'system_space':
i['value'] = eval(system_entry.get())
render('normal', view_page)
apply_button.configure(command=apply_score_setup)
def next_page(event):
global view_page
view_page += 1
render('normal', view_page)
def prev_page(event):
global view_page
view_page -= 1
render('normal', view_page)
def write_note(event):
global note_write
print(event)
if event.keysym == 'space':
if note_write == 0:
note_write = 1
elif note_write == 1:
note_write = 0
if note_write == 1:
curs_pos = 0
note_pos = 0
for i in file[0]:
if isinstance(i,dict):
if i['type'] == 'cursor':
curs_pos = i['time']
note_pos = i['note']
file[1].append({'type':'note', 'time':curs_pos, 'duration':0, 'note':note_pos, 'hand':0, 'beam':0, 'slur':0})
render('normal', view_page)
#------------------
# export
#------------------
def exportPDF():
def is_tool(name):
"""Check whether `name` is on PATH and marked as executable."""
return which(name) is not None
print('exportPDF')
if platform.system() == 'Linux':
if is_tool('ps2pdfwr') == 0:
messagebox.showinfo(title="Can't export PDF!",
message='PianoScript cannot export the PDF because function "ps2pdfwr" is not installed on your computer.')
return
f = filedialog.asksaveasfile(mode='w', parent=root, filetypes=[("pdf file","*.pdf")], initialfile=title, initialdir='~/Desktop')
if f:
pslist = []
for rend in range(render('export')):
editor.postscript(file="/tmp/tmp%s.ps" % rend, x=XYPAPER, y=XYPAPER+(rend*(PAPER_HEIGHT+XYPAPER)), width=PAPER_WIDTH, height=PAPER_HEIGHT, rotate=False)
process = subprocess.Popen(["ps2pdfwr", "-sPAPERSIZE=a4", "-dFIXEDMEDIA", "-dEPSFitPage", "/tmp/tmp%s.ps" % rend, "/tmp/tmp%s.pdf" % rend])
process.wait()
os.remove("/tmp/tmp%s.ps" % rend)
pslist.append("/tmp/tmp%s.pdf" % rend)
cmd = 'pdfunite '
for i in range(len(pslist)):
cmd += pslist[i] + ' '
cmd += '"%s"' % f.name
process = subprocess.Popen(cmd, shell=True)
process.wait()
render('normal')
return
else:
return
elif platform.system() == 'Windows':
f = filedialog.asksaveasfile(mode='w', parent=root, filetypes=[("pdf file","*.pdf")], initialfile=title, initialdir='~/Desktop')
if f:
print(f.name)
counter = 0
pslist = []
for export in range(render('export')):
counter += 1
print('printing page ', counter)
editor.postscript(file=f"{f.name}{counter}.ps", colormode='gray', x=40, y=50+(export*(paperheigth+50)), width=paperwidth, height=paperheigth, rotate=False)
pslist.append(str('"'+str(f.name)+str(counter)+'.ps'+'"'))
try:
process = subprocess.Popen(f'''"{windowsgsexe}" -dQUIET -dBATCH -dNOPAUSE -dFIXEDMEDIA -sPAPERSIZE=a4 -dEPSFitPage -sDEVICE=pdfwrite -sOutputFile="{f.name}.pdf" {' '.join(pslist)}''', shell=True)
process.wait()
process.terminate()
for i in pslist:
os.remove(i.strip('"'))
f.close()
os.remove(f.name)
except:
messagebox.showinfo(title="Can't export PDF!", message='Be sure you have selected a valid path in the default.pnoscript file. You have to set the path+gswin64c.exe. example: ~windowsgsexe{C:/Program Files/gs/gs9.54.0/bin/gswin64c.exe}')
#--------------------------------------------------------
# MENU
#--------------------------------------------------------
menubar = Menu(root, relief='flat', bg='#333333', fg='white')
root.config(menu=menubar)
fileMenu = Menu(menubar, tearoff=0, bg='#333333', fg='white')
fileMenu.add_command(label='new', command=new_file)
fileMenu.add_command(label='open', command=open_file)
fileMenu.add_command(label='import MIDI', command=midi_import)
fileMenu.add_command(label='save', command=None)
fileMenu.add_command(label='save as', command=save_as)
fileMenu.add_separator()
submenu = Menu(fileMenu, tearoff=0, bg='#333333', fg='white')
submenu.add_command(label="postscript", command=None)
submenu.add_command(label="pdf", command=exportPDF)
fileMenu.add_cascade(label='export', menu=submenu, underline=0)
fileMenu.add_separator()
fileMenu.add_command(label="horizontal/vertical", underline=0, command=None)
fileMenu.add_command(label="fullscreen/windowed (F11)", underline=0, command=None)
fileMenu.add_separator()
fileMenu.add_command(label="exit", underline=0, command=None)
menubar.add_cascade(label="menu", underline=0, menu=fileMenu)
editMenu = Menu(menubar, tearoff=0, bg='#333333', fg='white')
editMenu.add_command(label='score setup...', command=None)
menubar.add_cascade(label="edit", underline=0, menu=editMenu)
#--------------------------------------------------------
# BIND (shortcuts)
#--------------------------------------------------------
root.bind('<Up>',move_cursor)
root.bind('<Down>',move_cursor)
root.bind('<Left>',move_cursor)
root.bind('<Right>',move_cursor)
# list_dur.bind("<<ListboxSelect>>", change_length)
root.bind('<Next>', next_page)
root.bind('<Prior>', prev_page)
root.bind('<space>', write_note)
#--------------------------------------------------------
# MAINLOOP
#--------------------------------------------------------
render('normal', 10)
root.mainloop()
#--------------------------------------------------------
# TODO
#--------------------------------------------------------
'''
* grid editor
* render per page
* divide function note length
* save file
* toolbar
'''Currently program can do:- Import midi file
- Next page previous page using pageup/pagedown
- Changing the score settings like margins titles etc.. and applying by clicking the button.
- You can walk trough the score using arrow keys with the cursor
