Python Forum
Music Notation editor; how to build the editor? direction to go?
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Music Notation editor; how to build the editor? direction to go?
#1
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:
#---------------------
# 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 Smile .

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
Reply
#2
I have done this in the past, the code is still on GitHub, but needs an update.
At any rate, I created lilypond script with python for the notation which was then converted to LaTex and rendered in a tkinter GUI.
You can find the package here: https://github.com/Larz60p/MusicScales

Sample:
   
Gribouillis likes this post
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Forcing matplotlib to NOT use scientific notation when graphing sawtooth500 4 407 Mar-25-2024, 03:00 AM
Last Post: sawtooth500
  My code works on Jupyter Lab/Notebook, but NOT on Visual Code Editor jst 4 1,056 Nov-15-2023, 06:56 PM
Last Post: jst
  ''.join and start:stop:step notation for lists ringgeest11 2 2,448 Jun-24-2023, 06:09 AM
Last Post: ferdnyc
  issue with converting a scientific notation to standard notation thomaswfirth 4 1,392 Jun-06-2023, 06:06 PM
Last Post: rajeshgk
  notation MCL169 8 1,506 Apr-14-2023, 12:06 PM
Last Post: MCL169
  while loop not working-I am using sublime text editor mma_python 4 1,149 Feb-05-2023, 06:26 PM
Last Post: deanhystad
  Issue in writing sql data into csv for decimal value to scientific notation mg24 8 3,083 Dec-06-2022, 11:09 AM
Last Post: mg24
  Sublime Text Editor not recognizing Python elroberto 5 2,916 Jun-13-2022, 04:00 PM
Last Post: rob101
  VSCODE Split Editor Window Krischu 1 1,276 Mar-09-2022, 10:29 AM
Last Post: Larz60+
  Project Direction bclanton50 1 1,334 Jan-06-2022, 11:38 PM
Last Post: lucasbazan

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020