├── logo.png ├── MAESTOSO.ico ├── MAESTOSO.png ├── MAESTOSO-large.png ├── View ├── Resources │ ├── Icons │ │ ├── play.png │ │ ├── harmony.png │ │ ├── pause.png │ │ ├── preview.png │ │ ├── render.png │ │ ├── rhythm.png │ │ ├── fullvideo.png │ │ ├── monophony.png │ │ └── polyphony.png │ └── DejaVuSans-Bold.ttf ├── framerate_popup.py ├── runexcept.py ├── instrument_panel.py ├── scrollable_frame.py ├── videoplayer.py ├── customization_classes.py ├── theme_functions.py └── toolbar.py ├── requirements.txt ├── PNG_EXPORTS └── blankfile.rtf ├── animator.py ├── theme_handler.py ├── README.md ├── main.py ├── utility.py ├── Maestoso.py ├── init_tracklist.py ├── tracklist_functions.py ├── THEMES ├── ChordCycle.py ├── VideoGameTheme.py ├── NecklaceTheme.py ├── CyberPunkTheme.py ├── BasicTheme.py ├── DrumballTheme.py └── SwankyDrumMachine.py ├── theme_extras └── SwankyDrumMachine_Functions.py └── LICENSE /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amanatron/Maestoso/HEAD/logo.png -------------------------------------------------------------------------------- /MAESTOSO.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amanatron/Maestoso/HEAD/MAESTOSO.ico -------------------------------------------------------------------------------- /MAESTOSO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amanatron/Maestoso/HEAD/MAESTOSO.png -------------------------------------------------------------------------------- /MAESTOSO-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amanatron/Maestoso/HEAD/MAESTOSO-large.png -------------------------------------------------------------------------------- /View/Resources/Icons/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amanatron/Maestoso/HEAD/View/Resources/Icons/play.png -------------------------------------------------------------------------------- /View/Resources/Icons/harmony.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amanatron/Maestoso/HEAD/View/Resources/Icons/harmony.png -------------------------------------------------------------------------------- /View/Resources/Icons/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amanatron/Maestoso/HEAD/View/Resources/Icons/pause.png -------------------------------------------------------------------------------- /View/Resources/Icons/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amanatron/Maestoso/HEAD/View/Resources/Icons/preview.png -------------------------------------------------------------------------------- /View/Resources/Icons/render.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amanatron/Maestoso/HEAD/View/Resources/Icons/render.png -------------------------------------------------------------------------------- /View/Resources/Icons/rhythm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amanatron/Maestoso/HEAD/View/Resources/Icons/rhythm.png -------------------------------------------------------------------------------- /View/Resources/DejaVuSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amanatron/Maestoso/HEAD/View/Resources/DejaVuSans-Bold.ttf -------------------------------------------------------------------------------- /View/Resources/Icons/fullvideo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amanatron/Maestoso/HEAD/View/Resources/Icons/fullvideo.png -------------------------------------------------------------------------------- /View/Resources/Icons/monophony.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amanatron/Maestoso/HEAD/View/Resources/Icons/monophony.png -------------------------------------------------------------------------------- /View/Resources/Icons/polyphony.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amanatron/Maestoso/HEAD/View/Resources/Icons/polyphony.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | matplotlib==3.3.4 2 | mido==1.2.9 3 | mplcyberpunk==0.1.11 4 | opencv-python==4.5.1.48 5 | Pillow==8.1.0 6 | pretty-midi==0.2.9 7 | pychord==0.5.1 8 | tkmacosx==0.1.6 9 | -------------------------------------------------------------------------------- /PNG_EXPORTS/blankfile.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf2576 2 | \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 3 | {\colortbl;\red255\green255\blue255;} 4 | {\*\expandedcolortbl;;} 5 | \paperw11900\paperh16840\margl1440\margr1440\vieww11520\viewh8400\viewkind0 6 | \pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0 7 | 8 | \f0\fs24 \cf0 This is a blank file. You can choose to delete it yourself. Maestoso will anyways automatically delete it later on. } -------------------------------------------------------------------------------- /View/framerate_popup.py: -------------------------------------------------------------------------------- 1 | """ THIS FILE CREATES THE SELECT FRAME RATE TOP LEVEL""" 2 | 3 | from tkinter import * 4 | 5 | def create_render_toplevel(root,main): 6 | root_x = root.winfo_rootx() 7 | root_y = root.winfo_rooty() 8 | x_pos = root_x + 500 9 | y_pos = root_y + 200 10 | top = Toplevel(bg = 'gray9') 11 | top.geometry(f'500x300+{x_pos}+{y_pos}') 12 | top.resizable(0,0) 13 | top.title("Adjust Frame Rate") 14 | top.grab_set() 15 | 16 | descriptiontop = Label(top,text = "Select the desired frame rate", bg = "gray9", fg = "white") 17 | descriptiontop.pack(side = TOP) 18 | 19 | framerates = [24,25,30,60] 20 | 21 | def selected(): 22 | selection = var.get() 23 | main.fps = selection 24 | notification = Label(top, text = f"{selection} fps has been selected. Close this window when you're satisfied.",bg = "gray9", fg = "white") 25 | notification.place(y = 200, x = 50) 26 | 27 | var = IntVar() 28 | add = 0 29 | for i in framerates: 30 | rb = Radiobutton(top, text = f'{i} fps', variable = var, value = i, command = selected,bg = "gray9", fg = 'white') 31 | rb.place(x = 10 + add, y = 150) 32 | add += 125 -------------------------------------------------------------------------------- /animator.py: -------------------------------------------------------------------------------- 1 | """ THIS FILE CONTAINS THE ANIMATOR CLASS""" 2 | 3 | 4 | import cv2 5 | import numpy as np 6 | import glob 7 | import os 8 | from tkinter import messagebox 9 | 10 | #animator class 11 | class Animator: 12 | #combines all frames within the outputdir and writes them into video 13 | def __init__(self,image_folder,fps,filenameuser): 14 | try: 15 | path = image_folder 16 | img_array = [] 17 | filenames = [] 18 | for count in range(len(os.listdir(path))-1): 19 | filename = f'{path}/frame_{count}.png' 20 | filenames.append(filename) #append filename to filenames list 21 | img = cv2.imread(filename) #read the image file 22 | height, width, layers = img.shape #retrieve height, width from img 23 | size = (width,height) 24 | img_array.append(img) #add img to the img_array list 25 | out = cv2.VideoWriter(f'{filenameuser}.mp4',cv2.VideoWriter_fourcc(*'mp4v'), fps, size) #filename = user assigned file name 26 | for i in range(len(img_array)): 27 | out.write(img_array[i]) 28 | out.release() 29 | messagebox.showinfo("EXPORT SUCCESSFUL!","EXPORT SUCCESSFUL!") 30 | except: 31 | messagebox.showerror("Export Error","No Animation to Export") 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /theme_handler.py: -------------------------------------------------------------------------------- 1 | """ THIS FILE HANDLES ALL THE IMPORTS AND INITIALIZATIONS OF THEMES FROM THE "THEMES" DIRECTORY """ 2 | 3 | from os import listdir, replace 4 | from os.path import isfile, join, dirname, abspath,basename 5 | from importlib import import_module 6 | from tkinter import filedialog, messagebox 7 | 8 | #this function initializes all files within the "THEMES" directory 9 | def return_filenames(tracklist): 10 | current_path = dirname(abspath(__file__)) 11 | mypath = join(current_path,'THEMES') 12 | filelist = [f for f in listdir(mypath) if isfile(join(mypath, f))] 13 | #list of themes 14 | theme_list = [] 15 | #list of themes that failed to load 16 | errored_themes = [] 17 | for i in filelist: 18 | if i != '__init__.py' and i != '.DS_Store': 19 | try: 20 | #replace the file extension from the theme file name 21 | filename = i.replace('.py','') 22 | mod = import_module(f'.{filename}',package='THEMES') # import module 23 | #get the class attr 24 | mod_class = getattr(mod,filename) 25 | theme = mod_class(tracklist) 26 | theme_list.append(theme) 27 | except: 28 | #append themes that failed to errored_themes 29 | errored_themes.append(filename) 30 | 31 | if len(errored_themes) > 0: 32 | messagebox.showwarning("Import Error", "The following themes failed to import: {}".format(errored_themes)) 33 | 34 | return theme_list 35 | 36 | # this function handles the import of themes. It takes a selected theme and places it in the "THEME" directory 37 | def import_theme(): 38 | file = filedialog.askopenfile(mode ='r', filetypes =[('Maestoso Themes', '*.py')]) 39 | if file is not None: 40 | import_warning = messagebox.askokcancel("Import Warning", "Maestoso themes are directly imported as .py scripts. Never import an unknown script or one that hasn't been directly downloaded from www.maestoso.app - Do you still wish to continue?") 41 | if import_warning == 0: 42 | return 43 | filepath = file.name 44 | filename = basename(filepath) 45 | current_path = dirname(abspath(__file__)) 46 | mypath = join(current_path,'THEMES') 47 | new_theme_path = join(mypath,filename) 48 | replace(filepath,new_theme_path) 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Maestoso 2 | 3 | [![Maestoso Logo](https://i.postimg.cc/NMskK3mZ/Screenshot-2021-03-17-at-9-54-33-PM.png)](https://postimg.cc/7CR2cQBN) 4 | ## An Open Source Music Animation Software 5 | 6 | Maestoso creates stunning 2D Animations directly from MIDI files. It currently features 7 customizable themes pertaining to 4 seperate categories of musical information: **Rhythm, Harmony, Melody and Polyphony**. This software also offers in-built methods that make retrieval of MIDI information easy and straightforward, making it easy for contributors to solely focus on theme construction. 7 | 8 | [![A screenshot of the Maestoso environment](https://i.postimg.cc/HnQYFhPs/Screenshot-2021-03-17-at-9-46-37-PM.png)](https://postimg.cc/9wQH7Jhv) 9 | 10 | 11 | 12 | ## Maestoso 2.0 is coming soon! 13 | 14 | Maestoso initially began with an idea for a software that allowed users to construct their own music animations in a way that gave them enough control to create anything that they could possibly imagine whilst letting the software do all the heavy lifting in terms of syncronisation, information retrieval and coordination of musical information. The current version of Maestoso was a bit of a sidetrack while I was working on the original software. This version borrows many functions from Maestoso 2.0 but doesn't offer the intended flexibility and instead retricts the user to a set of themes. **All of that will change soon!** 15 | 16 | ### Some of the features currently being developed for Maestoso 2.0: 17 | - Give the users more power with themes by letting them design the themes themeselves without having to write a single line of code 18 | - Methods that tackle ornamentations and articulations more accurately and creatively 19 | - Support for lyrics: bind text to other musical features and have more control 20 | - Advanced harmonic analysis and manipulations: retrieve information specific to chord functions and provide methods to manipulate animations to them 21 | - Support for different musical forms: support for musical forms such as fugues, raps and etc where their distinctive stylistic traits are better highlighted, i.e, rhyme scheme analysis, melodic transformations and etc can be better animated. 22 | 23 | 24 | ## Documentation 25 | The official documentation is hosted on the Wiki section of the repository. There is documentation for ![users](https://github.com/amanatron/Maestoso/wiki/User-Guide) and there is also documentation available for ![developers](https://github.com/amanatron/Maestoso/wiki/Developer-Guide). 26 | 27 | 28 | -------------------------------------------------------------------------------- /View/runexcept.py: -------------------------------------------------------------------------------- 1 | """ THIS FILE CONTAINS THE CHECK_TYPERROR_RENDER FUNCTION""" 2 | 3 | from tkinter import messagebox 4 | from tracklist_functions import create_chord_list 5 | 6 | def check_typerror_render(main): 7 | if main.selected_animation == '': 8 | messagebox.showerror("Render Error", "No animation has been selected.") 9 | return False 10 | 11 | #check if the file contains the relevent information for the animation to even run... This function is called to avoid scripts incapable of processing the necessary data from running 12 | for i in main.theme_data: 13 | if i.display_name == main.selected_animation: 14 | if i.category == 'RHYTHM': 15 | if not check_if_rhythmic(main,state = True): 16 | messagebox.showerror("Theme Error ","No drum track was found. Please select a more suitable theme.") 17 | return False 18 | else: 19 | return True 20 | elif i.category == 'POLYPHONIC' or i.category == 'MONOPHONIC': 21 | if not check_if_pitch_based(main): 22 | messagebox.showerror("Theme Error ","No pitch based track was found. Please select a more suitable theme.") 23 | return False 24 | else: 25 | return True 26 | else: 27 | if not check_if_harmonic(main): 28 | messagebox.showerror("Theme Error ","No harmonic information was detected. Please select a more suitable theme.") 29 | return False 30 | else: 31 | return True 32 | 33 | #check if the midi file contains even a single drum track - an easy alternative to this function would be to simply check the drum_count attr from tracklist 34 | def check_if_rhythmic(main,state = True): 35 | counter = 0 36 | for i in main.project_tracklist: 37 | if i.isdrum: 38 | counter += 1 39 | 40 | if state == True: 41 | if counter == 0: 42 | return False 43 | else: 44 | return True 45 | else: 46 | return counter 47 | 48 | #check if the midi file contains only drum tracks - an easy alternative to this function would be to simply check the drum_count attr from tracklist 49 | def check_if_pitch_based(main): 50 | if check_if_rhythmic(main,state = False) == main.number_of_instruments: 51 | return False 52 | else: 53 | return True 54 | 55 | #check if the midi file contains any harmonic information 56 | def check_if_harmonic(main): 57 | if len(create_chord_list(main.project_tracklist,main.PPQN)) == 0: 58 | return False 59 | else: 60 | return True 61 | 62 | 63 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | """ THIS FILE STORES THE MAIN CLASS - MAIN FUNCTIONS AS A CONFIG FILE AND BEHAVES AS AN INSTANCE OF THE ENTIRE PROJECT """ 2 | 3 | from init_tracklist import TrackList 4 | from tracklist_functions import find_pitch_range, create_timecode_hash 5 | from utility import initialize_colours, return_random, delete_frames 6 | from theme_handler import return_filenames 7 | from importlib import import_module 8 | from tkinter import messagebox 9 | 10 | 11 | class Main: 12 | def __init__(self): 13 | self.filedir = '' #the file directory of the selected midi file 14 | self.project_name = '' 15 | self.tracklist = '' #tracklist object 16 | self.project_tracklist = '' #tracklist.track_list object 17 | self.number_of_instruments = '' 18 | self.outputdir = 'PNG_EXPORTS' #the outputdir for all frames 19 | self.PPQN = '' 20 | self.fps = 30 21 | self.endtime = '' 22 | self.preview_time = 45 23 | 24 | #this method initializes the entire project. It's run from the import menu in maestoso.py 25 | def initialize_project(self): 26 | delete_frames(self.outputdir) #deletes all the files in the outputdir 27 | self.selected_animation = '' 28 | try: 29 | self.tracklist = TrackList(self.filedir) #create an instances of Tracklist from the selected filedir 30 | except RuntimeWarning: 31 | messagebox.showwarning("Improper Midi","Tempo, Key or Time signature change events found on non-zero tracks. This is not a valid type 0 or type 1 MIDI file. Tempo, Key or Time Signature may be wrong.") 32 | finally: 33 | self.tracklist = TrackList(self.filedir) 34 | self.project_tracklist = self.tracklist.track_list 35 | self.number_of_instruments = len(self.project_tracklist) 36 | initialize_colours(self.project_tracklist,return_random) #initialize the random colours assigned to each instrument* type 37 | self.endtime = self.tracklist.midifile.get_end_time() 38 | self.theme_data = return_filenames(self.tracklist) #create an instance of all the themes and store them in a list 39 | self.PPQN = self.tracklist.PPQN 40 | 41 | #this method is called every time the render button from toolbar.py is selected. 42 | def animate(self,preview): 43 | delete_frames(self.outputdir) #delete any file in the outputdir 44 | for i in self.theme_data: 45 | if i.display_name == self.selected_animation: 46 | theme = i 47 | #run the ANIMATE() method from the selected theme instance 48 | theme.ANIMATE(self.project_tracklist,self.PPQN,self.fps,self.outputdir,preview,self.preview_time) 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /utility.py: -------------------------------------------------------------------------------- 1 | ''' GENERAL FUNCTIONS ''' 2 | 3 | import random 4 | import os 5 | 6 | def frame_rate_note(note_duration,frame_rate): 7 | """[RETURNS THE NUMBER OF FRAMES THAT ADD UP TO ANY PARTICULAR NOTE DURATION] 8 | 9 | Args: 10 | note_duration ([int]): [note value obtained from tracklist.instrument.notes] 11 | 12 | Returns: 13 | [int]: [number of frames] 14 | """ 15 | return round(frame_rate * note_duration) 16 | 17 | 18 | def frames_to_second(frames,frame_rate): 19 | """[RETURNS THE INVERSE OF frame_rate_note] 20 | 21 | Args: 22 | frames ([int): [numbers of frames passed] 23 | frame_rate ([int]): [fps] 24 | 25 | Returns: 26 | [bool]: [seconds] 27 | """ 28 | return (frames/frame_rate) 29 | 30 | ''' COLOUR PALLETE AND ASSIGNMENT FUNCTION ''' 31 | 32 | def return_random(number_of_instruments, itemslist): 33 | array = [i for i in range(len(itemslist))] 34 | n = len(array) 35 | for i in range(n-1,0,-1): 36 | j = random.randint(0,i+1) 37 | array[i],array[j] = array[j],array[i] 38 | 39 | return array[0:number_of_instruments] 40 | 41 | # assigns randomc colours to instruments upon initialization 42 | def initialize_colours(project_tracklist,return_random): 43 | colour_pallete = ['#ff124f', '#ff00a0','#fe75fe','#7a04eb','#120458','#ff6e27','#fbf665','#00b3fe','#9cf862','#383e65','#defe47'] #colour pallete specific to the Maestoso style 44 | number_of_instruments = len(project_tracklist) 45 | if number_of_instruments <= len(colour_pallete): 46 | random_colour = return_random(number_of_instruments, colour_pallete) 47 | for i in range(number_of_instruments): 48 | project_tracklist[i].colour = colour_pallete[random_colour[i]] 49 | else: 50 | random_colour = [] 51 | for i in range(number_of_instruments): 52 | random_colour.append(random.choice(range(len(colour_pallete)))) 53 | for i in range(number_of_instruments): 54 | project_tracklist[i].colour = colour_pallete[random_colour[i]] 55 | 56 | #picks and returns a random colour from the colour pallete 57 | def pick_random_colour(): 58 | colour_pallete = ['#ff124f', '#ff00a0','#fe75fe','#7a04eb','#120458','#ff6e27','#fbf665','#00b3fe','#9cf862','#383e65','#defe47'] 59 | return random.choice(colour_pallete) 60 | 61 | 62 | def delete_frames(outputdir): 63 | #deletes all files titled "frame_{number}" from a given outputdir 64 | path = os.path.join(os.getcwd(),outputdir) 65 | for count in range(len(os.listdir(path))-1): 66 | try: 67 | os.remove(f'{path}/frame_{count}.png') 68 | except: 69 | pass 70 | 71 | def max_frame_number(outputdir): 72 | #returns the total size of the given outputdir 73 | path = outputdir 74 | return len(os.listdir(path)) - 2 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Maestoso.py: -------------------------------------------------------------------------------- 1 | """ RUN THIS FILE TO START YOUR PROGRAM """ 2 | 3 | 4 | from tkinter import * 5 | import os 6 | from os.path import join 7 | from tkinter import filedialog 8 | from tkinter import messagebox 9 | from main import Main 10 | from View import instrument_panel, toolbar, videoplayer, framerate_popup, theme_functions 11 | from animator import Animator 12 | from utility import delete_frames 13 | from theme_handler import import_theme 14 | 15 | 16 | # importing the MIDI file 17 | def open_file(): 18 | file = filedialog.askopenfile(mode ='r', filetypes =[('Midi Files', '*.mid')]) 19 | if file is not None: 20 | filepath = file.name 21 | filename = os.path.basename(filepath) 22 | main.filedir = filepath 23 | main.project_name = filename 24 | main.initialize_project() # run the initialization method from the initialize method in the main instance 25 | root.title("Maestoso - {}".format(filename)) 26 | filemenu.entryconfig(2, command = lambda: export_video(main.outputdir),state = ACTIVE) 27 | filemenu.entryconfig(3,command = lambda: [root.destroy(),delete_frames(main.outputdir)]) 28 | preferences.entryconfig(1,state = ACTIVE) 29 | preferences.entryconfig(2,state = ACTIVE) 30 | 31 | #instance the three primary UI elements 32 | 33 | instrumentpanel = instrument_panel.InstrumentPanel(root,main) 34 | videowindow = videoplayer.VideoPlayer(root,main.outputdir,main.fps) 35 | toolbar_project = toolbar.ToolBar(root,main,videowindow) 36 | 37 | # export command - first checks the file size of the outputdir and then runs the animate function from Animator.py 38 | def export_video(outputdir): 39 | export_path = join(os.getcwd(),outputdir) 40 | if len([f for f in os.listdir(export_path)]) <= 1: 41 | messagebox.showerror("Export Error","Nothing to export") 42 | return 43 | file = filedialog.asksaveasfilename(title = "Export Your Animation") 44 | if file is not None: 45 | animate = Animator(main.outputdir,main.fps,file) 46 | 47 | if __name__ == "__main__": 48 | root = Tk() 49 | main = Main() 50 | icon = PhotoImage(file = f"{join(os.getcwd(),'MAESTOSO.png')}") 51 | root.iconphoto(False,icon) 52 | root.title("Maestoso") 53 | root.geometry("1440x900") 54 | root.configure(background='gray12') 55 | menubar = Menu(root) 56 | root.protocol("WM_DELETE_WINDOW",lambda: [root.destroy(),delete_frames(main.outputdir)]) 57 | filemenu = Menu(menubar,tearoff = 0) 58 | preferences = Menu(menubar,tearoff=0) 59 | menubar.add_cascade(label='File',menu=filemenu) 60 | menubar.add_cascade(label = 'Preferences',menu = preferences) 61 | 62 | 63 | 64 | filemenu.add_command(label="Import Midi",command = lambda:open_file()) 65 | filemenu.add_command(label="Import Theme", command = import_theme) 66 | filemenu.add_command(label="Export", state = DISABLED) 67 | filemenu.add_command(label="Exit",command = root.destroy) 68 | 69 | 70 | preferences.add_command(label = "Adjust Frame Rate",command = lambda: framerate_popup.create_render_toplevel(root,main)) 71 | preferences.add_command(label = "Edit Selected Theme", command = lambda: theme_functions.customize_theme(root,main),state = DISABLED) 72 | preferences.add_command(label = "View Theme Description", command = lambda: theme_functions.display_theme_properties(root,main),state = DISABLED) 73 | 74 | root.configure(menu=menubar) 75 | root.resizable(0,0) 76 | 77 | 78 | root.mainloop() 79 | -------------------------------------------------------------------------------- /View/instrument_panel.py: -------------------------------------------------------------------------------- 1 | """ THIS FILE CONTAINS THE INSTRUMENTPANEL AND INSTRUMENTBLOCK CLASSES""" 2 | 3 | from tkinter import * 4 | from tkmacosx import Button as BT 5 | import tkinter.font 6 | from tkinter import colorchooser 7 | import os, sys 8 | currentdir = os.path.dirname(os.path.realpath(__file__)) 9 | parentdir = os.path.dirname(currentdir) 10 | sys.path.append(parentdir) 11 | from utility import return_random, initialize_colours 12 | from View import scrollable_frame 13 | 14 | #the main panel 15 | class InstrumentPanel: 16 | def __init__(self,root,main): 17 | #create a vertical scrollable canvas 18 | self.scrollable = scrollable_frame.VerticalScrolledFrame(root, height = 900, width = 305, bg = 'gray12') 19 | self.scrollable.place(x = 0, y = 0, anchor = NW) 20 | self.create_panel(main) 21 | 22 | #create instrument panels from InstrumentBlock objects 23 | def create_panel(self,main): 24 | for i in main.project_tracklist: 25 | instrument = InstrumentBlock(self.scrollable,main,i) 26 | 27 | 28 | 29 | class InstrumentBlock: 30 | def __init__(self,parent,main,instrument): 31 | self.name = instrument.name 32 | self.colour = instrument.colour 33 | self.specialcode = instrument.specialcode #specialcode relating to the instrument class in init_tracklist.py 34 | self.frame = Frame(parent,bg = "gray9", height = 100, width = 300, border = 1, relief = RIDGE) 35 | self.frame.pack(side = TOP) 36 | self.frame.pack_propagate(0) 37 | self.nametag = Label(self.frame,text = self.name,bg = "gray9", fg = "white") 38 | self.nametag.pack(side = TOP) 39 | self.lower_frame = Frame(self.frame, width = 300, height = 20, bg = "gray9") #the lower frame holding the colour and checkbox widgets 40 | self.lower_frame.pack(side = BOTTOM) 41 | self.lower_frame.pack_propagate(0) 42 | self.create_colour_box(main) 43 | self.create_check_box(main) 44 | 45 | 46 | def create_colour_box(self,main): 47 | self.colour_box = BT(self.lower_frame, bg = self.colour, height = 20, width = 20, borderless = TRUE, command = lambda: self.change_colour(main)) 48 | self.colour_box.pack(side = RIGHT) 49 | 50 | def create_check_box(self,main): 51 | self.checkvar = IntVar() 52 | self.check_box = Checkbutton(self.lower_frame, variable = self.checkvar, text = "Active", bg = "gray9", fg = "white", onvalue = 1, offvalue = 0, command = lambda: self.make_inactive(main)) 53 | self.check_box.pack(side = RIGHT) 54 | self.check_box.select() 55 | 56 | def change_colour(self,main): 57 | color_code = colorchooser.askcolor(title = " Select Colour") 58 | for i in main.project_tracklist: 59 | if i.specialcode == self.specialcode: 60 | i.colour = color_code[1] 61 | self.colour = i.colour 62 | self.colour_box.config(bg = self.colour) 63 | 64 | def make_inactive(self,main): 65 | for i in main.project_tracklist: 66 | if i.specialcode == self.specialcode: 67 | if self.checkvar.get() == 1: 68 | i.active = True 69 | elif self.checkvar.get() == 0: 70 | i.active = False 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /View/scrollable_frame.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | 3 | class VerticalScrolledFrame: 4 | def __init__(self, master, **kwargs): 5 | width = kwargs.pop('width', None) 6 | height = kwargs.pop('height', None) 7 | bg = kwargs.pop('bg', kwargs.pop('background', None)) 8 | self.outer = tk.Frame(master, **kwargs) 9 | 10 | self.vsb = tk.Scrollbar(self.outer, orient=tk.VERTICAL) 11 | self.vsb.pack(fill=tk.Y, side=tk.RIGHT) 12 | self.canvas = tk.Canvas(self.outer, highlightthickness=0, width=width, height=height, bg=bg) 13 | self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) 14 | self.canvas['yscrollcommand'] = self.vsb.set 15 | self.canvas.bind("", self._bind_mouse) 16 | self.canvas.bind("", self._unbind_mouse) 17 | self.vsb['command'] = self.canvas.yview 18 | 19 | self.inner = tk.Frame(self.canvas, bg=bg) 20 | self.canvas.create_window(4, 4, window=self.inner, anchor='nw') 21 | self.inner.bind("", self._on_frame_configure) 22 | 23 | self.outer_attr = set(dir(tk.Widget)) 24 | 25 | def __getattr__(self, item): 26 | if item in self.outer_attr: 27 | return getattr(self.outer, item) 28 | else: 29 | return getattr(self.inner, item) 30 | 31 | def _on_frame_configure(self, event=None): 32 | x1, y1, x2, y2 = self.canvas.bbox("all") 33 | height = self.canvas.winfo_height() 34 | self.canvas.config(scrollregion = (0,0, x2, max(y2, height))) 35 | 36 | def _bind_mouse(self, event=None): 37 | self.canvas.bind_all("<4>", self._on_mousewheel) 38 | self.canvas.bind_all("<5>", self._on_mousewheel) 39 | self.canvas.bind_all("", self._on_mousewheel) 40 | 41 | def _unbind_mouse(self, event=None): 42 | self.canvas.unbind_all("<4>") 43 | self.canvas.unbind_all("<5>") 44 | self.canvas.unbind_all("") 45 | 46 | def _on_mousewheel(self, event): 47 | if event.num == 4 or event.delta > 0: 48 | self.canvas.yview_scroll(-1, "units" ) 49 | elif event.num == 5 or event.delta < 0: 50 | self.canvas.yview_scroll(1, "units" ) 51 | 52 | def __str__(self): 53 | return str(self.outer) 54 | 55 | 56 | class HorizontalScrolledFrame: 57 | def __init__(self, master, **kwargs): 58 | width = kwargs.pop('width', None) 59 | height = kwargs.pop('height', None) 60 | bg = kwargs.pop('bg', kwargs.pop('background', None)) 61 | self.outer = tk.Frame(master, **kwargs) 62 | 63 | self.vsb = tk.Scrollbar(self.outer, orient=tk.HORIZONTAL) 64 | self.vsb.pack(fill=tk.X, side=tk.BOTTOM) 65 | self.canvas = tk.Canvas(self.outer, highlightthickness=0, width=width, height=height, bg=bg) 66 | self.canvas.pack(side=tk.TOP, fill=tk.BOTH, expand=True) 67 | self.canvas['xscrollcommand'] = self.vsb.set 68 | self.canvas.bind("", self._bind_mouse) 69 | self.canvas.bind("", self._unbind_mouse) 70 | self.vsb['command'] = self.canvas.xview 71 | 72 | self.inner = tk.Frame(self.canvas, bg=bg) 73 | self.canvas.create_window(4, 4, window=self.inner, anchor='nw') 74 | self.inner.bind("", self._on_frame_configure) 75 | 76 | self.outer_attr = set(dir(tk.Widget)) 77 | 78 | def __getattr__(self, item): 79 | if item in self.outer_attr: 80 | return getattr(self.outer, item) 81 | else: 82 | return getattr(self.inner, item) 83 | 84 | def _on_frame_configure(self, event=None): 85 | x1, y1, x2, y2 = self.canvas.bbox("all") 86 | height = self.canvas.winfo_height() 87 | self.canvas.config(scrollregion = (0,0, x2, max(y2, height))) 88 | 89 | def _bind_mouse(self, event=None): 90 | self.canvas.bind_all("<4>", self._on_mousewheel) 91 | self.canvas.bind_all("<5>", self._on_mousewheel) 92 | self.canvas.bind_all("", self._on_mousewheel) 93 | 94 | def _unbind_mouse(self, event=None): 95 | self.canvas.unbind_all("<4>") 96 | self.canvas.unbind_all("<5>") 97 | self.canvas.unbind_all("") 98 | 99 | def _on_mousewheel(self, event): 100 | if event.num == 4 or event.delta > 0: 101 | self.canvas.xview_scroll(-1, "units" ) 102 | elif event.num == 5 or event.delta < 0: 103 | self.canvas.xview_scroll(1, "units" ) 104 | 105 | def __str__(self): 106 | return str(self.outer) -------------------------------------------------------------------------------- /View/videoplayer.py: -------------------------------------------------------------------------------- 1 | """ THIS FILE HOLDS THE VIDEOPLAYER CLASS""" 2 | 3 | from tkinter import * 4 | import cv2 5 | from PIL import ImageTk, Image 6 | from os import getcwd, listdir 7 | from os.path import join 8 | from utility import max_frame_number, frame_rate_note 9 | 10 | #create a videoplayer class 11 | class VideoPlayer: 12 | def __init__(self,root,outputdir,fps): 13 | self.parentframe = Frame(root,bg = "gray12", width = 1125, height = 830, relief = SUNKEN, bd = 5) #the main frame 14 | self.parentframe.place(x = 315, y = 70, anchor = NW) 15 | self.parentframe.pack_propagate(0) 16 | self.bottomframe = Frame(self.parentframe,bg = "gray10", width = 1125, height = 110) #the frame that holds the slider 17 | self.bottomframe.pack(side = BOTTOM) 18 | self.bottomframe.pack_propagate(0) 19 | self.slider = Scale(self.bottomframe, orient = HORIZONTAL, length = 1125, bg = "gray12", fg = "white",state = DISABLED) #create a slider 20 | self.slider.pack(side = TOP) 21 | self.outputdir = join(getcwd(),outputdir) #create a path to the outputdir 22 | 23 | self.generate_text_labels() 24 | 25 | #this method creates the appropriate text labels on the parent frame 26 | def generate_text_labels(self, text = True): 27 | if text == True: 28 | content = "Everything seems so empty...why not do something about it?" 29 | else: 30 | content = "" 31 | self.rendertext = Label(self.parentframe,text = content , bg = "gray12", fg = "white") 32 | self.rendertext.place(x = 350, y = 450) 33 | self.loading_text = Label(self.parentframe,bg = "gray12", fg = "white") 34 | self.loading_text.place(x = 450, y = 600) 35 | 36 | 37 | #this method updates self.image_display depending on the val retrieved from self.slider 38 | def update_image(self,val): 39 | self.frame.config(file = f'{self.outputdir}/frame_{val}.png') 40 | self.image_display.config(image = self.frame) 41 | 42 | #this method is called from toolbar.py when the animation is render 43 | def display_video(self): 44 | self.slider.set(0) #set slider value to 0 45 | self.slider.config(state = ACTIVE, command = lambda val : self.update_image(val)) #active slider 46 | self.max_frame = round(max_frame_number(self.outputdir)) #the max_frame count retrieved using the max_frame_number func from utility.py 47 | self.slider.config(from_=0, to = self.max_frame) 48 | self.frame = PhotoImage(file = f'{self.outputdir}/frame_0.png') 49 | self.image_display = Label(self.parentframe, image = self.frame) 50 | self.image_display.pack() 51 | self.image_display.config(image = self.frame) 52 | 53 | #this function runs when render is called from toolbar.py... it disables the slider and destroys three widgets from the screen 54 | def while_render(self): 55 | self.slider.config(state = DISABLED) 56 | try: 57 | self.loading_text.destroy() 58 | self.rendertext.destroy() 59 | self.image_display.destroy() 60 | except: 61 | pass 62 | 63 | #the loading screen function 64 | def generate_loading_screen(self,main,preview): 65 | dir_size = len([f for f in listdir(self.outputdir)]) - 1 #retrieve the size of the directory 66 | self.parentframe.after(5000) #wait for 5 seconds in order to avoid collision in case main.animate returns an error 67 | if len([f for f in listdir(self.outputdir)]) - 1 == dir_size: #check if the main function returned an error 68 | return 69 | 70 | self.generate_text_labels(text = False) #create text labels 71 | 72 | endtime = main.endtime 73 | fps = main.fps 74 | prev_time = main.preview_time 75 | minimum_frames = 0.75 # a safe number to minimize bugs due to discrepencies 76 | self.rendertext.config(text = " " * 22 + "Rendering your animation...Please be patient.") 77 | if preview == True and prev_time <= endtime: 78 | est_frame_count = frame_rate_note(prev_time,fps) 79 | else: 80 | est_frame_count = frame_rate_note(endtime,fps) 81 | 82 | dir_size = len([f for f in listdir(self.outputdir)]) - 1 83 | while dir_size < est_frame_count * minimum_frames: 84 | prog_percentage = round((dir_size * 100)/(est_frame_count * minimum_frames)) 85 | self.loading_text.config(text = f'-----------------{prog_percentage}% completed') #Update self.loading text with the prog_percentage val 86 | dir_size = len([f for f in listdir(self.outputdir)]) - 1 87 | 88 | self.rendertext.config(text = " " * 22 + "Almost there....Just hang in there soldier.") 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /init_tracklist.py: -------------------------------------------------------------------------------- 1 | """ THIS FILE CONTAINS THE NOTE, INSTRUMENT AND TRACKLIST CLASSES""" 2 | 3 | 4 | import pretty_midi 5 | import mido 6 | 7 | #the note class assigned to each onset 8 | class Note: 9 | def __init__(self,pitch,start_time,end_time,velocity,ts,note_length,start_ticks,end_ticks): 10 | self.pitch = pitch 11 | self.start_time = start_time 12 | self.end_time = end_time 13 | self.velocity = velocity #intensity of the note 14 | self.ts = ts #time signature 15 | self.note_length = note_length #note length in integers - 1 = quarternote, etc 16 | self.start_ticks = start_ticks 17 | self.end_ticks = end_ticks 18 | 19 | 20 | def return_duration(self): 21 | return round(self.end_time - self.start_time,3) 22 | 23 | def return_numerator(self): 24 | return self.ts[0] 25 | 26 | def return_denominator(self): 27 | return self.ts[1] 28 | 29 | #instrument class built from notes 30 | class Instrument: 31 | def __init__(self,notes,name,category,duration,specialcode, isdrum = False): 32 | self.notes = notes #all notes within the instrument 33 | self.name = name 34 | self.isdrum = isdrum #if instrument is a drum type 35 | self.category = category #the category assigned to the instrument from MIDI information 36 | self.duration = duration #the total duration of the instrument 37 | self.colour = 'colour' #colour assigned to the instrument from initialize_colours func in utility.py 38 | self.active = True #True if instrument is active 39 | self.specialcode = specialcode #a special code assigned to each instrument and all notes within it 40 | 41 | def change_shape(self): 42 | pass 43 | 44 | def change_colour(self): 45 | pass 46 | 47 | 48 | class TrackList: 49 | def __init__(self,filedir): 50 | self.number_of_instruments = 0 51 | self.midifile = pretty_midi.PrettyMIDI(filedir) #create a midi file object 52 | self.track_list = [] 53 | self.PPQN = self.determine_ppqn(filedir) 54 | self.pulse = self.midifile.get_beats() #all the pulses retrieved using the midifile.get_beats() method 55 | self.ts_changes = self.midifile.time_signature_changes #list of all time_signature changes 56 | self.drum_count = 0 #how many drum tracks exist in a given tracklist 57 | self.construct_tracklist() 58 | self.tempo_changes = self.midifile.get_tempo_changes() #retrieve all tempo changes 59 | self.accented_beats = self.midifile.get_downbeats() #find all downbeats 60 | self.selected_animation = '' 61 | 62 | #determine the PPQN of the tracklist 63 | def determine_ppqn(self,filedir): 64 | ppqn_mid = mido.MidiFile(filedir) 65 | return ppqn_mid.ticks_per_beat 66 | 67 | def construct_tracklist(self): 68 | instrument_name = 'No Name' 69 | for i in self.midifile.instruments: 70 | self.number_of_instruments += 1 71 | instrument = [] 72 | i.remove_invalid_notes() 73 | if i.name == 'No Name' and i.is_drum == False: 74 | instrument_name = "Instrument {}".format(self.number_of_instruments) 75 | elif i.name == 'No Name' and i.is_drum == True: 76 | self.drum_count += 1 77 | instrument_name = "Drum Instrument {}".format(self.drum_count) 78 | else: 79 | instrument_name = i.name 80 | 81 | if len(self.ts_changes) == 1: 82 | for j in i.notes: 83 | note_length = self.midifile.time_to_tick(j.duration)/self.PPQN 84 | start_tick = self.midifile.time_to_tick(j.start) 85 | end_tick = self.midifile.time_to_tick(j.end) 86 | instrument.append(Note(j.pitch,j.start,j.end,j.velocity,(self.ts_changes[0].numerator,self.ts_changes[0].denominator),note_length,start_tick,end_tick)) 87 | 88 | elif len(self.ts_changes) > 1: #add time signature information for each note 89 | for j in range(len(self.ts_changes)): 90 | if j != len(self.ts_changes) - 1: 91 | for k in i.notes: 92 | if k.start >= self.ts_changes[j].time and k.start < self.ts_changes[j+1].time: 93 | note_length = self.midifile.time_to_tick(k.duration)/self.PPQN 94 | start_tick = self.midifile.time_to_tick(k.start) 95 | end_tick = self.midifile.time_to_tick(k.end) 96 | if k.start < self.ts_changes[-1].time: 97 | instrument.append(Note(k.pitch,k.start,k.end,k.velocity,(self.ts_changes[j].numerator,self.ts_changes[j].denominator),note_length,start_tick,end_tick)) 98 | elif k.start >= self.ts_changes[-1].time: 99 | instrument.append(Note(k.pitch,k.start,k.end,k.velocity,(self.ts_changes[-1].numerator,self.ts_changes[-1].denominator),note_length,start_tick,end_tick)) 100 | 101 | self.track_list.append(Instrument(instrument,instrument_name,i.program,i.get_end_time(),self.number_of_instruments,i.is_drum)) 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /View/customization_classes.py: -------------------------------------------------------------------------------- 1 | """ THIS FILE HOLDS ALL THE CUSTOMIZABLE CLASSES FOR THEMES""" 2 | 3 | from tkinter import * 4 | from tkmacosx import Button as BT 5 | from tkinter import colorchooser 6 | 7 | """ 8 | All widgets created in this file follow a pattern that must be followed for a seamless functionality across various scripts. 9 | 10 | 1) Their attributes must all be the same: the standard attr are self.name, self.default_val, self.selection, self.r, self.c, and self.parent_frame 11 | 2) They must all contain a self.construct() method 12 | 13 | 14 | """ 15 | # THE COLORBOX WIDGET 16 | class ColorButton: 17 | def __init__(self,name,default_val,r,c,parent_frame): 18 | self.name = name #the name of the widget - used as a label in the theme_functions.py file 19 | self.default_val = default_val #the default value of the widget 20 | self.selection = '' #the attr the selected value is assigned to 21 | self.r = r #row 22 | self.c = c #column 23 | self.parent_frame = parent_frame #the parent frame that holds the widget 24 | 25 | #all widgets must have a construct method that creates the entire widget to be called from theme_functions.py 26 | def construct(self): 27 | self.colour_box = BT(self.parent_frame, bg = self.default_val, height = 20, width = 20, borderless = TRUE, command = lambda: self.change_colour()) 28 | self.colour_box.grid(row = self.r, column = self.c, sticky = W) 29 | self.colourlabel = Label(self.parent_frame,text = self.name, bg = "gray9", fg = 'white') 30 | self.colourlabel.grid(row = self.r, column = self.c + 1, sticky = W) 31 | 32 | #changes colour of the widget and adds that value to the self.selection attr 33 | def change_colour(self): 34 | color_code = colorchooser.askcolor(title = " Select Colour") 35 | self.selection = color_code[1] 36 | self.colour_box.config(bg = self.selection) 37 | 38 | #THE DROPDOWN WIDGET 39 | class Dropdown: 40 | def __init__(self,name,default_val,items,r,c,parent_frame): 41 | self.name = name 42 | self.default_val = default_val 43 | self.items = items 44 | self.clicked = StringVar() 45 | self.clicked.set(self.default_val) 46 | self.selection = '' 47 | self.r = r 48 | self.c = c 49 | self.parent_frame = parent_frame 50 | 51 | def construct(self): 52 | self.dropdown = OptionMenu(self.parent_frame, self.clicked,*self.items, command = self.change_selection) 53 | self.dropdown["menu"].config(bg = "gray9") 54 | self.dropdown.grid(row = self.r, column = self.c, sticky = W) 55 | self.dropdownlabel = Label(self.parent_frame,text = self.name, bg = "gray9", fg = 'white') 56 | self.dropdownlabel.grid(row = self.r, column = self.c + 1, sticky = W) 57 | 58 | #sets the value of self.selection to the selected item 59 | def change_selection(self,event): 60 | self.selection = self.clicked.get() 61 | 62 | #THE CHECKBOX WIDGET 63 | class CheckBox: 64 | def __init__(self,name,default_val,r,c,parent_frame): 65 | self.name = name 66 | self.default_val = default_val 67 | self.checkvar = IntVar() 68 | self.selection = '' 69 | self.r = r 70 | self.c = c 71 | self.parent_frame = parent_frame 72 | 73 | def construct(self): 74 | self.checkbox = Checkbutton(self.parent_frame, variable = self.checkvar, bg = "gray9", fg = "white", onvalue = 1, offvalue = 0, command = self.change_value) 75 | if self.default_val == True: 76 | self.checkbox.select() 77 | self.checkbox.grid(row = self.r, column = self.c, sticky = W) 78 | self.checklabel = Label(self.parent_frame,text = self.name, bg = "gray9", fg = 'white') 79 | self.checklabel.grid(row = self.r, column = self.c + 1, sticky = W) 80 | 81 | def change_value(self): 82 | if self.checkvar.get() == 1: 83 | self.selection = True 84 | elif self.checkvar.get() == 0: 85 | self.selection = False 86 | 87 | #THE ENTRY BOX WIDGET 88 | """ 89 | The entry widget allows for both str and int type values. self.type holds the entry type 90 | """ 91 | class EntryBox: 92 | def __init__(self,name,default_val,r,c,parent_frame,entry_limit,entry_range,entry_type): 93 | self.name = name 94 | self.default_val = default_val 95 | self.r = r 96 | self.c = c 97 | self.parent_frame = parent_frame 98 | self.range = entry_range #the range of digits - stored as a tuple() where range[0] is the starting value and range[1] is the max value 99 | self.type = entry_type 100 | if entry_limit < 2: #the number of chars that are allowed 101 | self.limit = 2 102 | else: 103 | self.limit = entry_limit 104 | 105 | self.selection = '' 106 | self.checkvar = StringVar() 107 | if self.type == 'int': 108 | self.checkvar.trace('w',self.limit_range) #call this method for integer values 109 | elif self.type == 'str': 110 | self.checkvar.trace('w',self.limit_entry) #call this method for str values 111 | 112 | self.checkvar.set(self.default_val) 113 | 114 | def construct(self): 115 | self.entry = Entry(self.parent_frame,bg = "gray9", fg = "white", textvariable = self.checkvar,width = self.limit) 116 | self.entry.grid(row = self.r, column = self.c, sticky = W) 117 | self.entrylabel = Label(self.parent_frame,text = self.name, bg = "gray9", fg = 'white') 118 | self.entrylabel.grid(row = self.r, column = self.c + 1, sticky = W) 119 | 120 | def limit_entry(self,*args): 121 | value = self.checkvar.get() 122 | try: 123 | float(value) 124 | self.checkvar.set("") #if the inserted value is an int 125 | except: 126 | if len(value) > self.limit: # if lenth of letters is greater than self.limit 127 | self.checkvar.set(value[:self.limit]) 128 | 129 | def limit_range(self,*args): 130 | value = self.checkvar.get() 131 | try: 132 | float(value) #if the value is either a float or an int 133 | except: 134 | self.checkvar.set("") #if value is not an int or float 135 | return 136 | if float(value) < float(self.range[0]) or float(value) > float(self.range[1]): 137 | self.checkvar.set(0) 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /View/theme_functions.py: -------------------------------------------------------------------------------- 1 | """ THIS FILE HOLDS THE TWO THEME RELATED FUNCTIONS""" 2 | 3 | from tkinter import * 4 | from tkinter import messagebox 5 | from tkmacosx import Button as BT 6 | from View import customization_classes as custom 7 | from View import scrollable_frame 8 | 9 | 10 | # called when the customize option is selected from the preferences menu 11 | def customize_theme(root,main): 12 | #check if any animation is selected 13 | if main.selected_animation == '': 14 | messagebox.showwarning("Import Error","No Theme Selected") 15 | return 16 | else: 17 | selected_theme = main.selected_animation 18 | 19 | # create a list called customizable elements from the instance of selected animation type stored in main.theme_data 20 | for i in main.theme_data: 21 | if i.display_name == selected_theme: 22 | customizable_elements = i.customizable_elements 23 | 24 | 25 | root_x = root.winfo_rootx() 26 | root_y = root.winfo_rooty() 27 | x_pos = root_x + 500 28 | y_pos = root_y + 200 29 | #create a top level window to display the customizable elements 30 | top = Toplevel(bg = 'gray9') 31 | top.geometry(f'500x300+{x_pos}+{y_pos}') 32 | top.resizable(0,0) 33 | top.title("Customize Selected Theme") 34 | top.grab_set() 35 | #create a horizontal scroll bar 36 | scrollable_canvas = scrollable_frame.HorizontalScrolledFrame(top,height = 250, width = 500,bg = "gray9") 37 | scrollable_canvas.place(x = 0, y = 0, anchor = NW) 38 | # the top frame that holds all the customizable widgets 39 | widget_holder = Frame(scrollable_canvas, bg = "gray9", width = 500, height = 250) 40 | widget_holder.pack(side = TOP) 41 | #bottom frame that holds the confirm button 42 | bottom_frame = Frame(top, bg = "gray9", width = 500, height = 50) 43 | bottom_frame.pack(side = BOTTOM) 44 | 45 | #function to create a confirm button 46 | def create_confirm_button(r,c): 47 | conf = BT(bottom_frame, bg = "gray6", fg = "white",height = 40, width = 120, borderless = TRUE, text = "Confirm Changes", command = lambda: make_changes(widget_list)) 48 | conf.pack(side = LEFT) 49 | 50 | #makes changes to the elements in customizable elements based on the selected variable values 51 | def make_changes(lst): 52 | for i in main.theme_data: 53 | if i.display_name == selected_theme: 54 | for j in i.customizable_elements: 55 | if j[1] != "ENTRY": 56 | if lst[i.customizable_elements.index(j)].selection != '': 57 | j[2] = lst[i.customizable_elements.index(j)].selection 58 | else: 59 | j[2] = lst[i.customizable_elements.index(j)].default_val 60 | elif j[1] == "ENTRY": 61 | j[2] = lst[i.customizable_elements.index(j)].checkvar.get() 62 | 63 | top.destroy() 64 | 65 | #constructs widget list from customizable elements 66 | def construct_widget_list(): 67 | r = 0 #row number 68 | c = 0 #column number 69 | counter = 1 70 | output_list = [] 71 | for i in customizable_elements: #i[0] == name, i[1] == type, i[2] == default_val, i[3] == items 72 | if i[1] == 'COLORBOX': 73 | wid = custom.ColorButton(i[0],i[2],r,c,widget_holder) 74 | output_list.append(wid) 75 | elif i[1] == 'CHECKBOX': 76 | wid = custom.CheckBox(i[0],i[2],r,c,widget_holder) 77 | output_list.append(wid) 78 | elif i[1] == 'DROPDOWN': 79 | wid = custom.Dropdown(i[0],i[2],i[3],r,c,widget_holder) 80 | output_list.append(wid) 81 | elif i[1] == 'ENTRY': 82 | if i[4] == "str": 83 | wid = custom.EntryBox(i[0],i[2],r,c,widget_holder,i[3],'',i[4]) 84 | elif i[4] == "int": 85 | wid = custom.EntryBox(i[0],i[2],r,c,widget_holder,len(str(i[3][1])),i[3],i[4]) 86 | 87 | output_list.append(wid) 88 | if counter < 7: 89 | r += 1 90 | counter += 1 91 | if counter == 7: 92 | r = 0 93 | c += 2 94 | counter = 1 95 | 96 | create_confirm_button(r,c) 97 | 98 | return output_list 99 | 100 | widget_list = construct_widget_list() 101 | 102 | for i in widget_list: 103 | i.construct() 104 | 105 | #this function displays the theme properties of the selected theme 106 | def display_theme_properties(root,main): 107 | if main.selected_animation == '': 108 | messagebox.showwarning("Import Error","No Theme Selected") 109 | return 110 | else: 111 | selected_theme = main.selected_animation 112 | 113 | for i in main.theme_data: 114 | if i.display_name == selected_theme: 115 | customizable_elements = i.customizable_elements 116 | theme_description = i.theme_description 117 | 118 | root_x = root.winfo_rootx() 119 | root_y = root.winfo_rooty() 120 | x_pos = root_x + 500 121 | y_pos = root_y + 200 122 | top = Toplevel(bg = 'gray9') 123 | top.geometry(f'500x300+{x_pos}+{y_pos}') 124 | top.resizable(0,0) 125 | top.title("Theme Description") 126 | top.grab_set() 127 | 128 | title_holder = Frame(top,bg = "gray6",width = 500, height = 50) 129 | title_holder.propagate(0) 130 | title_holder.pack(side = TOP) 131 | 132 | title_label = Label(title_holder,bg = "gray6", text = f'{selected_theme} Theme', fg = "white", font = ("Arial", 25)) 133 | title_label.pack(side = TOP, pady = 10) 134 | 135 | 136 | description_holder = Frame(top, bg = "gray9", width = 500, height = 200) 137 | description_holder.propagate(0) 138 | description_holder.place(x = 0, y = 50) 139 | description_label = Label(description_holder,bg = "gray9", text = theme_description[0], fg = "white", font = ("Arial", 14), wraplength = 500) 140 | description_label.pack(side = TOP, pady = 20) 141 | 142 | 143 | bottom_frame = Frame(top, bg = "gray6", width = 500, height = 50) 144 | bottom_frame.propagate(0) 145 | bottom_frame.pack(side = BOTTOM) 146 | bottom_label = Label(bottom_frame, bg = "gray6", text = theme_description[1], fg = "white", font = ("Arial", 12), wraplength = 500) 147 | bottom_label.pack(side = TOP, pady = 10) 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /tracklist_functions.py: -------------------------------------------------------------------------------- 1 | ''' FUNCTIONS ASSOCIATED WITH TRACKLIST ''' 2 | 3 | from pretty_midi import note_number_to_drum_name, note_number_to_name 4 | from pychord import note_to_chord 5 | 6 | def find_pitch_range(tracklist): 7 | """[RETURNS AN ORDERED NON-REPEATING LIST OF ALL PITCHES THAT LIE BETWEEN THE MIN AND MAX PITCH IN TRACKLIST] 8 | 9 | Args: 10 | tracklist ([Project_Tracklist class]) 11 | 12 | Returns: 13 | [list]: [ordered pitches in the tracklist] 14 | """ 15 | notes = [] 16 | for i in tracklist: 17 | if i.isdrum == False: 18 | for j in i.notes: 19 | notes.append(j.pitch) 20 | 21 | max_note = max(notes) 22 | min_note = min(notes) 23 | pitch_range = [i for i in range(min_note,max_note + 1)] 24 | return pitch_range 25 | 26 | 27 | def find_pitch_range_monophonic(monophonic): 28 | """[RETURNS AN ORDERED NON-REPEATING LIST OF ALL PITCHES THAT LIES BETWEEN THE MIN AND MAX PITCH IN A MONOPHONIC MELODY] 29 | 30 | Args: 31 | monophonic ([list]): [Monophonic] 32 | 33 | Returns: 34 | [list]: [pitch range] 35 | """ 36 | notes = [i.pitch for i in monophonic] 37 | max_note = max(notes) 38 | min_note = min(notes) 39 | pitch_range = [i for i in range(min_note,max_note + 1)] 40 | return pitch_range 41 | 42 | 43 | def create_timecode_hash(tracklist): # creates a polyphonic dictionary 44 | """[CREATES A POLYPHONIC DICTIONARY WHERE ALL NOTES THAT OCCUR ON THE SAME ONSET ARE STORED AS VALUES IN THE ONSET KEY] 45 | 46 | Args: 47 | tracklist ([tracklist.track_list): [tracklist attr of Tracklist] 48 | 49 | Returns: 50 | [dict]: [key:value pairs where key = onset and values = all notes that occur at the onset] 51 | """ 52 | polyphonic_hash = {} 53 | timecodes = [] 54 | 55 | for i in tracklist: 56 | if i.active == True: 57 | if i.isdrum == False: 58 | for j in i.notes: 59 | timecodes.append(j.start_time) 60 | j.colour = i.colour # add note colour attribute to Note class 61 | j.specialcode = i.specialcode #add a unique identifier to each note 62 | timecodes_nonrepeating = list(set(timecodes)) 63 | timecodes_nonrepeating.sort() 64 | for i in timecodes_nonrepeating: 65 | polyphonic_hash.update({i:[]}) 66 | 67 | for i in tracklist: 68 | if i.active == True: 69 | if i.isdrum == False: 70 | for j in i.notes: 71 | polyphonic_hash[j.start_time].append(j) 72 | 73 | return polyphonic_hash 74 | 75 | 76 | def create_monophonic(polyphonic_hash): 77 | """[DERIVES A MONOPHONIC MELODY FROM A POLYPHONIC DICTIONARY] 78 | 79 | Args: 80 | polyphonic_hash ([dict): [polyphonic map created from the create_timecode_hash func] 81 | 82 | Returns: 83 | [list]: [all the notes that best fit the monophonic layout of the music] 84 | """ 85 | monophonic_list = [] 86 | for k, v in polyphonic_hash.items(): 87 | temp_list = [] 88 | for note in v: 89 | temp_list.append(note.pitch) 90 | temp_list = list(set(temp_list)) #remove all repeating pitch values 91 | index = temp_list.index(max(temp_list)) 92 | monophonic_list.append(v[index]) 93 | return monophonic_list 94 | 95 | 96 | def create_drum_hash(tracklist): 97 | """[CREATES A DRUM MAP OF ALL PERCUSSIVE NOTES THAT OCCUR AT THE SAME ONSET] 98 | 99 | Args: 100 | tracklist ([tracklist.track_list): [tracklist attr of Tracklist] 101 | 102 | Returns: 103 | [dict]: [key:value pairs where key = onset and values = all notes that occur at the onset] 104 | """ 105 | drum_hash = {} 106 | timecodes = [] 107 | 108 | for i in tracklist: 109 | if i.isdrum == True and i.active == True: 110 | for j in i.notes: 111 | timecodes.append(j.start_time) 112 | j.colour = i.colour #assigns the note colour from selected instrument colour 113 | #isolate only the non-repeating onsets 114 | timecodes_nonrepeating = list(set(timecodes)) 115 | timecodes_nonrepeating.sort() 116 | for i in timecodes_nonrepeating: 117 | drum_hash.update({i:[]}) 118 | 119 | for i in tracklist: 120 | if i.isdrum == True and i.active == True: 121 | for j in i.notes: 122 | drum_hash[j.start_time].append(j) 123 | 124 | return drum_hash 125 | 126 | 127 | def find_all_drums(tracklist,name = False): # finds all the unique drum samples within the tracklist if array = True else returns the number of drum samples 128 | """[FINDS ALL THE UNIQUE DRUM SAMPLES FROM DRUM INSTRUMENTS] 129 | 130 | Args: 131 | tracklist ([Tracklist.track_list]): [tracklist attr of Tracklist] 132 | name (bool, optional): [set to true if you want the function to return a list with name instead of pitch values]. Defaults to False. 133 | 134 | Returns: 135 | [list]: [all unique drum names/pitches] 136 | """ 137 | midi_list = [] 138 | count = 1 139 | for i in tracklist: 140 | if i.isdrum == True: 141 | if i.active == True: 142 | for j in i.notes: 143 | if name == True: 144 | try: 145 | midi_list.append(note_number_to_drum_name(j.pitch)) 146 | except: 147 | midi_list.append(f"Drum {count}") 148 | count += 1 149 | else: 150 | midi_list.append(j.pitch) 151 | 152 | update_list = list(set(midi_list)) 153 | return update_list 154 | 155 | 156 | 157 | def create_chord_list(project_tracklist,PPQN): 158 | """[CREATES A LIST OF CHORDS FROM LIST] 159 | 160 | Args: 161 | project_tracklist ([Tracklist.track_list]): [tracklist attr of Tracklist] 162 | PPQN ([int]): [PPQN value required to estimate note duration] 163 | 164 | Returns: 165 | [list]: [returns a chord object list] 166 | """ 167 | polyphonic_hash = create_timecode_hash(project_tracklist) #create a polyphonic hash from the create_timecode_hash func 168 | chord_list = [] 169 | # create a chord class 170 | class Chord: 171 | def __init__(self,name,notes,start_time,end_time): 172 | self.name = name #chord name 173 | self.notes = notes #list of notes within the chord 174 | self.start_time = start_time 175 | self.end_time = end_time 176 | 177 | # this function converts pitch numbers to pitch names 178 | def convert_number_to_note(pitch_list): 179 | final_name_list = [] 180 | for i in pitch_list: 181 | note_name = note_number_to_name(i) 182 | if len(note_name) == 3: 183 | #remove the octave value from the str 184 | upd_note_name = note_name.replace(note_name[2],'') 185 | elif len(note_name) == 2: 186 | upd_note_name = note_name.replace(note_name[1],'') 187 | final_name_list.append(upd_note_name) 188 | 189 | return final_name_list 190 | 191 | #this function evaluates the weight of the given note - this is done to reduce chances of errors in chord retrieval 192 | def evaluate_note_weight(lst): 193 | counter = 0 194 | for i in lst: 195 | tick_duration = (i.end_ticks - i.start_ticks)/PPQN 196 | if tick_duration >= 0.25: #if note value is greater than 0.25 - or quaver 197 | counter += 1 198 | if counter == len(lst): 199 | return True 200 | else: 201 | return False 202 | 203 | for k, v in polyphonic_hash.items(): 204 | if len(set(v)) > 2: #if there are more than 2 unique pithces in a given value list 205 | if evaluate_note_weight(v) == True: #if all notes are greater than or equal to eight note value 206 | pitch_list = [i.pitch for i in v] #create a list of pitches 207 | pitch_list.sort() #sort the pitches in order 208 | chord_pitches = list(set(convert_number_to_note(pitch_list))) #convert pitch_list to a list of notes 209 | if len(chord_pitches) > 2: 210 | if len(note_to_chord(chord_pitches)) != 0: 211 | chord_name = note_to_chord(chord_pitches)[0]._chord #obtain chord name using the note_to_chord function in pychord 212 | else: 213 | chord_name = '' 214 | end_time = max([i.end_time for i in v]) 215 | chord_list.append(Chord(chord_name,chord_pitches,k,end_time)) 216 | else: 217 | continue 218 | else: 219 | continue 220 | else: 221 | continue 222 | 223 | return chord_list 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | -------------------------------------------------------------------------------- /THEMES/ChordCycle.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | from utility import frame_rate_note, pick_random_colour 3 | from tracklist_functions import create_chord_list, create_timecode_hash 4 | from math import sin, cos, pi 5 | import matplotlib 6 | 7 | matplotlib.use('TkAgg') 8 | 9 | class ChordCycle: 10 | def __init__(self,tracklist): 11 | self.category = 'HARMONY' 12 | self.display_name = 'Circle of Fifths' 13 | self.tracklist = tracklist 14 | self.background_color = ['Background Colour','COLORBOX','black'] 15 | self.circle_color = ['Circle Colour','COLORBOX','white'] 16 | self.beads_colour = ['Beads Colour','COLORBOX','white'] 17 | self.grid_on = ['Grid','CHECKBOX',True] 18 | self.display_chords = ['Display Chords','CHECKBOX',True] 19 | self.grid_color = ['Grid Colour','COLORBOX','white'] 20 | self.grid_width = ['Grid Width','ENTRY',0.1,(0,1),"int"] 21 | 22 | self.customizable_elements = [self.background_color,self.circle_color,self.beads_colour,self.grid_on,self.display_chords,self.grid_color,self.grid_width] 23 | 24 | self.theme_description = ["HARMONY! - that's all I have to say. This theme identifies and analyses the chords present in your MIDI file to create a chord based animation. Although, the algorithm is still not fully accurate and therefore it'd be best to use this theme with a bit of caution. Conversely, until the algorithm is developed further, you could feed an only chord MIDI file to get a more accurate output.","Created by Aman Trivedi as part of the Maestoso Project"] 25 | 26 | 27 | def ANIMATE(self,upd_project_tracklist,upd_PPQN,upd_fps,upd_output_dir,upd_preview,upd_preview_time): 28 | 29 | tracklist = self.tracklist 30 | project_tracklist = upd_project_tracklist 31 | PPQN = upd_PPQN 32 | fps = upd_fps 33 | output_dir = upd_output_dir 34 | preview = upd_preview 35 | preview_time = upd_preview_time 36 | dpi = 120 37 | aspect_ratio = (16,9) 38 | height = 720 39 | width = 1280 40 | chord_list = create_chord_list(project_tracklist,PPQN) 41 | 42 | 43 | circle_of_fifths = ['C','G','D','A','E','B','F#','C#','G#','D#','A#','F'] 44 | 45 | 46 | background_colour = self.background_color[2] 47 | circle_colour = self.circle_color[2] 48 | beads_colour = self.beads_colour[2] 49 | grid_on = self.grid_on[2] 50 | display_chords = self.display_chords[2] 51 | grid_color = self.grid_color[2] 52 | grid_width = self.grid_width[2] 53 | 54 | 55 | def pix_to_inch(size): 56 | one_pix = 1/dpi 57 | return one_pix * size 58 | 59 | 60 | class GraphAnimation: 61 | def __init__(self): 62 | plt.ioff() 63 | self.fig, self.ax = plt.subplots(figsize=(pix_to_inch(1280),pix_to_inch(720)),dpi=dpi) 64 | self.initialize_plot() 65 | 66 | def clear_patches(self): 67 | self.ax.patches = [] 68 | 69 | def initialize_plot(self): 70 | plt.xlim([0,width]) 71 | plt.ylim([0,height]) 72 | if grid_on == True: 73 | plt.grid(color = grid_color,linewidth = grid_width) 74 | elif grid_on == False: 75 | plt.grid(False) 76 | self.ax.set_xticklabels([]) 77 | self.ax.set_yticklabels([]) 78 | self.fig.patch.set_facecolor(background_colour) 79 | self.ax.patch.set_facecolor(background_colour) 80 | self.ax.axis('off') 81 | 82 | class Circle: 83 | def __init__(self): 84 | self.center = (width/2,height/2) 85 | self.radius = width/4.5 86 | 87 | def draw(self): 88 | circle = plt.Circle((self.center[0], self.center[1]), self.radius, color=circle_colour,fill=False) 89 | plt.gca().add_patch(circle) 90 | 91 | 92 | class Beads: 93 | def __init__(self,pitch,index): 94 | self.pitch = pitch 95 | self.index = index 96 | self.angle = ((360/len(circle_of_fifths)) * self.index) * pi/180 97 | self.x = ((width/4.5) * sin(self.angle)) + width/2 98 | self.y = ((width/4.5) * cos(self.angle)) + height/2 99 | self.radius = 10 100 | 101 | def draw(self): 102 | bead = plt.Circle((self.x, self.y), self.radius, color=beads_colour) 103 | plt.gca().add_patch(bead) 104 | annotate_x = (((width/4.5) * 1.2) * sin(self.angle)) + width/2 105 | annotate_y = (((width/4.5) * 1.2) * cos(self.angle)) + height/2 106 | plt.annotate(self.pitch,(annotate_x,annotate_y),color = circle_colour) 107 | 108 | 109 | class Chord: 110 | def __init__(self,chord,beads_list): 111 | self.notes = chord.notes 112 | self.colour = pick_random_colour() 113 | self.index = [circle_of_fifths.index(i) for i in self.notes] 114 | self.index.sort() 115 | self.lines = self.form_lines(beads_list) 116 | 117 | 118 | def draw(self): 119 | plt.plot(self.lines[0],self.lines[1], color = self.colour,marker= 'o') 120 | 121 | 122 | def form_lines(self,beads_list): 123 | x_coords = [] 124 | y_coords = [] 125 | for i in self.index: 126 | current_index = self.index.index(i) 127 | current_coords = (beads_list[i].x, beads_list[i].y) 128 | x_coords.append(current_coords[0]) 129 | y_coords.append(current_coords[1]) 130 | if i != self.index[-1]: 131 | next_coords = (beads_list[self.index[current_index + 1]].x, beads_list[self.index[current_index + 1]].y) 132 | else: 133 | next_coords = (beads_list[self.index[0]].x, beads_list[self.index[0]].y) 134 | x_coords.append(next_coords[0]) 135 | y_coords.append(next_coords[1]) 136 | 137 | chord_coords = [x_coords,y_coords] 138 | return chord_coords 139 | 140 | def change_colour(self): 141 | if len(self.colour) == 7: 142 | self.colour = self.colour + '4D' 143 | 144 | 145 | def animate(): 146 | graph = GraphAnimation() 147 | circle = Circle() 148 | beads_list = [] 149 | image_number = 0 150 | time_elapsed = 0 151 | chord_diagrams = [] 152 | 153 | 154 | for i in circle_of_fifths: 155 | beads_list.append(Beads(i,circle_of_fifths.index(i))) 156 | 157 | 158 | def initialize_drawings(): 159 | circle.draw() 160 | for i in beads_list: 161 | i.draw() 162 | 163 | def chord_name_annotation(chord_name): 164 | if display_chords == True: 165 | ypos = height/2 166 | xpos = (width/2) + 400 167 | plt.annotate(chord_name,(xpos,ypos),color = beads_colour) 168 | else: 169 | return 170 | 171 | initialize_drawings() 172 | 173 | for i in chord_list: 174 | if len(chord_diagrams) == 3: # if chords within the diagram equate to 3 175 | chord_diagrams.clear() 176 | 177 | time_elapsed = i.start_time 178 | 179 | if i.start_time == 0: 180 | current_frame = 0 181 | next_frame = frame_rate_note(chord_list[chord_list.index(i) + 1].start_time,fps) 182 | else: 183 | current_frame = frame_rate_note(i.start_time,fps) 184 | if i != chord_list[-1]: 185 | next_frame = frame_rate_note(chord_list[chord_list.index(i) + 1].start_time,fps) 186 | else: 187 | next_frame = frame_rate_note(5,fps) 188 | 189 | if i == chord_list[0] and i.start_time != 0: 190 | pass 191 | else: 192 | chord_name_annotation(i.name) 193 | chord_diagrams.append(Chord(i,beads_list)) 194 | for j in chord_diagrams: 195 | j.draw() 196 | 197 | frame = 0 198 | 199 | while frame < next_frame - current_frame: 200 | plt.savefig(f'{output_dir}/frame_{image_number}.png') 201 | image_number += 1 202 | frame += 1 203 | 204 | plt.cla() 205 | graph.initialize_plot() 206 | initialize_drawings() 207 | for j in chord_diagrams: 208 | j.change_colour() 209 | if preview == True and time_elapsed > preview_time: 210 | return 211 | animate() 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | -------------------------------------------------------------------------------- /View/toolbar.py: -------------------------------------------------------------------------------- 1 | """ THIS FILE HOLDS THE TOOLBAR CLASS""" 2 | from tkinter import * 3 | from tkinter.ttk import Combobox as Combo, Style 4 | from tkinter import ttk 5 | from tkmacosx import Button as BT 6 | import os, sys 7 | import multiprocessing 8 | import threading 9 | import time 10 | from View import runexcept 11 | from tkinter import messagebox 12 | 13 | 14 | currentdir = os.path.dirname(os.path.realpath(__file__)) #current directory 15 | parentdir = os.path.dirname(currentdir) #parent directory 16 | 17 | #function that triggers render 18 | def render_in_background(success,main,preview): 19 | try: 20 | main.animate(preview) #main.animate 21 | success.value = True 22 | except: 23 | success.value = True #regardless of the outcome, returns true 24 | 25 | #the toolbar class 26 | class ToolBar: 27 | def __init__(self,root,main,videowindow): 28 | ###### assigns icons ######### 29 | self.rhythmicon = PhotoImage(file = os.path.join(parentdir,'View/Resources/Icons/rhythm.png')) 30 | self.polyphonyicon = PhotoImage(file = os.path.join(parentdir,'View/Resources/Icons/polyphony.png')) 31 | self.monophonyicon = PhotoImage(file = os.path.join(parentdir,'View/Resources/Icons/monophony.png')) 32 | self.harmonyicon = PhotoImage(file = os.path.join(parentdir,'View/Resources/Icons/harmony.png')) 33 | self.rendericon = PhotoImage(file = os.path.join(parentdir,'View/Resources/Icons/render.png')) 34 | self.shuffleicon = PhotoImage(file = os.path.join(parentdir,'MAESTOSO.png')) 35 | 36 | ####### create all the necessary frames ####### 37 | 38 | self.parentframe = Frame(root,bg = "gray9", width = 1125, height = 70, border = 3, relief = SUNKEN) #the main frame 39 | self.parentframe.place(x = 315, y = 0) 40 | self.parentframe.pack_propagate(0) 41 | 42 | #################################################################################################################################################### 43 | 44 | class ThemeSelection: #creates a dropdown list for the category of theme selected 45 | def __init__(self,parent): 46 | #### creates a list of all display names from themes stored in main.theme_data ##### 47 | self.RHYTHM = [i.display_name for i in main.theme_data if i.category == 'RHYTHM'] 48 | self.HARMONY = [i.display_name for i in main.theme_data if i.category == 'HARMONY'] 49 | self.POLYPHONY = [i.display_name for i in main.theme_data if i.category == 'POLYPHONIC'] 50 | self.MONOPHONY = [i.display_name for i in main.theme_data if i.category == 'MONOPHONIC'] 51 | self.parent = parent 52 | self.textvar = StringVar() 53 | self.construct_combobox() 54 | 55 | def adjust_comboboxstyle(): 56 | self.combostyle = ttk.Style() 57 | self.combostyle.theme_create('combostyle', parent='alt', 58 | settings = {'TCombobox': 59 | {'configure': 60 | {'selectbackground': 'purple', 61 | 'fieldbackground': 'gray8', 62 | 'background': 'gray4', 63 | 'foreground': 'white' 64 | }}}) 65 | self.combostyle.theme_use('combostyle') 66 | 67 | try: 68 | adjust_comboboxstyle() 69 | except: 70 | pass # in case theme style already exists in the system 71 | 72 | #function to construct the combobox drop down menu 73 | def construct_combobox(self): 74 | self.theme_selector = Combo(self.parent,width = 30, textvariable = self.textvar, state = 'disabled') 75 | self.theme_selector.set('Select a theme category') 76 | self.theme_selector.place(x = 700, y = 22) 77 | self.theme_selector.bind("<>",self.get_selected_value) 78 | 79 | #update combobox with the values based on category selected 80 | def update_combobox(self,category): 81 | selections = {'RHYTHM':self.RHYTHM,'MONOPHONY':self.MONOPHONY,'POLYPHONY':self.POLYPHONY,'HARMONY':self.HARMONY} 82 | self.theme_selector.config(state = 'readonly') 83 | selected_category = selections.get(category) 84 | self.theme_selector['values'] = selected_category 85 | 86 | #retrieve the selected value 87 | def get_selected_value(self,event): 88 | selected = self.textvar.get() 89 | main.selected_animation = selected 90 | ####################################################################################################################################################### 91 | #initialize all buttons on top of the toolbar 92 | 93 | 94 | self.combo_themes = ThemeSelection(self.parentframe) #combobox object containing all theme names 95 | 96 | self.shufflebutton = BT(self.parentframe,image = self.shuffleicon, borderwidth = 0, height = 48, width = 48, borderless = TRUE, bg = "gray6", state = DISABLED) 97 | self.shufflebutton.pack(side = LEFT, padx = 30) 98 | 99 | self.rhythmbutton = BT(self.parentframe,image = self.rhythmicon, borderwidth = 0, height = 48, width = 48,borderless = TRUE, command = lambda:self.combo_themes.update_combobox('RHYTHM')) 100 | self.rhythmbutton.place(x = 400, y = 5.5) 101 | 102 | self.polyphonybutton = BT(self.parentframe,image = self.polyphonyicon, borderwidth = 0, height = 48, width = 48,borderless = TRUE,command = lambda:self.combo_themes.update_combobox('POLYPHONY')) 103 | self.polyphonybutton.place(x = 460, y = 5.5) 104 | 105 | self.monophonybutton= BT(self.parentframe,image = self.monophonyicon, borderwidth = 0, height = 48, width = 48,borderless = TRUE,command = lambda:self.combo_themes.update_combobox('MONOPHONY')) 106 | self.monophonybutton.place(x = 520, y = 5.5) 107 | 108 | self.harmonybutton = BT(self.parentframe,image = self.harmonyicon, borderwidth = 0, height = 48, width = 48,borderless = TRUE,command = lambda:self.combo_themes.update_combobox('HARMONY')) 109 | self.harmonybutton.place(x = 580, y = 5.5) 110 | 111 | self.renderbutton = BT(self.parentframe,image = self.rendericon, borderwidth = 0, height = 48, width = 48,borderless = TRUE, command = lambda:self.if_render_click(root,main,videowindow)) 112 | self.renderbutton.pack(side = RIGHT, padx = 20) 113 | 114 | 115 | ####################################################################################################################################################### 116 | 117 | def if_render_click(self,root,main,videowindow): #if the render button is clicked 118 | if not runexcept.check_typerror_render(main): #run the check_typerror func from runexcept.py 119 | return 120 | else: 121 | self.render_toplevel(root,main,videowindow) #else run the render_toplevel method 122 | 123 | 124 | def render_toplevel(self,root,main,videowindow): # creates the top level pop when render is clicked 125 | root_x = root.winfo_rootx() 126 | root_y = root.winfo_rooty() 127 | x_pos = root_x + 500 128 | y_pos = root_y + 200 129 | 130 | top = Toplevel(bg = 'gray9') 131 | top.geometry(f'500x300+{x_pos}+{y_pos}') 132 | top.resizable(0,0) 133 | top.title("Render Animation") 134 | top.grab_set() 135 | 136 | def start_render(preview): 137 | top.destroy() 138 | self.renderbutton.config(state = DISABLED) 139 | videowindow.while_render() 140 | 141 | try: 142 | multiprocessing.set_start_method('spawn') 143 | except: 144 | pass 145 | success = multiprocessing.Value('i',False) 146 | process = multiprocessing.Process(target = render_in_background,args = (success,main,preview)) #run the render in background function on a seperate process 147 | process.start() 148 | 149 | t1 = threading.Thread(target = videowindow.generate_loading_screen, args = (main,preview)) #thread the loading screen func 150 | t1.start() 151 | 152 | while True: 153 | root.update_idletasks() 154 | root.update() 155 | if success.value: 156 | break 157 | try: 158 | videowindow.display_video() 159 | messagebox.showinfo("RENDER SUCCESFUL","RENDER SUCCESFUL! Scroll through the frames to see your animation.") 160 | self.renderbutton.config(state = ACTIVE) 161 | except: 162 | messagebox.showerror("Render Error", "There was some issue running your animation. Please try using another theme. You can report the bug on github.com") 163 | self.renderbutton.config(state = ACTIVE) 164 | return 165 | 166 | ############################################################################################################################# 167 | #create the toplevel popup when render is clicked 168 | 169 | def select_time(*args): 170 | main.preview_time = self.clicked.get() #select preview time 171 | 172 | toptitleframe = Frame(top,height = 50, width = 500, bg = "gray6") 173 | toptitleframe.pack(side = TOP) 174 | toptitleframe.propagate(0) 175 | topcenterframe = Frame(top,height = 200, width = 500, bg = "gray9") 176 | topcenterframe.pack(side = TOP) 177 | topcenterframe.propagate(0) 178 | topbottomframe = Frame(top,height = 50, width = 500, bg = "gray6") 179 | topbottomframe.pack(side = BOTTOM) 180 | topbottomframe.propagate(0) 181 | 182 | top_title = Label(toptitleframe, text = "Select the Type of Render", bg = "gray6", fg = "white") 183 | top_title.pack(side = TOP) 184 | 185 | self.previewbutton = BT(topcenterframe, borderwidth = 0, height = 48, width = 80,borderless = TRUE, text = "Preview", bg = "gray6", fg = "white", relief = GROOVE, command = lambda: start_render(preview = True)) 186 | self.previewbutton.place(x = 160, y = 60) #the commands defer by the value of preview 187 | 188 | self.fullrenderbutton = BT(topcenterframe, borderwidth = 0, height = 48, width = 80,borderless = TRUE, text = "Full Render", bg = "gray6", fg = "white", relief = GROOVE,command = lambda: start_render(preview = False)) 189 | self.fullrenderbutton.place(x = 260, y = 60) 190 | 191 | preview_time_label = Label(topcenterframe,text = "Choose a preview duration (in seconds)", bg = "gray9", fg = "white") 192 | preview_time_label.pack(side = BOTTOM) 193 | 194 | 195 | self.clicked = IntVar() 196 | self.clicked.set(main.preview_time) 197 | 198 | self.items = [3,5,10,15,30,45,60] #all the preview values 199 | 200 | self.preview_time = Combo(topbottomframe,width = 30, textvariable = self.clicked) 201 | self.preview_time['values'] = self.items 202 | self.preview_time.bind("<>",select_time) 203 | self.preview_time.pack(side = TOP) 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | -------------------------------------------------------------------------------- /THEMES/VideoGameTheme.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import mplcyberpunk 3 | import matplotlib.image as image 4 | from utility import frame_rate_note 5 | from tracklist_functions import create_monophonic, find_pitch_range_monophonic, create_timecode_hash 6 | import matplotlib 7 | 8 | matplotlib.use("TkAgg") 9 | 10 | 11 | class VideoGameTheme: 12 | def __init__(self,tracklist): 13 | self.category = 'MONOPHONIC' 14 | self.display_name = 'Arcade Game' 15 | self.tracklist = tracklist 16 | 17 | self.grid_colour = ['Grid Colour','COLORBOX','#d600ff'] 18 | self.grid_width = ["Grid Width","ENTRY",0.3,(0,1),"int"] 19 | self.background_colour = ['Background Colour','COLORBOX',"black"] 20 | self.line_colour = ['Line Colour','COLORBOX','#00b8ff'] 21 | self.line_react = ['Line React','CHECKBOX',True] 22 | self.line_highlight = ['Line Highlight','COLORBOX','#FFFFFF'] 23 | self.box_colour = ['Box Colour','COLORBOX','#d600ff'] 24 | self.grid_line_style = ["Grid Line Style","DROPDOWN","solid",["solid","dashed","dotted","extra dotted"]] 25 | self.obstacle_line_style = ["Obstacle Line Style","DROPDOWN","solid",["solid","dashed","dotted","extra dotted"]] 26 | self.grid_on = ['Grid','CHECKBOX',True] 27 | 28 | self.customizable_elements = [self.grid_colour,self.grid_width,self.background_colour,self.line_colour,self.line_react,self.line_highlight,self.box_colour,self.grid_line_style,self.obstacle_line_style,self.grid_on] 29 | 30 | self.theme_description = ["Music is an art form flooding with information just waiting to be visualized in exciting ways. This theme is my attempt at visualizing something as simple as a single melody line and turning it into a run and dodge video game level. The algorithm automatically analyses the melodic line from the MIDI file and creates the animation for you. But, in case you aren't satisfied with the outcome you can always manually disable instruments from the instrument panel. Note that there are an extra 4 seconds added at the beginning of the Animation.","Created by Aman Trivedi as part of the Maestoso Project."] 31 | 32 | def ANIMATE(self,upd_project_tracklist,upd_PPQN,upd_fps,upd_output_dir,upd_preview,upd_preview_time): 33 | tracklist = self.tracklist 34 | project_tracklist = upd_project_tracklist 35 | 36 | PPQN = upd_PPQN 37 | polyphonic_list = create_timecode_hash(project_tracklist) 38 | monophonic_list = create_monophonic(polyphonic_list) 39 | pitch_range = find_pitch_range_monophonic(monophonic_list) 40 | fps = upd_fps 41 | output_dir = upd_output_dir 42 | preview = upd_preview 43 | preview_time = upd_preview_time 44 | dpi = 120 45 | aspect_ratio = (16,9) 46 | margin = 200 47 | yspace = 20 48 | measures = 1 49 | base_note_length = 4 #quarter note 50 | 51 | 52 | line_styles = {"solid":"solid","dotted":"dotted","dashed":"dashed","extra dotted":(0,(1,10))} 53 | 54 | grid_colour = self.grid_colour[2] 55 | grid_width = float(self.grid_width[2]) 56 | background_colour = self.background_colour[2] 57 | line_colour = self.line_colour[2] 58 | line_react = self.line_react[2] 59 | line_highlight = self.line_highlight[2] 60 | box_colour = self.box_colour[2] 61 | grid_line_style = line_styles.get(self.grid_line_style[2]) 62 | obstacle_line_style = line_styles.get(self.obstacle_line_style[2]) 63 | grid_on = self.grid_on[2] 64 | 65 | def pix_to_inch(size): 66 | one_pix = 1/dpi 67 | return one_pix * size 68 | 69 | 70 | def adjust_dimensions(pitch_range): 71 | height = (len(pitch_range) * yspace) + margin + int(margin/2) 72 | width = round((height * aspect_ratio[0])/aspect_ratio[1]) 73 | return (width,height) 74 | 75 | 76 | width = adjust_dimensions(pitch_range)[0] 77 | height = adjust_dimensions(pitch_range)[1] 78 | character_height = 80 79 | 80 | 81 | def box_location_x(note): 82 | if note.start_ticks == 0: 83 | x_pos = 0 + width 84 | return x_pos # if note onset is 0 ticks 85 | else: 86 | distance_length = note.start_ticks/PPQN #The total distance that occurs before the note 87 | nx = 1/distance_length 88 | x_pos = (width / (nx * (base_note_length * measures))) + width 89 | return x_pos 90 | 91 | def box_location_y(note): 92 | max_position = height - (margin/2) # location of highest note in pitchrange will always be at margin/2 93 | max_note = max(pitch_range) 94 | difference = max_note - note.pitch # find difference current note value and max note value to obtain it's position in acccordance to margin/2 95 | location = max_position - (difference * yspace) 96 | return location 97 | 98 | 99 | class VerticalLines: 100 | def __init__(self,x,y,start_time): 101 | self.x = x 102 | self.y = y 103 | self.start_time = start_time 104 | self.colour = line_colour 105 | 106 | 107 | class GraphAnimation: 108 | def __init__(self): 109 | plt.ioff() 110 | self.fig, self.ax = plt.subplots(figsize=(pix_to_inch(1280),pix_to_inch(720)),dpi=dpi) 111 | plt.xlim([0,width]) 112 | plt.ylim([0,height]) 113 | plt.grid(False) 114 | self.ax.set_xticklabels([]) 115 | self.ax.set_yticklabels([]) 116 | self.ax.axis('off') 117 | self.verticallines = [] 118 | self.image_number = 0 119 | self.boxlocationx = width/2 120 | self.gridxloc = [] 121 | self.gridyloc = [] 122 | self.fig.set_facecolor(background_colour) 123 | self.ax.set_facecolor(background_colour) 124 | 125 | for j in monophonic_list: 126 | self.verticallines.append(VerticalLines(box_location_x(j),box_location_y(j),j.start_time)) 127 | 128 | self.construct_gridlines() 129 | 130 | def construct_gridlines(self): 131 | x_end = self.verticallines[-1].x 132 | y_end = height 133 | x_interval = width/8 134 | y_interval = height/8 135 | x = 0 136 | y = 0 137 | while x < x_end: 138 | self.gridxloc.append(x) 139 | x += x_interval 140 | 141 | while y < y_end: 142 | self.gridyloc.append(y) 143 | y += y_interval 144 | 145 | 146 | def plot_gridlines(self): 147 | if grid_on == False: 148 | return 149 | for i in self.gridxloc: 150 | if i > width: 151 | break 152 | elif i < 0: 153 | continue 154 | else: 155 | plt.vlines(i,ymin = 0, ymax = height, color = grid_colour,linewidth = grid_width,linestyle = grid_line_style) 156 | for i in self.gridyloc: 157 | if i > height: 158 | break 159 | elif i < 0: 160 | continue 161 | else: 162 | plt.hlines(i,xmin = 0, xmax = self.verticallines[-1].x, color = grid_colour,linewidth = grid_width,linestyle = grid_line_style) 163 | 164 | 165 | def plot_graph(self,lst): 166 | for i in lst: 167 | if i.x > width: 168 | break 169 | elif i.x < 0: 170 | continue 171 | else: 172 | if i.x > self.boxlocationx and i.x < self.boxlocationx + 20: 173 | if line_react == True: 174 | i.colour = line_highlight # if user selects to highlight the lines 175 | else: 176 | i.colour = line_colour 177 | else: 178 | i.colour = line_colour 179 | plt.vlines(i.x, ymin=0, ymax=i.y, color = i.colour,linestyle = obstacle_line_style) 180 | plt.vlines(i.x, ymin=i.y + character_height, ymax=height, color = i.colour,linestyle = obstacle_line_style) 181 | 182 | 183 | def clear_plot(self): 184 | plt.cla() 185 | plt.xlim([0,width]) 186 | plt.ylim([0,height]) 187 | plt.grid(False) 188 | self.fig.set_facecolor(background_colour) 189 | self.ax.set_facecolor(background_colour) 190 | self.ax.set_xticklabels([]) 191 | self.ax.set_yticklabels([]) 192 | self.ax.axis('off') 193 | 194 | 195 | def clear_patches(self): 196 | self.ax.patches = [] 197 | 198 | 199 | def create_character(self): 200 | plt.gca().add_patch(plt.Rectangle((self.boxlocationx,height/2),20,20,linewidth = 1, edgecolor = box_colour,facecolor = box_colour)) 201 | 202 | 203 | def calculate_xspeed(self,f1,f2,startpos,endpos): #end pos = current position, start pos = destination 204 | distance = endpos - startpos 205 | diff = f2 - f1 206 | if diff != 0: 207 | speed = distance/diff 208 | else: 209 | speed = distance 210 | return speed 211 | 212 | def calculate_yspeed(self,f1,f2,endpos): 213 | distance = endpos - ((height/2) - 30) 214 | diff = f2 - f1 215 | if diff != 0: 216 | speed = distance/diff 217 | else: 218 | speed = distance 219 | return speed 220 | 221 | 222 | def construct_plot(self): 223 | self.create_character() 224 | self.plot_graph(self.verticallines) 225 | self.plot_gridlines() 226 | plt.savefig(f'{output_dir}/frame_{self.image_number}.png') 227 | self.clear_plot() 228 | self.clear_patches() 229 | self.image_number += 1 230 | 231 | def adjust_xy(self,lst,x,y): 232 | for i in lst: 233 | i.x -= x 234 | i.y -= y 235 | 236 | for i in range(len(self.gridxloc)): 237 | self.gridxloc[i] = self.gridxloc[i] - x 238 | 239 | for i in range(len(self.gridyloc)): 240 | self.gridyloc[i] = self.gridyloc[i] - y 241 | 242 | 243 | def entry_animation(self,startposx,endposy): 244 | frame = 0 245 | time = 4 * fps + frame_rate_note(self.verticallines[0].start_time,fps) 246 | xspeed = self.calculate_xspeed(0,time,self.boxlocationx,self.verticallines[0].x) 247 | yspeed = self.calculate_yspeed(0,time,endposy) 248 | while frame < time: 249 | self.construct_plot() 250 | self.adjust_xy(self.verticallines,xspeed,yspeed) 251 | frame += 1 252 | 253 | 254 | def animate_theme(self): 255 | self.entry_animation(self.verticallines[0].x,self.verticallines[0].y) 256 | previous_time = self.verticallines[0].start_time 257 | time_list = [] 258 | for i in self.verticallines: 259 | time_list.append(i.start_time) 260 | 261 | 262 | for i in time_list: 263 | if i == self.verticallines[0].start_time: 264 | continue 265 | frames = 0 266 | f1 = frame_rate_note(previous_time,fps) 267 | f2 = frame_rate_note(i,fps) 268 | note = self.verticallines[time_list.index(i)] 269 | xspeed = self.calculate_xspeed(f1,f2,self.boxlocationx,note.x) 270 | yspeed = self.calculate_yspeed(f1,f2,note.y) 271 | while frames < f2 - f1: 272 | self.construct_plot() 273 | self.adjust_xy(self.verticallines,xspeed,yspeed) 274 | frames += 1 275 | 276 | previous_time = i 277 | if preview == True and previous_time > preview_time: 278 | return 279 | 280 | Graph = GraphAnimation() 281 | Graph.animate_theme() 282 | -------------------------------------------------------------------------------- /THEMES/NecklaceTheme.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | from utility import frame_rate_note, pick_random_colour 3 | from tracklist_functions import create_drum_hash, find_all_drums 4 | from math import pi, cos, sin 5 | import matplotlib 6 | from pretty_midi import note_number_to_drum_name 7 | 8 | matplotlib.use("TkAgg") 9 | 10 | class NecklaceTheme: 11 | def __init__(self,tracklist): 12 | self.category = 'RHYTHM' 13 | self.display_name = 'Toussaint' 14 | self.tracklist = tracklist 15 | self.background_color = ["Background Colour","COLORBOX","black"] 16 | self.bass_effect = ["Bass Effect","DROPDOWN","FUNKY",["OFF","FUNKY","A LITTLE CRAZY!","SUPA-CRAZY!!"]] 17 | self.beads_color = ["Beads Colour","COLORBOX","#ffffff"] 18 | self.pulse_sensitivity = ["Accent Sensitivity","DROPDOWN","Moderate",["Off","Moderate","Intense","You must be kidding me!"]] 19 | 20 | self.customizable_elements = [self.background_color,self.bass_effect,self.beads_color,self.pulse_sensitivity] 21 | 22 | for i in find_all_drums(tracklist.track_list, name = True): 23 | self.customizable_elements.append([f"{i}","COLORBOX",f"{pick_random_colour()}","iterated"]) 24 | 25 | self.theme_description = ["Inspired by the works of musicologist Godfried Toussaint, this theme offers a cyclic analysis of rhythm sections. The only drawback to using this theme is that it doesn't support changes in time signature.","Created by Aman Trivedi as part of the Maestoso Project."] 26 | 27 | 28 | def ANIMATE(self,upd_project_tracklist,upd_PPQN,upd_fps,upd_output_dir,upd_preview,upd_preview_time): 29 | tracklist = self.tracklist 30 | project_tracklist = upd_project_tracklist 31 | PPQN = upd_PPQN 32 | fps = upd_fps 33 | output_dir = upd_output_dir 34 | preview = upd_preview 35 | preview_time = upd_preview_time 36 | dpi = 120 37 | aspect_ratio = (16,9) 38 | height = 720 39 | width = 1280 40 | 41 | bass_effect_dir = {"FUNKY":0.98,"A LITTLE CRAZY!":0.90,"SUPA-CRAZY!!":0.80,"OFF":False} 42 | accent_sens_dir = {"Moderate":0.02,"Intense":0.05,"You must be kidding me!":0.08,"OFF":False} 43 | 44 | background_color = self.background_color[2] 45 | bass_effect = bass_effect_dir.get(self.bass_effect[2]) 46 | beads_color = self.beads_color[2] 47 | pulse_sensitivity = accent_sens_dir.get(self.pulse_sensitivity[2]) 48 | additional_colours = [] 49 | 50 | for i in self.customizable_elements: 51 | try: 52 | if i[3] == "iterated": 53 | additional_colours.append((i[0],i[2])) 54 | except: 55 | continue 56 | 57 | rhythm_tracklist = create_drum_hash(project_tracklist) 58 | 59 | name_list = find_all_drums(project_tracklist,name=False) 60 | name_list.sort() 61 | 62 | if len(tracklist.ts_changes) == 1: 63 | pass 64 | else: 65 | messagebox.showerror("Theme Error"," Multiple instances of time signature changes have been found. This theme doesn't support changes in time signature.") 66 | return 67 | 68 | numerator = tracklist.ts_changes[0].numerator 69 | denominator = tracklist.ts_changes[0].denominator 70 | measure_duration = (4/denominator) * numerator 71 | 72 | pulse_array = [frame_rate_note(i,fps) for i in tracklist.pulse] #all the pulses of the track converted to frames 73 | accent_array = [frame_rate_note(i,fps) for i in tracklist.accented_beats] 74 | 75 | 76 | def pix_to_inch(size): 77 | one_pix = 1/dpi 78 | return one_pix * size 79 | 80 | 81 | class GraphAnimation: 82 | def __init__(self): 83 | plt.ioff() 84 | self.fig, self.ax = plt.subplots(figsize=(pix_to_inch(1280),pix_to_inch(720)),dpi=dpi) 85 | plt.xlim([0,width]) 86 | plt.ylim([0,height]) 87 | plt.grid(False) 88 | self.ax.set_xticklabels([]) 89 | self.ax.set_yticklabels([]) 90 | self.ax.axis('off') 91 | self.fig.set_facecolor(background_color) 92 | self.ax.set_facecolor(background_color) 93 | 94 | def clear_patches(self): 95 | self.ax.patches = [] 96 | 97 | 98 | class Polygon: 99 | def __init__(self,radius,colour,pitch): 100 | self.pitch = pitch 101 | self.radius = radius 102 | self.colour = colour 103 | self.x = width/2 104 | self.y = height/2 105 | self.inner_radius = self.find_inner_circle() 106 | self.frame = 0 107 | self.animating = False 108 | self.pulse_size = 0 109 | 110 | def find_inner_circle(self): 111 | space = self.radius - (self.radius * 0.80) 112 | inner_radius = self.radius - (space/2) 113 | return inner_radius 114 | 115 | def draw_polygon(self): 116 | circle = plt.Circle((self.x, self.y), self.radius, color=self.colour) 117 | return circle 118 | 119 | 120 | def animate_polygon(self,image_number): 121 | if bass_effect == False: 122 | return 123 | shrink = bass_effect 124 | expand = 1 - shrink 125 | for i in pulse_array: 126 | if i == image_number: 127 | try: 128 | self.pulse_size = pulse_array[pulse_array.index(i)+1] - i #in fps 129 | except: 130 | self.pulse_size = round(fps * 0.6) 131 | 132 | if pulse_sensitivity != False: 133 | for j in accent_array: 134 | if j == i: 135 | shrink -= pulse_sensitivity 136 | 137 | self.frame = i 138 | self.animating = True 139 | self.shrink_rate = (self.radius - (self.radius * shrink))/round(self.pulse_size/2) 140 | self.expand_rate = (self.radius - (self.radius * shrink))/(self.pulse_size - round(self.pulse_size/2)) 141 | self.shrink_time = self.frame + round(self.pulse_size/2) 142 | self.expand_time = (self.frame + self.pulse_size) 143 | break 144 | 145 | if self.animating == True: 146 | 147 | if self.frame < self.shrink_time: 148 | self.radius -= self.shrink_rate 149 | self.inner_radius -= self.shrink_rate 150 | self.frame += 1 151 | 152 | elif self.frame >= self.shrink_time and self.frame < self.expand_time: 153 | self.radius += self.expand_rate 154 | self.inner_radius += self.expand_rate 155 | self.frame += 1 156 | 157 | elif self.frame >= self.frame + self.pulse_size: 158 | self.animating = False 159 | 160 | 161 | def create_polygon_list(): 162 | current_radius = width/4 163 | polygon_list = [] 164 | for i in name_list: 165 | drum_name = note_number_to_drum_name(i) 166 | for j in additional_colours: 167 | if j[0] == drum_name: 168 | inst_colour = j[1] 169 | polygon_list.append(Polygon(current_radius,inst_colour,i)) 170 | current_radius = current_radius * 0.80 171 | 172 | current_radius = current_radius * 0.80 173 | polygon_list.append(Polygon(current_radius,background_color,'NONE')) 174 | 175 | return polygon_list 176 | 177 | polygon_list = create_polygon_list() 178 | 179 | class Point: 180 | def __init__(self,polygon_radius,inner_radius,angle,pitch): 181 | self.pitch = pitch 182 | self.polygon_radius = polygon_radius 183 | self.space = (self.polygon_radius) - (self.polygon_radius * 0.80) 184 | self.radius = self.space/4 185 | self.origin = (width/2,height/2) 186 | self.inner_radius = inner_radius 187 | self.x = (self.inner_radius * sin(angle)) + self.origin[0] 188 | self.y = (self.inner_radius * cos(angle)) + self.origin[1] 189 | self.colour = beads_color 190 | self.angle = angle 191 | 192 | def draw_point(self): 193 | circle = plt.Circle((self.x, self.y), self.radius, color=self.colour) 194 | return circle 195 | 196 | def change_opacity(self): 197 | if self.colour == '#ffffff': 198 | self.colour += '80' 199 | 200 | def readjust(self): 201 | self.space = (self.polygon_radius) - (self.polygon_radius * 0.80) 202 | self.radius = self.space/4 203 | self.x = (self.inner_radius * sin(self.angle)) + self.origin[0] 204 | self.y = (self.inner_radius * cos(self.angle)) + self.origin[1] 205 | 206 | 207 | def draw_polygons(): 208 | for i in polygon_list: 209 | plt.gca().add_patch(i.draw_polygon()) 210 | 211 | def display_points(lst): 212 | for i in lst: 213 | plt.gca().add_patch(i.draw_point()) 214 | 215 | 216 | 217 | 218 | 219 | def animate(): 220 | graph = GraphAnimation() 221 | image_number = 0 222 | 223 | time_elapsed = 0 #variable to measure, measure based duration 224 | time_elapsed_seconds = 0 # variable to measure frame duration 225 | previous_tick = 0 226 | keys = list(rhythm_tracklist) 227 | points = [] 228 | 229 | def create_points_list(v,polygon_list,angle): 230 | for j in v: 231 | for i in polygon_list: 232 | if i.pitch == j.pitch: 233 | polygon_radius = i.radius 234 | inner_radius = i.inner_radius 235 | points.append(Point(polygon_radius,inner_radius,angle,i.pitch)) 236 | 237 | 238 | def adjust_points_list(): 239 | for i in points: 240 | for j in polygon_list: 241 | if i.pitch == j.pitch: 242 | i.polygon_radius = j.radius 243 | i.inner_radius = j.inner_radius 244 | i.readjust() 245 | 246 | for k, v in rhythm_tracklist.items(): 247 | time_elapsed += (v[0].start_ticks - previous_tick)/PPQN 248 | previous_tick = v[0].start_ticks 249 | if time_elapsed >= measure_duration: # if cycle is complete 250 | time_elapsed -= measure_duration 251 | graph.clear_patches() 252 | points.clear() 253 | 254 | angle = ((time_elapsed/measure_duration) * 360) * pi/180 # set value of angle 255 | 256 | 257 | create_points_list(v,polygon_list,angle) 258 | 259 | 260 | frame = 0 261 | current_frame = frame_rate_note(k,fps) 262 | 263 | if k != keys[-1]: 264 | next_frame = frame_rate_note(keys[keys.index(k) + 1],fps) 265 | elif k == keys[-1]: 266 | next_frame = frame_rate_note(5,fps) 267 | 268 | 269 | duration = next_frame - current_frame 270 | 271 | 272 | if k == keys[0] and k!= 0: 273 | temp_frame = 0 274 | draw_polygons() 275 | while temp_frame < current_frame: 276 | plt.savefig(f'{output_dir}/frame_{image_number}.png') 277 | image_number += 1 278 | temp_frame += 1 279 | else: 280 | draw_polygons() 281 | display_points(points) 282 | while frame < duration: 283 | plt.savefig(f'{output_dir}/frame_{image_number}.png') 284 | for i in polygon_list: 285 | i.animate_polygon(image_number) 286 | graph.clear_patches() 287 | draw_polygons() 288 | adjust_points_list() 289 | display_points(points) 290 | image_number += 1 291 | frame += 1 292 | 293 | 294 | time_elapsed_seconds = k 295 | 296 | for i in points: 297 | i.change_opacity() 298 | graph.clear_patches() 299 | 300 | 301 | if preview == True and time_elapsed_seconds > preview_time: 302 | return 303 | animate() 304 | 305 | 306 | 307 | 308 | 309 | 310 | -------------------------------------------------------------------------------- /THEMES/CyberPunkTheme.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | currentdir = os.path.dirname(os.path.realpath(__file__)) 3 | parentdir = os.path.dirname(currentdir) 4 | sys.path.append(parentdir) #import modules from parent folder 5 | import matplotlib.pyplot as plt 6 | import matplotlib.ticker as plticker 7 | import mplcyberpunk 8 | from utility import frame_rate_note, frames_to_second 9 | from tracklist_functions import find_pitch_range, create_timecode_hash 10 | import matplotlib 11 | 12 | matplotlib.use("TkAgg") 13 | 14 | class CyberPunkTheme: 15 | def __init__(self,tracklist): 16 | self.category = 'POLYPHONIC' 17 | self.display_name = 'CyberPunk' 18 | self.tracklist = tracklist 19 | self.grid_on = ['Grid','CHECKBOX',True] 20 | self.grid_width = ['Grid Width','ENTRY',0.2,(0,10),"int"] 21 | self.background_colour = ['Background Colour','COLORBOX','blue'] 22 | self.underglow_effect = ['Underglow Effect','CHECKBOX',True] 23 | self.neon_effect = ['Neon Effect','CHECKBOX',True] 24 | self.grid_colour = ['Grid Colour','COLORBOX','blue'] 25 | self.camera_speed = ['Camera Speed','ENTRY',1,(1,10),"int"] 26 | self.view_size = ['Measures','ENTRY',4,(1,50),"int"] 27 | self.meter_grid = ["Grid Meter Display","CHECKBOX",True] 28 | 29 | self.customizable_elements = [self.grid_on,self.background_colour,self.underglow_effect,self.neon_effect,self.grid_colour,self.camera_speed,self.view_size,self.grid_width,self.meter_grid] 30 | self.theme_description = ['The most lit theme in the software - literally. This cyberpunk styled theme features a host of styling options including but not limited to neon like glows and underglows. This theme is an answer to the question: "what if cartesian geometry was a night club"?','Created by Aman Trivedi as part of the Maestoso Project.'] 31 | 32 | def ANIMATE(self,upd_project_tracklist,upd_PPQN,upd_fps,upd_output_dir,upd_preview,upd_preview_time): 33 | 34 | tracklist = self.tracklist 35 | project_tracklist = upd_project_tracklist 36 | 37 | pitch_range = find_pitch_range(project_tracklist) 38 | 39 | PPQN = upd_PPQN 40 | polyphonic_list = create_timecode_hash(project_tracklist) 41 | fps = upd_fps 42 | output_dir = upd_output_dir 43 | preview = upd_preview 44 | preview_time = upd_preview_time 45 | 46 | grid_on = self.grid_on[2] 47 | background_colour = self.background_colour[2] 48 | underglow_effect = self.underglow_effect[2] 49 | neon_effect = self.neon_effect[2] 50 | grid_colour = self.grid_colour[2] 51 | camera_speed = float(self.camera_speed[2]) 52 | view_size = float(self.view_size[2]) 53 | grid_width = float(self.grid_width[2]) 54 | grid_meter = self.meter_grid[2] 55 | 56 | ''' CONSTRUCTING POLYPHONIC ANIMATION ''' 57 | aspect_ratio = (16,9) 58 | dpi = 120 59 | 60 | def adjust_dimensions(pitch_range): 61 | """[ADJUSTS THE DIMENSIONS OF ALL FRAMES IN THE POLYPHONIC ANIMATION] 62 | 63 | Args: 64 | pitch_range ([list]): [function within init_project.py] 65 | 66 | Returns: 67 | [tuple]: [width and height] 68 | """ 69 | height = (chosen_note_height * len(pitch_range)) + margin + int(margin/2) 70 | width = round((height * aspect_ratio[0])/aspect_ratio[1]) 71 | return (width,height) 72 | 73 | 74 | measures = view_size # The number of measures to be displayed at any time in one camera frame 75 | chosen_note_height = 50 #height of every note 76 | margin = 200 # space left 100 above and 100 below aesthetic purposes 77 | width = adjust_dimensions(pitch_range)[0] 78 | height = adjust_dimensions(pitch_range)[1] 79 | 80 | base_pulse_size = width/(tracklist.ts_changes[0].numerator * measures) #pulse size 81 | base_note_size = (tracklist.ts_changes[0].denominator/4) * base_pulse_size #quarter note size 82 | 83 | def box_location_x(note): 84 | """[LOCATION OF THE BLOCK ALONG THE X-AXIS IN ACCORDANCE WITH THE NOTE ONSET ] 85 | 86 | Args: 87 | note ([Note class]): [initialized in init_tracklist.py] 88 | 89 | Returns: 90 | [float]: [location along the x-axis] 91 | """ 92 | if note.start_ticks == 0: 93 | x_pos = 0 + width/2 94 | return x_pos # if note onset is 0 ticks 95 | else: 96 | distance_length = note.start_ticks/PPQN #The total distance that occurs before the note 97 | nx = (4/tracklist.ts_changes[0].denominator)/distance_length 98 | x_pos = (width / (nx * (tracklist.ts_changes[0].numerator * measures))) + width/2 99 | return x_pos 100 | 101 | def box_location_y(note): 102 | """[LOCATION OF THE BLOCK ALONG THE Y-AXIS IN ACCORDANCE WITH PITCH RANGE] 103 | 104 | Args: 105 | note ([Note class]): [initialized in init_tracklist.py] 106 | 107 | Returns: 108 | [float]: [location along the y-axis] 109 | """ 110 | max_position = height - (margin/2) # location of highest note in pitchrange will always be at margin/2 111 | max_note = max(pitch_range) 112 | difference = max_note - note.pitch # find difference current note value and max note value to obtain it's position in acccordance to margin/2 113 | location = max_position - (difference * chosen_note_height) 114 | return location 115 | 116 | def pix_to_inch(size): 117 | one_pix = 1/dpi 118 | return one_pix * size 119 | 120 | class GraphAnimation: 121 | def __init__(self): 122 | plt.ioff() 123 | self.fig, self.ax = plt.subplots(figsize=(pix_to_inch(1280),pix_to_inch(720)),dpi=dpi) 124 | plt.xlim([0,width]) 125 | plt.ylim([0,height]) 126 | if grid_on == True: 127 | if grid_meter == True: 128 | grd_space = plticker.MultipleLocator(base = base_pulse_size) 129 | self.ax.yaxis.set_major_locator(grd_space) 130 | self.ax.xaxis.set_major_locator(grd_space) 131 | plt.grid(which = 'major', color = grid_colour,linewidth = grid_width) 132 | self.ax.spines['right'].set_visible(False) 133 | self.ax.spines['left'].set_visible(False) 134 | self.ax.spines['top'].set_visible(False) 135 | self.ax.spines['bottom'].set_visible(False) 136 | else: 137 | plt.grid(False) 138 | self.ax.axis('off') 139 | self.ax.set_xticklabels([]) 140 | self.ax.set_yticklabels([]) 141 | self.fig.patch.set_facecolor(background_colour) 142 | self.ax.patch.set_facecolor(background_colour) 143 | 144 | def clear_plot(self): 145 | plt.cla() 146 | plt.xlim([0,width]) 147 | plt.ylim([0,height]) 148 | if grid_on == True: 149 | if grid_meter == True: 150 | grd_space = plticker.MultipleLocator(base = base_pulse_size) 151 | self.ax.yaxis.set_major_locator(grd_space) 152 | self.ax.xaxis.set_major_locator(grd_space) 153 | plt.grid(which = 'major', color = grid_colour,linewidth = grid_width) 154 | self.ax.spines['right'].set_visible(False) 155 | self.ax.spines['left'].set_visible(False) 156 | self.ax.spines['top'].set_visible(False) 157 | self.ax.spines['bottom'].set_visible(False) 158 | elif grid_on == False: 159 | plt.grid(False) 160 | self.ax.axis('off') 161 | self.ax.set_xticklabels([]) 162 | self.ax.set_yticklabels([]) 163 | self.fig.patch.set_facecolor(background_colour) 164 | self.ax.patch.set_facecolor(background_colour) 165 | 166 | 167 | class Camera: 168 | def __init__(self): 169 | self.quarter_length = base_note_size 170 | self.tempo_changes = self.construct_tempo_map() 171 | self.current_tempo = self.tempo_changes.get(0.0) 172 | self.camera_movement = self.calculate_camera_movement(0.0) 173 | 174 | def construct_tempo_map(self): 175 | tempo_changes = {} 176 | for i in range(len(tracklist.tempo_changes[0])): 177 | tempo_changes.update({frame_rate_note(tracklist.tempo_changes[0][i],fps):tracklist.tempo_changes[1][i]}) 178 | return tempo_changes 179 | 180 | 181 | def calculate_camera_movement(self,frame_number): 182 | try: 183 | self.current_tempo = self.tempo_changes.get(frame_number) 184 | nqps = self.current_tempo/60 185 | self.camera_movement = (self.quarter_length * nqps)/fps 186 | return self.camera_movement 187 | except: 188 | return self.camera_movement 189 | 190 | def destroy_block(lst): 191 | for i in lst: 192 | if i.x < 0: 193 | lst.remove(i) 194 | 195 | def move_camera(lst,camera): 196 | for i in lst: 197 | for j in range(len(i.x)): 198 | i.x[j] = i.x[j] - (camera * camera_speed) 199 | 200 | def delete_items(lst): 201 | for i in lst: 202 | for j in i.x: 203 | if j < 0: 204 | i.y.remove(i.y[i.x.index(j)]) 205 | i.x.remove(j) 206 | elif j > 0: 207 | break 208 | 209 | 210 | class LineGraph: 211 | def __init__(self,k,colour): 212 | self.specialcode = k 213 | self.x = [] 214 | self.y = [] 215 | self.colour = colour 216 | 217 | 218 | def create_plot(lst): 219 | for i in lst: 220 | plt.plot(i.x,i.y, color = i.colour, marker = 'o') 221 | 222 | 223 | if neon_effect == True: 224 | mplcyberpunk.make_lines_glow() 225 | if underglow_effect == True: 226 | mplcyberpunk.add_underglow() 227 | 228 | class CyberPunkThemeAnimation: 229 | def __init__(self,output_dir): 230 | self.time_elapsed = 0 231 | self.image_number = 0 232 | self.temp_list = [] 233 | self.camera = Camera() 234 | self.output_dir = output_dir 235 | self.initialize_templist() 236 | 237 | 238 | def initialize_templist(self): 239 | for i in project_tracklist: 240 | self.temp_list.append(LineGraph(i.specialcode,i.colour)) 241 | 242 | def create_frames(self,k): 243 | create_plot(self.temp_list) 244 | plt.savefig(f'{self.output_dir}/frame_{self.image_number}.png') 245 | self.image_number += 1 246 | camera_movement = self.camera.calculate_camera_movement(self.image_number) 247 | move_camera(self.temp_list,camera_movement) 248 | 249 | 250 | def add_elements_to_temp(self,v): 251 | for i in v: 252 | for j in self.temp_list: 253 | if i.specialcode == j.specialcode: 254 | j.x.append(box_location_x(i)) 255 | j.y.append(box_location_y(i)) 256 | 257 | 258 | def animate_theme(self): 259 | graph = GraphAnimation() 260 | plt.ioff() 261 | keys = list(polyphonic_list) 262 | for k, v in polyphonic_list.items(): 263 | difference = k - self.time_elapsed 264 | self.time_elapsed = k 265 | if difference == 0: 266 | self.add_elements_to_temp(v) 267 | self.create_frames(k) 268 | graph.clear_plot() 269 | else: 270 | frame = 0 271 | current_onset = k 272 | previous_onset = k - difference 273 | while frame < frame_rate_note(current_onset,fps) - frame_rate_note(previous_onset,fps): 274 | self.create_frames(k) 275 | frame += 1 276 | graph.clear_plot() 277 | self.add_elements_to_temp(v) 278 | delete_items(self.temp_list) 279 | 280 | if k == keys[-1]: # if last note of the sequence 281 | frame = 0 282 | while frame < frame_rate_note(5,fps): 283 | self.create_frames(k) 284 | frame += 1 285 | graph.clear_plot() 286 | self.time_elapsed += 5 287 | 288 | 289 | 290 | if preview == True and self.time_elapsed > 45: 291 | return 292 | 293 | theme = CyberPunkThemeAnimation(output_dir) 294 | theme.animate_theme() -------------------------------------------------------------------------------- /THEMES/BasicTheme.py: -------------------------------------------------------------------------------- 1 | '''BASIC THEME - ALSO KNOWN AS CLASSIC THEME''' 2 | import os, sys 3 | currentdir = os.path.dirname(os.path.realpath(__file__)) 4 | parentdir = os.path.dirname(currentdir) 5 | sys.path.append(parentdir) #import modules from parent folder 6 | import matplotlib.pyplot as plt 7 | import matplotlib.ticker as plticker 8 | import matplotlib 9 | from utility import frame_rate_note, frames_to_second 10 | from tracklist_functions import create_timecode_hash, find_pitch_range 11 | 12 | matplotlib.use('TkAgg') 13 | 14 | class BasicTheme: 15 | def __init__(self,tracklist): 16 | self.category = 'POLYPHONIC' 17 | self.display_name = 'Classic' 18 | self.tracklist = tracklist 19 | 20 | self.grid_on = ['Grid','CHECKBOX',True] 21 | self.background_colour = ['Background Colour','COLORBOX','blue'] 22 | self.grid_colour = ['Grid Color','COLORBOX',"white"] 23 | self.grid_width = ['Grid Width','ENTRY',0.2,(0,10),"int"] # Entry widgets take 4 arguments...[3][0] describes the starting limit of the entry..(if none, use '') at 3[1] max lmit entry...if type == str, only define max limit in [3],[4] describes the type.. if it's an int, the limit is defined by max number or tuple describing the range and if its a str, the max range is defined by character limit. 24 | self.camera_speed = ["Camera Speed","ENTRY",2,(1,10),"int"] 25 | self.view_size = ['Measures','ENTRY',30,(1,30),"int"] 26 | self.note_opacity = ["Note Opacity","CHECKBOX",True] 27 | self.meter_grid = ["Grid Meter Display","CHECKBOX",True] 28 | self.customizable_elements = [self.grid_on,self.background_colour,self.grid_colour,self.grid_width,self.camera_speed,self.view_size,self.note_opacity,self.meter_grid] # always use the following attribute name and type 29 | self.theme_description = ["The classic piano roll styled theme inspired by the works of Stephen Malinowski. As they say: when in doubt, go with the classics - and there's nothing more classic than a piano roll theme.","Created by Aman Trivedi as part of the Maestoso Project."] 30 | 31 | def ANIMATE(self,upd_project_tracklist,upd_PPQN,upd_fps,upd_output_dir,upd_preview,upd_preview_time): 32 | tracklist = self.tracklist 33 | project_tracklist = upd_project_tracklist 34 | 35 | 36 | pitch_range = find_pitch_range(project_tracklist) 37 | 38 | 39 | PPQN = upd_PPQN 40 | polyphonic_list = create_timecode_hash(project_tracklist) 41 | fps = upd_fps 42 | output_dir = upd_output_dir 43 | preview = upd_preview 44 | preview_time = upd_preview_time 45 | 46 | grid_on = self.grid_on[2] 47 | background_colour = self.background_colour[2] 48 | grid_colour = self.grid_colour[2] 49 | grid_width = self.grid_width[2] 50 | camera_speed = float(self.camera_speed[2]) 51 | view_size = float(self.view_size[2]) 52 | note_opacity = self.note_opacity[2] 53 | grid_meter = self.meter_grid[2] 54 | 55 | 56 | ''' CONSTRUCTING POLYPHONIC ANIMATION ''' 57 | aspect_ratio = (16,9) 58 | dpi = 120 59 | 60 | def adjust_dimensions(pitch_range): 61 | """[ADJUSTS THE DIMENSIONS OF ALL FRAMES IN THE POLYPHONIC ANIMATION] 62 | 63 | Args: 64 | pitch_range ([list]): [function within init_project.py] 65 | 66 | Returns: 67 | [tuple]: [width and height] 68 | """ 69 | height = (chosen_note_height * len(pitch_range)) + margin + int(margin/2) 70 | width = round((height * aspect_ratio[0])/aspect_ratio[1]) 71 | return (width,height) 72 | 73 | margin = 200 # space left 100 above and 100 below aesthetic purposes 74 | chosen_note_height = 50 #height of every note 75 | width = adjust_dimensions(pitch_range)[0] 76 | height = adjust_dimensions(pitch_range)[1] 77 | measures = view_size # The number of measures to be displayed at any time in one camera frame 78 | base_pulse_size = width/(tracklist.ts_changes[0].numerator * measures) #pulse size 79 | base_note_size = (tracklist.ts_changes[0].denominator/4) * base_pulse_size #quarter note size 80 | 81 | def box_length(note): 82 | """[OBTAIN THE LENGTH OF BLOCKS BASED ON NOTE DURATION] 83 | 84 | Args: 85 | note ([Note class]): [initialized in init_tracklist.py] 86 | 87 | Returns: 88 | [float]: [length of the block] 89 | """ 90 | nx = ((4/tracklist.ts_changes[0].denominator)/note.note_length) # calculates note ratio against quarter note length -- quarter = 1 (PPQN/PPQN) 91 | box_length = width / (nx * (tracklist.ts_changes[0].numerator * measures)) 92 | return box_length 93 | 94 | def box_location_x(note): 95 | """[LOCATION OF THE BLOCK ALONG THE X-AXIS IN ACCORDANCE WITH THE NOTE ONSET ] 96 | 97 | Args: 98 | note ([Note class]): [initialized in init_tracklist.py] 99 | 100 | Returns: 101 | [float]: [location along the x-axis] 102 | """ 103 | if note.start_ticks == 0: 104 | x_pos = 0 + width/2 105 | return x_pos # if note onset is 0 ticks 106 | else: 107 | distance_length = note.start_ticks/PPQN #The total distance that occurs before the note 108 | nx = (4/tracklist.ts_changes[0].denominator)/distance_length 109 | x_pos = (width / (nx * (tracklist.ts_changes[0].numerator * measures))) + width/2 110 | return x_pos 111 | 112 | def box_location_y(note): 113 | """[LOCATION OF THE BLOCK ALONG THE Y-AXIS IN ACCORDANCE WITH PITCH RANGE] 114 | 115 | Args: 116 | note ([Note class]): [initialized in init_tracklist.py] 117 | 118 | Returns: 119 | [float]: [location along the y-axis] 120 | """ 121 | max_position = height - (margin/2) # location of highest note in pitchrange will always be at margin/2 122 | max_note = max(pitch_range) 123 | difference = max_note - note.pitch # find difference current note value and max note value to obtain it's position in acccordance to margin/2 124 | location = max_position - (difference * chosen_note_height) 125 | return location 126 | 127 | def pix_to_inch(size): #function to convert pixels to inch 128 | one_pix = 1/dpi 129 | return one_pix * size 130 | 131 | class GraphAnimation: #initialize graph 132 | def __init__(self): 133 | plt.ioff() 134 | self.fig, self.ax = plt.subplots(figsize=(pix_to_inch(1280),pix_to_inch(720)),dpi=dpi) 135 | plt.xlim([0,width]) 136 | plt.ylim([0,height]) 137 | if grid_on == True: 138 | if grid_meter == True: 139 | grd_space = plticker.MultipleLocator(base = base_pulse_size) 140 | self.ax.yaxis.set_major_locator(grd_space) 141 | self.ax.xaxis.set_major_locator(grd_space) 142 | plt.grid(which = 'major', color = grid_colour,linewidth = grid_width) 143 | self.ax.spines['right'].set_visible(False) 144 | self.ax.spines['left'].set_visible(False) 145 | self.ax.spines['top'].set_visible(False) 146 | self.ax.spines['bottom'].set_visible(False) 147 | else: 148 | plt.grid(False) 149 | self.ax.axis('off') 150 | self.ax.set_xticklabels([]) 151 | self.ax.set_yticklabels([]) 152 | self.fig.patch.set_facecolor(background_colour) 153 | self.ax.patch.set_facecolor(background_colour) 154 | 155 | 156 | def clear_patches(self): 157 | self.ax.patches = [] 158 | 159 | 160 | class Camera: 161 | def __init__(self): 162 | self.quarter_length = base_note_size 163 | self.tempo_changes = self.construct_tempo_map() 164 | self.current_tempo = self.tempo_changes.get(0.0) 165 | self.camera_movement = self.calculate_camera_movement(0.0) 166 | 167 | def construct_tempo_map(self): 168 | tempo_changes = {} 169 | for i in range(len(tracklist.tempo_changes[0])): 170 | tempo_changes.update({frame_rate_note(tracklist.tempo_changes[0][i],fps):tracklist.tempo_changes[1][i]}) 171 | return tempo_changes 172 | 173 | 174 | def calculate_camera_movement(self,frame_number): 175 | try: 176 | self.current_tempo = self.tempo_changes.get(frame_number) 177 | nqps = self.current_tempo/60 178 | self.camera_movement = (self.quarter_length * nqps)/fps 179 | return self.camera_movement 180 | except: 181 | return self.camera_movement 182 | 183 | 184 | 185 | class Block: 186 | def __init__(self,note): 187 | self.note = note 188 | self.height = chosen_note_height 189 | self.width = box_length(self.note) 190 | self.x = box_location_x(self.note) 191 | self.y = box_location_y(self.note) 192 | self.colour = self.note.colour 193 | self.duration = self.note.return_duration() 194 | self.start_time = self.note.start_time 195 | self.endtime = self.note.end_time 196 | self.ON = True 197 | if note_opacity == True: 198 | self.set_alpha() 199 | 200 | 201 | def block_draw(self): 202 | return plt.Rectangle((self.x,self.y),self.width,self.height,linewidth = 1, edgecolor = self.colour,facecolor = self.colour) 203 | 204 | def set_alpha(self): 205 | if len(self.colour) == 7: 206 | alpha_value = '80' 207 | self.colour = self.colour + alpha_value 208 | 209 | 210 | def create_plot(lst): 211 | for i in lst: 212 | plt.gca().add_patch(i.block_draw()) 213 | 214 | def destroy_block(lst): 215 | for i in lst: 216 | if i.width + i.x < 0: 217 | lst.remove(i) 218 | 219 | def move_camera(lst,camera): 220 | for i in lst: 221 | i.x -= (camera * camera_speed) 222 | 223 | 224 | class BasicThemeAnimation: 225 | def __init__(self,output_dir): 226 | self.time_elapsed = 0 227 | self.image_number = 0 228 | self.temp_list = [] 229 | self.output_dir = output_dir 230 | self.preview_time = preview_time 231 | self.camera = Camera() 232 | 233 | 234 | def create_frames(self,k): 235 | create_plot(self.temp_list) 236 | plt.savefig(f'{self.output_dir}/frame_{self.image_number}.png') 237 | self.image_number += 1 238 | camera_movement = self.camera.calculate_camera_movement(self.image_number) 239 | move_camera(self.temp_list,camera_movement) 240 | 241 | def detect_note_off(self,frame,k): 242 | for i in self.temp_list: 243 | if i.start_time < k + frames_to_second(frame,fps) and i.endtime >= k + frames_to_second(frame,fps): 244 | i.set_alpha() 245 | 246 | def animate_theme(self): 247 | graph = GraphAnimation() 248 | keys = list(polyphonic_list) 249 | for k, v in polyphonic_list.items(): 250 | difference = k - self.time_elapsed 251 | self.time_elapsed = k 252 | if difference == 0: 253 | for i in v: 254 | self.temp_list.append(Block(i)) 255 | self.create_frames(k) 256 | graph.clear_patches() 257 | else: 258 | frame = 0 259 | current_onset = k 260 | previous_onset = (k - difference) 261 | while frame < frame_rate_note(current_onset,fps) - frame_rate_note(previous_onset,fps): 262 | self.create_frames(k) 263 | frame += 1 264 | graph.clear_patches() 265 | destroy_block(self.temp_list) 266 | for i in v: 267 | self.temp_list.append(Block(i)) 268 | 269 | if k == keys[-1]: 270 | frame = 0 271 | while frame < frame_rate_note(5,fps): 272 | self.create_frames(k) 273 | frame += 1 274 | graph.clear_patches() 275 | destroy_block(self.temp_list) 276 | 277 | self.time_elapsed += 5 278 | 279 | 280 | if preview == True and self.time_elapsed > preview_time: 281 | return 282 | 283 | theme = BasicThemeAnimation(output_dir) 284 | theme.animate_theme() 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | -------------------------------------------------------------------------------- /THEMES/DrumballTheme.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | from utility import frame_rate_note 3 | from tracklist_functions import create_drum_hash, find_all_drums 4 | from pretty_midi import note_number_to_drum_name 5 | from PIL import ImageFont 6 | from os.path import join, dirname, abspath, pardir 7 | from os import getcwd 8 | from sys import argv 9 | import matplotlib 10 | 11 | matplotlib.use("TkAgg") 12 | 13 | class DrumballTheme: 14 | def __init__(self,tracklist): 15 | self.category = 'RHYTHM' 16 | self.display_name = 'Bouncing Balls' 17 | self.tracklist = tracklist 18 | self.background_color = ['Background Colour','COLORBOX','#092047',[]] 19 | self.ball_color = ['Ball Colour','COLORBOX','#fe00fe',[]] 20 | self.box_color = ['Box Colour','COLORBOX','#defe47',[]] 21 | self.line_color = ['Slope Colour','COLORBOX','#ff6e27',[]] 22 | self.inst_name = ['Instrument Names','CHECKBOX',True,[]] 23 | 24 | self.customizable_elements = [self.background_color,self.ball_color,self.box_color,self.line_color,self.inst_name] 25 | self.theme_description = ["Have you ever wondered what it would be like if numerous balls were dropped from a height on a drum set? Well someone did and their idea was the main inspiration behind this theme. Inspired by the works of 'DoodleChaos', this theme serves as a pseudo-physics simulation of musical rhythms. Note that there is an extra second added in the beginning of the animation.", "Created by Aman Trivedi as part of the Maestoso Project."] 26 | 27 | def ANIMATE(self,upd_project_tracklist,upd_PPQN,upd_fps,upd_output_dir,upd_preview,upd_preview_time): 28 | tracklist = self.tracklist 29 | project_tracklist = upd_project_tracklist 30 | PPQN = upd_PPQN 31 | fps = upd_fps 32 | output_dir = upd_output_dir 33 | preview = upd_preview 34 | preview_time = upd_preview_time 35 | dpi = 120 36 | aspect_ratio = (16,9) 37 | height = 720 38 | width = 1280 39 | box_y_pos = 300 40 | 41 | rhythm_tracklist = create_drum_hash(project_tracklist) 42 | 43 | 44 | name_list = find_all_drums(project_tracklist,name=False) 45 | 46 | background_color = self.background_color[2] 47 | ball_color = self.ball_color[2] 48 | box_color = self.box_color[2] 49 | slope_color = self.line_color[2] 50 | instrument_display = self.inst_name[2] 51 | 52 | 53 | size_coefficient = 0 #determines the box_length division based on length of name_list 54 | horizontal_spacing_coefficient = 0 #determines the division of the horizontal_spacing based on length of name_list 55 | if len(name_list) < 3: 56 | size_coefficient = 4 57 | horizontal_spacing_coefficient = 2 58 | elif len(name_list) > 2: 59 | size_coefficient = 2 60 | horizontal_spacing_coefficient = 1 61 | 62 | box_length = (width/len(name_list))/size_coefficient 63 | box_height = (9/16) * box_length 64 | drumbox_list = [] 65 | slope_offset = 40 # how much the slope is away from the box it rests on - on the x axis 66 | slope_ypos = height 67 | slope_height = 200 68 | 69 | def pix_to_inch(size): 70 | one_pix = 1/dpi 71 | return one_pix * size 72 | 73 | class GraphAnimation: 74 | def __init__(self): 75 | plt.ioff() 76 | self.fig, self.ax = plt.subplots(figsize=(pix_to_inch(1280),pix_to_inch(720)),dpi=dpi) 77 | plt.xlim([0,width]) 78 | plt.ylim([0,height]) 79 | plt.grid(False) 80 | self.ax.set_xticklabels([]) 81 | self.ax.set_yticklabels([]) 82 | self.ax.axis('off') 83 | self.fig.set_facecolor(background_color) 84 | self.ax.set_facecolor(background_color) 85 | 86 | def clear_patches(self): 87 | self.ax.patches = [] 88 | 89 | 90 | class DrumBox: 91 | def __init__(self,pitch,x,y): 92 | self.pitch = pitch 93 | self.x = x 94 | self.y = y 95 | self.length = box_length 96 | self.height = box_height 97 | self.colour = box_color + '80' 98 | self.highlight_colour = box_color 99 | self.active = False 100 | 101 | def box_draw(self): 102 | if self.active == False: 103 | colour = self.colour 104 | elif self.active == True: 105 | colour = self.highlight_colour 106 | return plt.Rectangle((self.x,self.y),self.length,self.height,linewidth = 1, edgecolor = colour,facecolor = colour) 107 | 108 | class Slope: 109 | def __init__(self,x,y,pitch): 110 | self.x1 = x - slope_offset 111 | self.y1 = y 112 | self.pitch = pitch 113 | 114 | self.x2 = self.x1 + (box_length/2) 115 | self.y2 = self.y1 - slope_height 116 | 117 | self.slope = (self.y2 - self.y1)/(self.x2 - self.x1) 118 | self.b = self.y1 - (self.x1 * self.slope) 119 | 120 | def plot_slope(self): 121 | plt.plot([self.x1,self.x2],[self.y1,self.y2],color = slope_color) 122 | 123 | 124 | def create_slope(lst): # function to create slope object 125 | slope_list = [] 126 | for i in lst: 127 | slope_list.append(Slope(i.x,slope_ypos,i.pitch)) 128 | return slope_list 129 | 130 | def draw_slope(lst): # function that runs the draw slope method for each slope object in slopelist 131 | for i in lst: 132 | i.plot_slope() 133 | 134 | 135 | def find_horizontal_spacing(): # finds the appropriate spacing between each drum box object 136 | difference = width - (box_length * len(name_list)) 137 | horizontal_spacing = difference/(len(name_list) * horizontal_spacing_coefficient) 138 | return horizontal_spacing 139 | 140 | 141 | def create_drumbox(): # creates drum box objects and adds them to the drum_box list array 142 | horizontal_spacing = find_horizontal_spacing() 143 | xpos = horizontal_spacing 144 | for i in name_list: 145 | drumbox_list.append(DrumBox(i,xpos,box_y_pos)) 146 | xpos += (box_length + horizontal_spacing) 147 | 148 | 149 | class Ball: 150 | def __init__(self,pitch,starting_time,ending_time): 151 | self.pitch = pitch 152 | self.radius = self.find_radius() 153 | self.x = 0 154 | self.y = height + self.radius 155 | self.slope = 0 156 | self.b = 0 157 | self.initialize_values() 158 | self.starting_frame = frame_rate_note(starting_time,fps) 159 | self.ending_frame = frame_rate_note(ending_time,fps) 160 | self.find_time() 161 | self.find_distances() 162 | self.initial_velocity = 0 163 | self.final_velocity = self.calculate_final_velocity() 164 | self.acceleration = self.calculate_acceleration_on_slope() 165 | self.frame = 0 166 | self.bounce = False 167 | 168 | 169 | def find_time(self): 170 | self.total_time = self.ending_frame - self.starting_frame 171 | self.time_in_fall = round(self.total_time * (0.5)) 172 | self.time_on_slope = self.total_time - self.time_in_fall 173 | 174 | def find_distances(self): 175 | self.distance_on_slope = ((slope_ypos - slope_height) + self.radius) - (slope_ypos + self.radius) 176 | self.distance_in_fall = ((box_y_pos + box_height) + self.radius) - ((slope_ypos - slope_height) + self.radius) 177 | 178 | 179 | def initialize_values(self): 180 | slope_list = create_slope(drumbox_list) 181 | for i in slope_list: 182 | if i.pitch == self.pitch: 183 | self.slope = i.slope 184 | self.b = i.b 185 | self.x = i.x1 + (self.radius/2) 186 | 187 | def find_box_pos(self): 188 | for i in drumbox_list: 189 | if i.pitch == self.pitch: 190 | return i.x + (box_length/2) 191 | 192 | def find_radius(self): 193 | horizontal_spacing = find_horizontal_spacing() 194 | return horizontal_spacing/12 195 | 196 | def draw_ball(self): 197 | circle = plt.Circle((self.x, self.y), self.radius, color=ball_color) 198 | return circle 199 | 200 | 201 | def calculate_final_velocity(self): 202 | return self.distance_in_fall/self.time_in_fall 203 | 204 | 205 | def calculate_acceleration_on_slope(self): 206 | return (2 * self.distance_on_slope)/(self.time_on_slope **2) 207 | 208 | 209 | 210 | 211 | def animate(self): 212 | plt.gca().add_patch(self.draw_ball()) 213 | time1_start_frame = 0 # exact frame number at which time 0 starts 214 | time2_start_frame = time1_start_frame + self.time_on_slope 215 | 216 | 217 | if self.frame < time2_start_frame: # if ball is sliding 218 | self.y += self.initial_velocity 219 | self.x = ((1/self.slope) * (self.y - self.b)) + (self.radius * 2) 220 | self.initial_velocity += self.acceleration # causes acceleration 221 | 222 | elif self.frame > time2_start_frame and self.frame < (time2_start_frame + self.time_in_fall): # if ball has left the slope and not yet touched the rectangle 223 | self.y += self.final_velocity 224 | self.final_x_pos = self.x #constant x pos during fall 225 | 226 | elif self.frame > time2_start_frame + self.time_in_fall: 227 | box_border = box_y_pos + box_height + self.radius # the edge of the border 228 | maximum_bounce = box_border + 150 229 | max_possible_speed = self.distance_in_fall # the maximum possible speed that can be attained before bounce 230 | 231 | intercepts_y = box_border # the y dimension of both points 232 | point_a = (self.final_x_pos,intercepts_y) # x on the center of the rectangle and y on the edge of the rectangle 233 | point_b = (((self.final_x_pos + box_length/2) + find_horizontal_spacing()/2), intercepts_y) #intercept b on the center of the horizontal spacing 234 | vertex_y = box_border + ((self.final_velocity * maximum_bounce)/max_possible_speed) # highest point of the parabola on the y axis 235 | vertex_x = (point_a[0] + point_b[0])/2 # the x coordinate of the vertex along the y axis 236 | a = (point_a[1] - vertex_y)/((point_a[0] - vertex_x)**2) # value of a in vertex form 237 | xspeed = (point_b[0] - point_a[0])/self.time_in_fall # horizontal speed estimated from previous speed (max_speed) 238 | 239 | nframes_before_switch = (time2_start_frame + self.time_in_fall) + 1 #the frame count when the ball begins to bounce 240 | total_frames_before_switch = (point_b[0] - self.final_x_pos)/xspeed # the number of frames it will take for the ball to reach point_b[0], i.e, the center of the gap 241 | 242 | if self.frame >= nframes_before_switch and self.frame < (nframes_before_switch + total_frames_before_switch): 243 | for i in drumbox_list: 244 | if i.pitch == self.pitch: 245 | i.active = True 246 | else: 247 | for i in drumbox_list: 248 | if i.pitch == self.pitch: 249 | i.active = False 250 | 251 | 252 | self.x += xspeed 253 | self.y = (a * ((self.x - vertex_x)**2)) + vertex_y 254 | self.frame += 1 255 | 256 | 257 | def create_blocks(lst): 258 | for i in lst: 259 | plt.gca().add_patch(i.box_draw()) 260 | 261 | 262 | def find_font_size(): #find ideal font size that fits within the rectangle size 263 | drum_names = [(note_number_to_drum_name(i.pitch)) for i in drumbox_list] 264 | drum_lens = [len(i) for i in drum_names] 265 | max_length = max(drum_lens) 266 | max_text = drum_names[drum_lens.index(max_length)] 267 | max_font_size = 17 # font size when length of drum_box_list = 1 268 | fnt_width = pix_to_inch(box_length * 2) 269 | while fnt_width > pix_to_inch(box_length/2): 270 | max_font_size -= 1 271 | current_dir = getcwd() 272 | font_path = join(current_dir,'View/Resources/DejaVuSans-Bold.ttf') 273 | font = ImageFont.truetype(font_path,max_font_size) 274 | width_font, height_font = font.font.getsize(max_text) 275 | fnt_width = pix_to_inch(width_font[0]) 276 | return max_font_size 277 | 278 | 279 | 280 | 281 | def annotate_graph(graph,drumbox_list,font_size): # function add text on the graph 282 | for i in drumbox_list: 283 | txt_x = i.x + (box_length/2) 284 | txt_y = i.y + (box_height/2) 285 | drum_name = note_number_to_drum_name(i.pitch) 286 | if drum_name == '': 287 | drum_name = 'Boom' 288 | graph.ax.annotate(drum_name,(txt_x,txt_y),color = 'white',weight = 'bold',fontsize = font_size ,ha = 'center', va ='center') 289 | 290 | 291 | def AnimateTheme(lst): 292 | graph = GraphAnimation() 293 | create_blocks(drumbox_list) 294 | draw_slope(create_slope(drumbox_list)) 295 | image_number = 0 296 | time_elapsed = 0 297 | waiting_time = 1 298 | font_size = find_font_size() 299 | 300 | if instrument_display == True: 301 | annotate_graph(graph,drumbox_list,font_size) 302 | 303 | balls = [] 304 | keys = list(rhythm_tracklist) 305 | for k, v in rhythm_tracklist.items(): 306 | starting_time = time_elapsed # time at which the ball starts to fall 307 | ending_time = k + waiting_time # time at which the ball touches the rectangle 308 | for j in v: 309 | balls.append(Ball(j.pitch,starting_time,ending_time)) 310 | 311 | frame = 0 312 | difference = frame_rate_note(ending_time,fps) - frame_rate_note(starting_time,fps) 313 | end_frames = (ending_time-starting_time)/2 314 | 315 | if k != keys[-1]: 316 | while frame < round(difference/2): # stop the while loop at half the run time 317 | for b in balls: 318 | if b.y < 0: 319 | continue 320 | else: 321 | b.animate() 322 | plt.savefig(f'{output_dir}/frame_{image_number}.png') 323 | graph.clear_patches() 324 | create_blocks(drumbox_list) 325 | frame += 1 326 | image_number += 1 327 | else: 328 | while frame < (5 * fps): # run for five seconds during the last onset 329 | for b in balls: 330 | if b.y < 0: 331 | continue 332 | else: 333 | b.animate() 334 | plt.savefig(f'{output_dir}/frame_{image_number}.png') 335 | graph.clear_patches() 336 | create_blocks(drumbox_list) 337 | frame += 1 338 | image_number += 1 339 | 340 | time_elapsed = ending_time - end_frames 341 | 342 | if preview == True and time_elapsed > preview_time: 343 | return 344 | 345 | create_drumbox() 346 | AnimateTheme(drumbox_list) 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | -------------------------------------------------------------------------------- /THEMES/SwankyDrumMachine.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | currentdir = os.path.dirname(os.path.realpath(__file__)) 3 | parentdir = os.path.dirname(currentdir) 4 | sys.path.append(parentdir) #import modules from parent folder 5 | 6 | import matplotlib.pyplot as plt 7 | import matplotlib.ticker as plticker 8 | import matplotlib 9 | from utility import frame_rate_note, pick_random_colour 10 | from tracklist_functions import create_drum_hash, find_all_drums 11 | from theme_extras import SwankyDrumMachine_Functions as SDF 12 | from pretty_midi import drum_name_to_note_number, note_number_to_drum_name 13 | from math import pi 14 | import random 15 | 16 | 17 | class SwankyDrumMachine: 18 | def __init__(self,tracklist): 19 | self.category = 'RHYTHM' 20 | self.display_name = 'Swanky Machine' 21 | self.tracklist = tracklist 22 | 23 | self.grid_on = ['Grid','CHECKBOX',True] 24 | self.background_colour = ['Background Colour','COLORBOX','blue'] 25 | self.grid_colour = ['Grid Color','COLORBOX',"white"] 26 | self.grid_width = ['Grid Width','ENTRY',0.2,(0,10),"int"] # Entry widgets take 4 arguments...[3][0] describes the starting limit of the entry..(if none, use '') at 3[1] max lmit entry...if type == str, only define max limit in [3],[4] describes the type.. if it's an int, the limit is defined by max number or tuple describing the range and if its a str, the max range is defined by character limit. 27 | self.camera_speed = ["Camera Speed","ENTRY",1,(1,10),"int"] 28 | self.view_size = ['Measures','ENTRY',2,(1,30),"int"] 29 | self.meter_grid = ["Grid Meter Display","CHECKBOX",True] 30 | self.random_flight = ["Random Flight","CHECKBOX",True] 31 | self.random_flight_chances = ["Chance of Flight","ENTRY",2,(1,5),"int" ] 32 | 33 | self.customizable_elements = [self.grid_on,self.background_colour,self.grid_colour,self.grid_width,self.camera_speed,self.view_size,self.meter_grid,self.random_flight,self.random_flight_chances] # always use the following attribute name and type 34 | count = 1 35 | for i in find_all_drums(self.tracklist.track_list, name = False): 36 | midi_number = i 37 | try: 38 | midi_name = note_number_to_drum_name(midi_number) 39 | except: 40 | midi_name = f'Drum {count}' 41 | count += 1 42 | self.customizable_elements.append([f"{midi_name}","COLORBOX",f"{pick_random_colour()}","colour"]) 43 | self.customizable_elements.append([f"{midi_name} Animation","DROPDOWN",SDF.fetch_ideal_animation(midi_number),["bounce","slide","shake","roll","jump and roll","expand","random"],"animation"]) 44 | self.customizable_elements.append([f"{midi_name} Shape","DROPDOWN",SDF.fetch_ideal_shape(midi_number),["square","triangle","circle","mid circle","upright rectangle","ellipse","rectangle","random"],"shape"]) 45 | 46 | self.theme_description = ["The classic piano roll styled theme inspired by the works of Stephen Malinowski. As they say: when in doubt, go with the classics - and there's nothing more classic than a piano roll theme.","Created by Aman Trivedi as part of the Maestoso Project."] 47 | 48 | 49 | def ANIMATE(self,upd_project_tracklist,upd_PPQN,upd_fps,upd_output_dir,upd_preview,upd_preview_time): 50 | tracklist = self.tracklist 51 | project_tracklist = upd_project_tracklist 52 | PPQN = upd_PPQN 53 | fps = upd_fps 54 | output_dir = upd_output_dir 55 | preview = upd_preview 56 | preview_time = upd_preview_time 57 | dpi = 120 58 | aspect_ratio = (16,9) 59 | rhythm_tracklist = create_drum_hash(project_tracklist) 60 | name_list = find_all_drums(project_tracklist,name=False) 61 | 62 | grid_on = self.grid_on[2] 63 | background_colour = self.background_colour[2] 64 | grid_colour = self.grid_colour[2] 65 | grid_width = self.grid_width[2] 66 | camera_speed = float(self.camera_speed[2]) 67 | view_size = float(self.view_size[2]) 68 | grid_meter = self.meter_grid[2] 69 | random_flight = self.random_flight[2] 70 | random_flight_chances = self.random_flight_chances[2] 71 | 72 | measures = view_size 73 | 74 | # if len(tracklist.ts_changes) > 1: 75 | # return 76 | 77 | item_map = {} 78 | for i in self.customizable_elements: 79 | try: 80 | if i[3] == "colour": 81 | item_map.update({i[0]:[i[2]]}) 82 | except: 83 | continue 84 | 85 | for i in self.customizable_elements: 86 | try: 87 | if i[4] == "animation": 88 | upd_name = i[0].replace(' Animation','') 89 | item_map[upd_name].append(i[2]) 90 | elif i[4] == 'shape': 91 | upd_name = i[0].replace(' Shape','') 92 | item_map[upd_name].append(i[2]) 93 | except: 94 | continue 95 | 96 | chosen_note_height = 60 97 | margin = 200 98 | 99 | def adjust_dimensions(pitch_range): 100 | height = (chosen_note_height * len(pitch_range)) + margin + int(margin/2) 101 | width = round((height * aspect_ratio[0])/aspect_ratio[1]) 102 | return (width,height) 103 | 104 | width = adjust_dimensions(name_list)[0] 105 | height = adjust_dimensions(name_list)[1] 106 | base_pulse_size = width/(tracklist.ts_changes[0].numerator * measures) #pulse size 107 | base_note_size = (tracklist.ts_changes[0].denominator/4) * base_pulse_size #quarter note size 108 | 109 | def box_length(note): 110 | nx = ((4/tracklist.ts_changes[0].denominator)/note.note_length) # calculates note ratio against quarter note length -- quarter = 1 (PPQN/PPQN) 111 | box_length = width / (nx * (tracklist.ts_changes[0].numerator * measures)) 112 | return box_length 113 | 114 | def box_location_x(note): 115 | if note.start_ticks == 0: 116 | x_pos = width/2 117 | return x_pos # if note onset is 0 ticks 118 | else: 119 | distance_length = note.start_ticks/PPQN #The total distance that occurs before the note 120 | nx = (4/tracklist.ts_changes[0].denominator)/distance_length 121 | x_pos = (width / (nx * (tracklist.ts_changes[0].numerator * measures))) + width/2 122 | return x_pos 123 | 124 | def create_ypos_hierarchy(): 125 | returning_dict = {} #a dictionary containing hierarchal seperation 126 | temp_lst = find_all_drums(project_tracklist,name= False) 127 | counter = len(temp_lst) 128 | while counter >= 1: 129 | max_note = max(temp_lst) 130 | returning_dict.update({max_note:counter}) 131 | max_index = temp_lst.index(max_note) 132 | temp_lst.remove(temp_lst[max_index]) 133 | counter -= 1 134 | return returning_dict 135 | 136 | returning_dict = create_ypos_hierarchy() 137 | 138 | def box_location_y(note): 139 | max_position = height - (margin/2) 140 | max_note = max(name_list) 141 | max_pos = len(name_list) 142 | difference = max_pos - returning_dict.get(note) 143 | location = max_position - (difference * chosen_note_height) 144 | return location 145 | 146 | 147 | def pix_to_inch(size): 148 | one_pix = 1/dpi 149 | return one_pix * size 150 | 151 | 152 | 153 | class GraphAnimation: 154 | def __init__(self): 155 | plt.ioff() 156 | self.fig, self.ax = plt.subplots(figsize=(pix_to_inch(1280),pix_to_inch(720)),dpi=dpi) 157 | plt.xlim([0,width]) 158 | plt.ylim([0,height]) 159 | if grid_on == True: 160 | if grid_meter == True: 161 | grd_space = plticker.MultipleLocator(base = base_pulse_size) 162 | self.ax.yaxis.set_major_locator(grd_space) 163 | self.ax.xaxis.set_major_locator(grd_space) 164 | plt.grid(which = 'major', color = grid_colour,linewidth = grid_width) 165 | self.ax.spines['right'].set_visible(False) 166 | self.ax.spines['left'].set_visible(False) 167 | self.ax.spines['top'].set_visible(False) 168 | self.ax.spines['bottom'].set_visible(False) 169 | else: 170 | plt.grid(False) 171 | self.ax.axis('off') 172 | self.ax.set_xticklabels([]) 173 | self.ax.set_yticklabels([]) 174 | self.fig.patch.set_facecolor(background_colour) 175 | self.ax.patch.set_facecolor(background_colour) 176 | 177 | def clear_patches(self): 178 | self.ax.patches = [] 179 | 180 | class Camera: 181 | def __init__(self): 182 | self.quarter_length = base_note_size 183 | self.tempo_changes = self.construct_tempo_map() 184 | self.current_tempo = self.tempo_changes.get(0.0) 185 | self.camera_movement = self.calculate_camera_movement(0.0) 186 | 187 | def construct_tempo_map(self): 188 | tempo_changes = {} 189 | for i in range(len(tracklist.tempo_changes[0])): 190 | tempo_changes.update({frame_rate_note(tracklist.tempo_changes[0][i],fps):tracklist.tempo_changes[1][i]}) 191 | return tempo_changes 192 | 193 | 194 | def calculate_camera_movement(self,frame_number): 195 | try: 196 | self.current_tempo = self.tempo_changes.get(frame_number) 197 | nqps = self.current_tempo/60 198 | self.camera_movement = (self.quarter_length * nqps)/fps 199 | return self.camera_movement 200 | except: 201 | return self.camera_movement 202 | 203 | class NoteBlock: 204 | def __init__(self,note): 205 | self.note = note 206 | self.width = box_length(self.note) 207 | self.x = box_location_x(self.note) 208 | self.y = box_location_y(self.note.pitch) 209 | self.instrument_name = note_number_to_drum_name(self.note.pitch) 210 | self.colour = item_map.get(self.instrument_name)[0] 211 | self.category = SDF.find_instrument_shape(self.note.pitch) 212 | self.height = SDF.return_entity_height(self.width,self.category,chosen_note_height) 213 | self.starting_frame = frame_rate_note(self.note.start_time,fps) 214 | self.note_duration = frame_rate_note(self.note.return_duration(),fps) * 2 215 | if self.note_duration == 0: 216 | self.note_duration = fps 217 | self.intensity = self.note.velocity 218 | self.colour_on = self.colour 219 | self.colour_off = self.colour + '80' 220 | self.randomselection = False 221 | 222 | if item_map.get(self.instrument_name)[1] == "random": 223 | self.animation_name = SDF.fetch_ideal_animation(self.note.pitch) 224 | else: 225 | self.animation_name = item_map.get(self.instrument_name)[1] 226 | 227 | if item_map.get(self.instrument_name)[2] == "random": 228 | self.shape = SDF.fetch_ideal_shape(self.note.pitch) 229 | else: 230 | self.shape = item_map.get(self.instrument_name)[2] 231 | 232 | if self.shape == "ellipse": 233 | self.angle = 180 234 | else: 235 | self.angle = 0 236 | 237 | def draw_shape(self,frame): 238 | entity = SDF.draw_shape(self.x,self.y,self.height,self.width,self.colour,self.shape) 239 | if self.animation_name == "shake" or self.animation_name == "roll" or self.animation_name == "jump and roll" and frame > self.starting_frame and self.randomselection == False: 240 | angle_rad = self.angle * (pi/180) 241 | x_center = self.x + (self.width/2) 242 | y_center = self.y + (self.height/2) 243 | transform = matplotlib.transforms.Affine2D().rotate_around(x_center,y_center,angle_rad) 244 | entity.set_transform(transform) 245 | plt.gca().add_patch(entity) 246 | 247 | def animate_object(self,frame): 248 | if self.randomselection == True: 249 | SDF.animate_entity(frame,self.starting_frame,self.note_duration,self.height,self, 250 | self.intensity,height,randomselection = True) 251 | else: 252 | SDF.animate_entity(frame,self.starting_frame,self.note_duration,self.height,self, 253 | self.intensity,height,randomselection = False) 254 | SDF.change_colour(object = self,current_frame = frame, starting_frame = self.starting_frame, note_duration = self.note_duration) 255 | 256 | 257 | def select_a_random_note(self,onsets,fps): 258 | keys = list(onsets.keys()) 259 | if keys[-1] == self.note.start_time: 260 | self.randomselection = False 261 | return 262 | current_index = keys.index(self.note.start_time) 263 | try: 264 | selection = keys[current_index + 1:current_index + 10] 265 | ideal_indexes = [] 266 | for i in selection: 267 | if frame_rate_note(i,fps) - self.starting_frame >= frame_rate_note(1,fps): 268 | temp_list = onsets.get(i) 269 | if box_location_x(temp_list[0]) - self.x <= (width/(measures * 2)): 270 | ideal_indexes.append(selection.index(i)) 271 | if len(ideal_indexes) == 0: 272 | self.randomselection = False 273 | return 274 | else: 275 | lucky_val = random.randint(0,10) 276 | if lucky_val < (random_flight_chances/10): 277 | selected_index = random.choice(ideal_indexes) 278 | selected_onset = selection[selected_index] 279 | values_in_selection = onsets.get(selected_onset) 280 | selected_note = random.choice(values_in_selection) 281 | self.landing_values = [frame_rate_note(selected_note.start_time,fps), box_location_x(selected_note), box_location_y(selected_note.pitch),box_length(selected_note),SDF.return_entity_height(box_length(selected_note), SDF.find_instrument_shape(selected_note.pitch),chosen_note_height)] #[0] = starting frame, [1] = x loc, [2] = y loc, [3] = length, [4] = height 282 | self.randomselection = True 283 | else: 284 | self.randomselection = False 285 | except: 286 | self.randomselection = False 287 | 288 | 289 | 290 | 291 | 292 | def animate_animation(): 293 | graph = GraphAnimation() 294 | camera = Camera() 295 | block_list = [] 296 | 297 | time_keys = list(rhythm_tracklist.keys()) 298 | 299 | if preview == True and preview_time < time_keys[-1]: 300 | end_time = preview_time 301 | else: 302 | end_time = time_keys[-1] 303 | 304 | for k, v in rhythm_tracklist.items(): 305 | for i in v: 306 | block_list.append(NoteBlock(i)) 307 | 308 | if random_flight == True: 309 | for i in block_list: 310 | i.select_a_random_note(rhythm_tracklist,fps) 311 | 312 | image_number = 0 313 | 314 | def create_drawings(camera_movement): 315 | for i in block_list: 316 | i.draw_shape(image_number) 317 | i.x -= (camera_movement * camera_speed) 318 | 319 | while image_number < frame_rate_note(end_time,fps): 320 | camera_movement = camera.calculate_camera_movement(image_number) 321 | create_drawings(camera_movement) 322 | for i in block_list: 323 | i.animate_object(image_number) 324 | plt.savefig(f'{output_dir}/frame_{image_number}.png') 325 | graph.clear_patches() 326 | image_number += 1 327 | 328 | animate_animation() 329 | 330 | -------------------------------------------------------------------------------- /theme_extras/SwankyDrumMachine_Functions.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib.patches as patches 3 | from math import sqrt 4 | import random 5 | 6 | instrument_general_midi = {"Bass":(35,36),"Snare":(37,38,39,40), 7 | "Tom":(41,43,45,47,48,50),"HH":(42,46),"Crash":(52,51,53), 8 | "PercA":(54,58,56,80,82,72,70,69) #Will be used with diagonal shaker anim 9 | ,"PercB":(60,61,62,63) # Will be used with circular percussive anim 10 | ,"PercC":(76,77,73,74,75,56,65,66,64,67,68) # Will be used with triangle anim 11 | ,"SpecialFx":(44,49,55,27,28,29,30,31,32,34) #Sweeping Animations and their variations 12 | ,"SpecialFx2":(84,83,58) # Shimmer effects 13 | } 14 | 15 | def find_instrument_shape(note_number): 16 | for k, v in instrument_general_midi.items(): 17 | for j in v: 18 | if j == note_number: 19 | return k 20 | return "default instrument" 21 | 22 | def return_entity_height(width,category,height): 23 | if category == "Snare": 24 | return width 25 | elif category == "HH" or category == "Crash": 26 | return width/4 27 | elif category == "PercA": 28 | return width * 1.5 29 | 30 | elif category == "default instrument": 31 | return width * (9/16) 32 | 33 | else: 34 | return height 35 | 36 | instrument_to_animation = {"Bass":["expand","bounce"],"Snare":["bounce","shake","slide"], 37 | "Tom":["bounce","shake","slide"],"HH":["shake","roll"],"Crash":["shake","roll"], 38 | "PercA":["shake","slide"] #Will be used with diagonal shaker anim 39 | ,"PercB":["expand","bounce","jump and roll"] # Will be used with circular percussive anim 40 | ,"PercC":["shake","roll","jump and roll","slide"] # Will be used with triangle anim 41 | ,"SpecialFx":["shake","slide"] #Sweeping Animations and their variations 42 | ,"SpecialFx2":["bounce","shake","slide"],"default instrument":["bounce","shake","slide"]} # Shimmer effects 43 | 44 | 45 | def fetch_ideal_animation(note_number): 46 | shape = find_instrument_shape(note_number) 47 | lst = instrument_to_animation.get(shape) 48 | return random.choice(lst) 49 | 50 | instrument_to_shape = {"Bass":"circle","Snare":"square", 51 | "Tom":"mid circle","HH":"ellipse","Crash":"ellipse", 52 | "PercA":"upright rectangle" 53 | ,"PercB":"mid circle" 54 | ,"PercC":"triangle" 55 | ,"SpecialFx":"square" 56 | ,"SpecialFx2":"triangle", 57 | "default instrument": "rectangle"} 58 | 59 | 60 | def fetch_ideal_shape(note_number): 61 | shape = find_instrument_shape(note_number) 62 | return instrument_to_shape.get(shape) 63 | 64 | 65 | 66 | ############################ 67 | 68 | def create_rectangle(x,y,height,width,colour): 69 | return plt.Rectangle((x,y),width,height,linewidth = 1, edgecolor = colour,facecolor = colour) # can rotate 70 | 71 | def create_rectangle_upright(x,y,height,width,colour): 72 | return plt.Rectangle((x,y),width,height,linewidth = 1, edgecolor = colour,facecolor = colour) # can rotate 73 | 74 | 75 | def create_triangle(x,y,height,width,colour): 76 | points_a = [x,y] 77 | points_b = [x + (width/2), y + height] 78 | points_c = [x + width, y] 79 | points = [points_a,points_b,points_c] 80 | return plt.Polygon(points,color = colour) # can't rotate 81 | 82 | def create_circle(x,y,height,width,colour): 83 | radius = width/2 84 | return plt.Circle((x,y),radius,edgecolor = colour,facecolor = colour) 85 | 86 | def create_ellipse(x,y,height,width,colour): 87 | return patches.Ellipse((x,y),width,height, color = colour) # can rotate 88 | 89 | def create_square(x,y,height,width,colour): 90 | return plt.Rectangle((x,y),width,height,linewidth = 1, edgecolor = colour,facecolor = colour) # can rotate 91 | 92 | def create_circle_not_bass(x,y,height,width,colour): 93 | radius = width/4 94 | return plt.Circle((x,y),radius,edgecolor = colour,facecolor = colour) 95 | 96 | ################################# 97 | 98 | 99 | instrument_shape_mapping = {"circle":create_circle, 100 | "ellipse":create_ellipse, 101 | "upright rectangle":create_rectangle_upright 102 | ,"mid circle":create_circle_not_bass 103 | ,"square":create_square 104 | ,"triangle":create_triangle 105 | ,"rectangle": create_rectangle} 106 | 107 | 108 | def draw_shape(x,y,height,width,colour,shape): 109 | return instrument_shape_mapping[shape](x,y,height,width,colour) 110 | 111 | ########################################################################## 112 | """ALL ANIMATION FUNCTIONS GO HERE """ 113 | 114 | 115 | def bounce(**kwargs): 116 | if kwargs['current_frame'] < kwargs['starting_frame'] or kwargs['current_frame'] > kwargs['starting_frame'] + (kwargs['note_duration'] * 2): 117 | return 118 | 119 | elif kwargs['current_frame'] == kwargs['starting_frame']: #intialize all important values 120 | kwargs['object'].distance = (kwargs['intensity']/127) * (kwargs['height'] * 0.75 ) #how much the note goes high is dependent on the notes velocity -- 127 === max velocity that will lead the note towards a height of note_height * 0.75 121 | kwargs['object'].time = kwargs['note_duration'] #time spent going up 122 | kwargs['object'].upward_acceleration = (2 * kwargs['object'].distance)/(kwargs['object'].time ** 2) 123 | kwargs['object'].velocity = kwargs['object'].upward_acceleration 124 | kwargs['object'].downward_velocity = (kwargs['object'].distance * -1)/kwargs['object'].time 125 | kwargs['object'].y += kwargs['object'].velocity 126 | 127 | elif kwargs['current_frame'] > kwargs['starting_frame'] and kwargs['current_frame'] < (kwargs['starting_frame'] + (kwargs['note_duration'])): 128 | kwargs['object'].velocity += kwargs['object'].upward_acceleration 129 | kwargs['object'].y += kwargs['object'].velocity 130 | 131 | elif kwargs['current_frame'] >= kwargs['starting_frame'] + kwargs['note_duration'] and kwargs['current_frame'] < kwargs['starting_frame'] + (kwargs['note_duration'] * 2): 132 | kwargs['object'].y += kwargs['object'].downward_velocity 133 | 134 | def change_colour(**kwargs): 135 | if kwargs['current_frame'] < kwargs['starting_frame'] or kwargs['current_frame'] > kwargs['starting_frame'] + (kwargs['note_duration'] * 2): 136 | kwargs['object'].colour = kwargs['object'].colour_off 137 | return 138 | else: 139 | kwargs['object'].colour = kwargs['object'].colour_on 140 | 141 | 142 | def expand(**kwargs): 143 | if kwargs['current_frame'] < kwargs['starting_frame'] or kwargs['current_frame'] > kwargs['starting_frame'] + (kwargs['note_duration'] * 2): 144 | return 145 | 146 | elif kwargs['current_frame'] == kwargs['starting_frame']: #intialize all important values 147 | kwargs['object'].expansion_rate = (kwargs['intensity']/127) * 2 #describes the expansion rate - i.e, the rate at which the radius or block will grow in size 148 | kwargs['object'].width += kwargs['object'].expansion_rate 149 | 150 | elif kwargs['current_frame'] > kwargs['starting_frame'] and kwargs['current_frame'] < (kwargs['starting_frame'] + (kwargs['note_duration'])): 151 | kwargs['object'].width += kwargs['object'].expansion_rate 152 | 153 | elif kwargs['current_frame'] >= kwargs['starting_frame'] + kwargs['note_duration'] and kwargs['current_frame'] < kwargs['starting_frame'] + (kwargs['note_duration'] * 2): 154 | kwargs['object'].width -= kwargs['object'].expansion_rate 155 | 156 | 157 | def shake(**kwargs): 158 | if kwargs['current_frame'] < kwargs['starting_frame'] or kwargs['current_frame'] > kwargs['starting_frame'] + (kwargs['note_duration'] * 4): 159 | return 160 | 161 | elif kwargs['current_frame'] == kwargs['starting_frame']: 162 | kwargs['object'].cycles = round((kwargs['intensity']/127) * 4) 163 | kwargs['object'].single_cycle = round(((kwargs['note_duration'] * 4)/kwargs['object'].cycles)) 164 | kwargs['object'].onset_list = [] 165 | kwargs['object'].final_onset_duration = (kwargs['note_duration'] * 4) - (kwargs['object'].single_cycle * (kwargs['object'].cycles - 1)) 166 | for i in range(kwargs['object'].cycles): 167 | if i == 0: 168 | kwargs['object'].onset_list.append(kwargs['starting_frame']) 169 | else: 170 | kwargs['object'].onset_list.append(kwargs['starting_frame'] + (kwargs['object'].single_cycle * i)) 171 | kwargs['object'].max_rotation = (kwargs['intensity']/127) * 60 172 | kwargs['object'].current_index = 0 173 | kwargs['object'].current_rotation = (1/(kwargs['object'].current_index + 1)) * kwargs['object'].max_rotation #reduce angle of rotation with each repetition to show loss of energy 174 | kwargs['object'].rate_of_rot = kwargs['object'].current_rotation/round((kwargs['object'].single_cycle/2)) #divide each cycle into two parts 175 | kwargs['object'].angle += kwargs['object'].rate_of_rot 176 | 177 | elif kwargs['current_frame'] > kwargs['starting_frame'] and kwargs['current_frame'] < kwargs['starting_frame'] + (kwargs['note_duration'] * 4): 178 | if kwargs['object'].current_index != len(kwargs['object'].onset_list) -1: 179 | if kwargs['current_frame'] < kwargs['object'].onset_list[kwargs['object'].current_index + 1]: 180 | time_clockwise = kwargs['object'].onset_list[kwargs['object'].current_index] + round((kwargs['object'].single_cycle)/2) 181 | if kwargs['current_frame'] < time_clockwise: 182 | kwargs['object'].angle += kwargs['object'].rate_of_rot 183 | else: 184 | time_anticlockwise = (kwargs['object'].onset_list[kwargs['object'].current_index] + kwargs['object'].single_cycle) - time_clockwise 185 | kwargs['object'].rate_of_rot = kwargs['object'].current_rotation/time_anticlockwise 186 | kwargs['object'].angle -= kwargs['object'].rate_of_rot 187 | else: 188 | kwargs['object'].current_index += 1 189 | kwargs['object'].current_rotation = (1/(kwargs['object'].current_index + 1)) * kwargs['object'].max_rotation 190 | kwargs['object'].rate_of_rot = kwargs['object'].current_rotation/round((kwargs['object'].single_cycle)/2) 191 | kwargs['object'].angle += kwargs['object'].rate_of_rot 192 | else: 193 | time_clockwise = kwargs['object'].onset_list[-1] + round((kwargs['object'].final_onset_duration)/2) 194 | kwargs['object'].current_rotation = (1/(kwargs['object'].current_index + 1)) * kwargs['object'].max_rotation 195 | kwargs['object'].rate_of_rot = kwargs['object'].current_rotation/round((kwargs['object'].final_onset_duration)/2) 196 | if kwargs['current_frame'] < time_clockwise: 197 | kwargs['object'].angle += kwargs['object'].rate_of_rot 198 | else: 199 | time_anticlockwise = (kwargs['object'].onset_list[kwargs['object'].current_index] + kwargs['object'].single_cycle) - time_clockwise 200 | kwargs['object'].rate_of_rot = kwargs['object'].current_rotation/time_anticlockwise 201 | kwargs['object'].angle -= kwargs['object'].rate_of_rot 202 | 203 | 204 | def slide(**kwargs): 205 | if kwargs['current_frame'] < kwargs['starting_frame'] or kwargs['current_frame'] > kwargs['starting_frame'] + (kwargs['note_duration'] * 2): 206 | return 207 | 208 | elif kwargs['current_frame'] == kwargs['starting_frame']: 209 | kwargs['object'].xdistance = (kwargs['intensity']/127) * 10 210 | kwargs['object'].xacceleration = (2 * kwargs['object'].xdistance)/(kwargs['note_duration'] ** 2) 211 | kwargs['object'].xvelocity_forward = kwargs['object'].xacceleration 212 | kwargs['object'].xvelocity_behind = kwargs['object'].xacceleration 213 | kwargs['object'].x += kwargs['object'].xvelocity_forward 214 | 215 | elif kwargs['current_frame'] > kwargs['starting_frame'] and kwargs['current_frame'] < (kwargs['starting_frame'] + (kwargs['note_duration'])): 216 | kwargs['object'].xvelocity_forward += kwargs['object'].xacceleration 217 | kwargs['object'].x += kwargs['object'].xvelocity_forward 218 | 219 | elif kwargs['current_frame'] >= kwargs['starting_frame'] + kwargs['note_duration'] and kwargs['current_frame'] < kwargs['starting_frame'] + (kwargs['note_duration'] * 2): 220 | kwargs['object'].x -= kwargs['object'].xvelocity_behind 221 | kwargs['object'].xvelocity_behind += kwargs['object'].xacceleration 222 | 223 | 224 | def roll(**kwargs): 225 | if kwargs['current_frame'] < kwargs['starting_frame'] or kwargs['current_frame'] > kwargs['starting_frame'] + (kwargs['note_duration'] * 2): 226 | return 227 | 228 | elif kwargs['current_frame'] == kwargs['starting_frame']: #intialize all important values 229 | kwargs['object'].roll_rate = 360/(kwargs['note_duration'] * 2) 230 | kwargs['object'].angle += kwargs['object'].roll_rate 231 | 232 | elif kwargs['current_frame'] > kwargs['starting_frame'] and kwargs['current_frame'] < kwargs['starting_frame'] + (kwargs['note_duration'] * 2): 233 | kwargs['object'].angle += kwargs['object'].roll_rate 234 | 235 | def bounce_and_roll(**kwargs): 236 | if kwargs['current_frame'] < kwargs['starting_frame'] or kwargs['current_frame'] > kwargs['starting_frame'] + (kwargs['note_duration'] * 2): 237 | return 238 | 239 | elif kwargs['current_frame'] == kwargs['starting_frame']: #intialize all important values 240 | kwargs['object'].distance = (kwargs['intensity']/127) * (kwargs['height'] * 0.75) #how much the note goes high is dependent on the notes velocity -- 127 === max velocity that will lead the note towards a height of note_height * 0.75 241 | kwargs['object'].time = kwargs['note_duration'] #time spent going up 242 | kwargs['object'].upward_acceleration = (2 * kwargs['object'].distance)/(kwargs['object'].time ** 2) 243 | kwargs['object'].velocity = kwargs['object'].upward_acceleration 244 | kwargs['object'].downward_velocity = (kwargs['object'].distance * -1)/kwargs['object'].time 245 | kwargs['object'].y += kwargs['object'].velocity 246 | kwargs['object'].roll_rate = 360/(kwargs['note_duration'] * 2) 247 | kwargs['object'].angle += kwargs['object'].roll_rate 248 | 249 | elif kwargs['current_frame'] > kwargs['starting_frame'] and kwargs['current_frame'] < (kwargs['starting_frame'] + (kwargs['note_duration'])): 250 | kwargs['object'].velocity += kwargs['object'].upward_acceleration 251 | kwargs['object'].y += kwargs['object'].velocity 252 | kwargs['object'].angle += kwargs['object'].roll_rate 253 | 254 | elif kwargs['current_frame'] >= kwargs['starting_frame'] + kwargs['note_duration'] and kwargs['current_frame'] < kwargs['starting_frame'] + (kwargs['note_duration'] * 2): 255 | kwargs['object'].y += kwargs['object'].downward_velocity 256 | kwargs['object'].angle += kwargs['object'].roll_rate 257 | 258 | 259 | def flight(**kwargs): 260 | if kwargs['current_frame'] < kwargs['starting_frame'] or kwargs['current_frame'] > kwargs['object'].landing_values[0]: 261 | return 262 | 263 | elif kwargs['current_frame'] == kwargs['starting_frame']: 264 | kwargs['object'].xdistance = (kwargs['object'].landing_values[1] + (kwargs['object'].landing_values[3]/2)) - kwargs['object'].x 265 | max_ver_dis = (kwargs['intensity']/127) * ((kwargs['screen_height'] - kwargs['object'].height) - kwargs['object'].y) #maximum vertical distance based on velocity 266 | max_ver_pos = kwargs['object'].y + max_ver_dis #the y point of the vertex 267 | kwargs['object'].time = kwargs['object'].landing_values[0] - kwargs['current_frame'] 268 | if max_ver_pos > (kwargs['object'].landing_values[2] + kwargs['object'].landing_values[4]): #if max position is greater than paired objects y position 269 | kwargs['object'].parabola = True 270 | kwargs['object'].vertex = (kwargs['object'].xdistance/2, max_ver_pos) 271 | kwargs['object'].a = (kwargs['object'].y - kwargs['object'].vertex[1])/((kwargs['object'].vertex[0] - kwargs['object'].x)**2) 272 | kwargs['object'].time_a = round(kwargs['object'].time * 0.6) 273 | kwargs['object'].time_b = kwargs['object'].time - kwargs['object'].time_a 274 | kwargs['object'].xspeed = (kwargs['object'].vertex[0] - kwargs['object'].x)/kwargs['object'].time_a 275 | kwargs['object'].acceleration = (kwargs['object'].xdistance - (2 * (kwargs['object'].xspeed * kwargs['object'].time_b)))/(kwargs['object'].time_b **2) 276 | 277 | kwargs['object'].x += kwargs['object'].xspeed 278 | kwargs['object'].y = (kwargs['object'].a * ((kwargs['object'].x - kwargs['object'].vertex[0])**2)) + kwargs['object'].vertex[1] 279 | 280 | else: 281 | kwargs['object'].parabola = False 282 | kwargs['object'].xspeed = kwargs['object'].xdistance/kwargs['object'].time 283 | kwargs['object'].gradient = (kwargs['object'].landing_values[2] - kwargs['object'].y)/(kwargs['object'].landing_values[1] - kwargs['object'].x) 284 | kwargs['object'].b = kwargs['object'].y - (kwargs['object'].gradient * kwargs['object'].x) 285 | kwargs['object'].x += kwargs['object'].xspeed 286 | kwargs['object'].y = (kwargs['object'].gradient * kwargs['object'].x) + kwargs['object'].b 287 | 288 | elif kwargs['current_frame'] > kwargs['starting_frame'] and kwargs['current_frame'] < kwargs['object'].landing_values[0]: 289 | if kwargs['object'].parabola == True: 290 | if kwargs['current_frame'] < (kwargs['starting_frame'] + kwargs['object'].time_a): 291 | kwargs['object'].x += kwargs['object'].xspeed 292 | kwargs['object'].y = (kwargs['object'].a * ((kwargs['object'].x - kwargs['object'].vertex[0])**2)) + kwargs['object'].vertex[1] 293 | else: 294 | kwargs['object'].xspeed += kwargs['object'].acceleration 295 | kwargs['object'].x += kwargs['object'].xspeed 296 | kwargs['object'].y = (kwargs['object'].a * ((kwargs['object'].x - kwargs['object'].vertex[0])**2)) + kwargs['object'].vertex[1] 297 | else: 298 | kwargs['object'].x += kwargs['object'].xspeed 299 | kwargs['object'].y = (kwargs['object'].gradient * kwargs['object'].x) + kwargs['object'].b 300 | 301 | 302 | 303 | animation_map = {"slide":slide, "bounce":bounce,"roll":roll,"jump and roll":bounce_and_roll,"shake":shake,"expand":expand,"flight":flight} 304 | 305 | def animate_entity(frame,starting_frame,note_duration, height,obj,intensity,screen_height,randomselection = False): 306 | if randomselection == False: 307 | an_nam = obj.animation_name 308 | else: 309 | an_nam = "flight" 310 | return animation_map[an_nam](current_frame = frame,starting_frame = starting_frame, note_duration = note_duration,height = height, object = obj, intensity = intensity, screen_height = screen_height) 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------