| @@ -0,0 +1,410 @@ | |||||
| #! /usr/bin/python | |||||
| # -*- coding: utf-8 -*- | |||||
| """ | |||||
| MusicQuizzer is a Python program that can help you prepare for any test that | |||||
| involves listening to excerpts of music pieces and answering multiple choice | |||||
| questions about them. For more information, see the included README.md. | |||||
| """ | |||||
| from __future__ import division | |||||
| import ConfigParser as configparser | |||||
| import os | |||||
| from pygame import mixer, error | |||||
| import random | |||||
| import thread | |||||
| import time | |||||
| from Tkinter import * | |||||
| from tkFont import Font | |||||
| from urllib import urlretrieve | |||||
| __author__ = "Ben Kurtovic" | |||||
| __copyright__ = "Copyright (c) 2011 by Ben Kurtovic" | |||||
| __license__ = "MIT License" | |||||
| __version__ = "0.1" | |||||
| __email__ = "ben.kurtovic@verizon.net" | |||||
| config_filename = "config.cfg" | |||||
| config = None | |||||
| piece_dir = None | |||||
| download_complete = False | |||||
| master_width = 500 | |||||
| master_height = 500 | |||||
| question_height = 100 | |||||
| class AnswerSheet(object): | |||||
| def __init__(self, master): | |||||
| self.master = master | |||||
| self.order = generate_piece_order() | |||||
| self.init_widgets() | |||||
| self.generate_questions() | |||||
| self.grid_questions() | |||||
| def init_widgets(self): | |||||
| self.scroll = Scrollbar(self.master) | |||||
| self.scroll.grid(row=1, column=1, sticky=N+S) | |||||
| self.canvas = Canvas(self.master, yscrollcommand=self.scroll.set, | |||||
| width=master_width, height=master_height-30) | |||||
| self.canvas.grid(row=1, column=0, sticky=N+S+E+W) | |||||
| self.canvas.grid_propagate(0) | |||||
| self.scroll.config(command=self.canvas.yview) | |||||
| self.master.grid_rowconfigure(0, weight=1) | |||||
| self.master.grid_columnconfigure(0, weight=1) | |||||
| self.frame = Frame(self.canvas) | |||||
| self.frame.rowconfigure(1, weight=1) | |||||
| self.frame.columnconfigure(1, weight=1) | |||||
| self.header = Frame(self.master, bd=2, relief="groove") | |||||
| self.header.grid(row=0, column=0, columnspan=2) | |||||
| self.header_buttons = Frame(self.header, width=250, height=30) | |||||
| self.header_buttons.grid_propagate(0) | |||||
| self.header_buttons.grid(row=0, column=0) | |||||
| self.playing_container = Frame(self.header, width=master_width-240, height=30) | |||||
| self.playing_container.grid_propagate(0) | |||||
| self.playing_container.grid(row=0, column=1) | |||||
| self.play_button = Button(self.header_buttons, text="Start Quiz", | |||||
| command=self.play) | |||||
| self.play_button.grid(row=0, column=0) | |||||
| self.submit_button = Button(self.header_buttons, text="Submit Answers", | |||||
| command=self.submit) | |||||
| self.submit_button.grid(row=0, column=1) | |||||
| self.playing = StringVar() | |||||
| self.playing.set("Now Playing: None") | |||||
| self.now_playing = Label(self.playing_container, textvar=self.playing) | |||||
| self.now_playing.grid(row=0, column=0) | |||||
| def generate_questions(self): | |||||
| num_answers = config.getint("general", "answers") | |||||
| answer_choices = {} # dict of {category1: {choice1, choice2...}, ...} | |||||
| questions = {} # dict of {piece1: [question1, question2...], ...} | |||||
| self.number_of_questions = 0 | |||||
| for piece in self.order: | |||||
| for category, answer_choice in config.items(piece): | |||||
| if category == "url": | |||||
| continue | |||||
| try: | |||||
| answer_choices[category].add(answer_choice) | |||||
| except KeyError: | |||||
| answer_choices[category] = set([answer_choice]) | |||||
| for piece in self.order: | |||||
| questions[piece] = dict() | |||||
| for category in config.options(piece): | |||||
| if category == "url": | |||||
| continue | |||||
| correct_choice = config.get(piece, category) | |||||
| questions[piece][category] = [correct_choice] | |||||
| all_choices = list(answer_choices[category]) | |||||
| all_choices.remove(correct_choice) | |||||
| for x in range(num_answers - 1): | |||||
| try: | |||||
| choice = random.choice(all_choices) | |||||
| except IndexError: | |||||
| break # if there aren't enough choices in the choice | |||||
| # bank, there will be fewer answer choices than | |||||
| # we want, but what else can we do? | |||||
| all_choices.remove(choice) | |||||
| questions[piece][category].append(choice) | |||||
| question_choices = questions[piece][category] | |||||
| questions[piece][category] = randomize_list(question_choices) | |||||
| self.number_of_questions += 1 | |||||
| self.questions = questions | |||||
| def grid_questions(self): | |||||
| self.answers = {} | |||||
| self.stuff_to_disable = [] # what gets turned off when we press submit? | |||||
| question_grid = Frame(self.frame) | |||||
| this_row_number = 0 | |||||
| excerpt = "A" | |||||
| for piece in self.order: | |||||
| question_row_number = 1 | |||||
| piece_questions = self.questions[piece].keys() | |||||
| piece_questions.reverse() # correct ordering issues | |||||
| self.answers[piece] = {} | |||||
| height = question_height * len(piece_questions) + 20 | |||||
| piece_grid = Frame(question_grid, width=master_width, | |||||
| height=height) | |||||
| title = Label(piece_grid, text="Excerpt {0}".format(excerpt), | |||||
| font=Font(family="Verdana", size=10, weight="bold")) | |||||
| excerpt = chr(ord(excerpt) + 1) # increment excerpt by 1 letter | |||||
| title.grid(row=0, column=0, columnspan=3) | |||||
| for question in piece_questions: | |||||
| agrid = LabelFrame(piece_grid, text=question.capitalize(), | |||||
| width=master_width, height=question_height) | |||||
| a = StringVar() | |||||
| self.answers[piece][question] = a | |||||
| w = (master_width / 2) - 4 | |||||
| lhgrid = Frame(agrid, width=w, height=question_height) | |||||
| rhgrid = Frame(agrid, width=w, height=question_height) | |||||
| this_row = 0 | |||||
| left_side = True | |||||
| for choice in self.questions[piece][question]: | |||||
| if left_side: | |||||
| r = Radiobutton(lhgrid, text=choice, | |||||
| value=choice, variable=a) | |||||
| left_side = False | |||||
| else: | |||||
| r = Radiobutton(rhgrid, text=choice, | |||||
| value=choice, variable=a) | |||||
| left_side = True | |||||
| this_row += 1 | |||||
| r.grid(row=this_row, column=0, sticky=W) | |||||
| self.stuff_to_disable.append(r) | |||||
| lhgrid.grid_propagate(0) | |||||
| lhgrid.grid(row=0, column=1) | |||||
| rhgrid.grid_propagate(0) | |||||
| rhgrid.grid(row=0, column=2) | |||||
| agrid.grid_propagate(0) | |||||
| agrid.grid(row=question_row_number, column=0) | |||||
| question_row_number += 1 | |||||
| piece_grid.grid_propagate(0) | |||||
| piece_grid.grid(row=this_row_number, column=0) | |||||
| this_row_number += 1 | |||||
| question_grid.grid(row=0, column=0) | |||||
| def play(self): | |||||
| self.play_button.configure(state=DISABLED) | |||||
| thread.start_new_thread(self.play_pieces, ()) | |||||
| def play_pieces(self): | |||||
| self.excerpt_length = (config.getfloat("general", "excerpt_length") - 5) * 1000 | |||||
| break_length = config.getfloat("general", "break_length") | |||||
| cur_excerpt = "A" | |||||
| mixer.init() | |||||
| for piece in self.order: | |||||
| try: | |||||
| self.playing.set("Now Playing: Excerpt {0}".format(cur_excerpt)) | |||||
| before = time.time() | |||||
| self.play_piece(piece) | |||||
| after = time.time() | |||||
| retries = 1 | |||||
| while after - before < 3 or retries >= 100: # if the piece | |||||
| before = time.time() # played for less than 3 seconds, | |||||
| self.play_piece(piece) # assume something went wrong | |||||
| after = time.time() # loading and try to replay it, | |||||
| retries += 1 # but don't get stuck in a loop if | |||||
| # we legitimately can't play it | |||||
| self.playing.set("That was Excerpt {0}...".format(cur_excerpt)) | |||||
| cur_excerpt = chr(ord(cur_excerpt) + 1) | |||||
| time.sleep(break_length) | |||||
| except error: # Someone quit our mixer? STOP EVERYTHING. | |||||
| break | |||||
| self.playing.set("Finished playing.") | |||||
| def play_piece(self, piece): | |||||
| mixer.music.load(os.path.join(piece_dir, piece)) | |||||
| mixer.music.play() | |||||
| fadeout_enabled = False | |||||
| while mixer.music.get_busy(): | |||||
| if mixer.music.get_pos() >= self.excerpt_length: | |||||
| if not fadeout_enabled: | |||||
| mixer.music.fadeout(5000) | |||||
| fadeout_enabled = True | |||||
| time.sleep(1) | |||||
| def submit(self): | |||||
| self.submit_button.configure(state=DISABLED) | |||||
| self.play_button.configure(state=DISABLED) | |||||
| for item in self.stuff_to_disable: | |||||
| item.configure(state=DISABLED) | |||||
| try: | |||||
| mixer.quit() | |||||
| except error: # pygame.error | |||||
| pass # music was never played, so we can't stop it | |||||
| right = 0 | |||||
| wrong = [] | |||||
| excerpt = "A" | |||||
| for piece in self.order: | |||||
| questions = self.questions[piece].keys() | |||||
| questions.reverse() # correct question ordering | |||||
| for question in questions: | |||||
| correct_answer = config.get(piece, question) | |||||
| given_answer = self.answers[piece][question].get() | |||||
| if given_answer == u"Der Erlk\xf6nig": # unicode bugfix | |||||
| given_answer = "Der Erlk\xc3\xb6nig" | |||||
| if correct_answer == given_answer: | |||||
| right += 1 | |||||
| else: | |||||
| wrong.append((excerpt, config.get(piece, "title"), | |||||
| question, given_answer, correct_answer)) | |||||
| excerpt = chr(ord(excerpt) + 1) | |||||
| results = Toplevel() # make a new window to display results | |||||
| results.title("Results") | |||||
| noq = self.number_of_questions | |||||
| text = "{0} of {1} answered correctly ({2}%):".format(right, noq, | |||||
| round((right / noq) * 100, 2)) | |||||
| if right == noq: | |||||
| text += "\n\nCongratulations, you got everything right!" | |||||
| else: | |||||
| text += "\n" | |||||
| for excerpt, title, question, given_answer, correct_answer in wrong: | |||||
| if not given_answer: | |||||
| if question == "title": | |||||
| text += "\nYou left the title of Excerpt {0} blank; it's \"{1}\".".format( | |||||
| excerpt, correct_answer) | |||||
| else: | |||||
| text += "\nYou left the {0} of \"{1}\" blank; it's {2}.".format( | |||||
| question, title, correct_answer) | |||||
| elif question == "title": | |||||
| text += "\nExcerpt {0} was {1}, not {2}.".format( | |||||
| excerpt, correct_answer, given_answer) | |||||
| else: | |||||
| text += "\nThe {0} of \"{1}\" is {2}, not {3}.".format( | |||||
| question, title, correct_answer, given_answer) | |||||
| label = Label(results, text=text, justify=LEFT, padx=15, pady=10, | |||||
| font=Font(family="Verdana", size=8)) | |||||
| label.pack() | |||||
| def randomize_list(old): | |||||
| new = [] | |||||
| while old: | |||||
| obj = random.choice(old) | |||||
| new.append(obj) | |||||
| old.remove(obj) | |||||
| return new | |||||
| def generate_piece_order(): | |||||
| pieces = config.sections() | |||||
| pieces.remove("general") # remove config section that is not a piece | |||||
| return randomize_list(pieces) | |||||
| def load_config(): | |||||
| global config, piece_dir | |||||
| config = configparser.SafeConfigParser() | |||||
| config.read(config_filename) | |||||
| if not config.has_section("general"): | |||||
| exit("Your config file is missing or malformed.") | |||||
| piece_dir = os.path.abspath(config.get("general", "piece_dir")) | |||||
| def get_missing_pieces(root): | |||||
| pieces = config.sections() | |||||
| pieces.remove("general") | |||||
| missing_pieces = [] | |||||
| for piece in pieces: | |||||
| if not os.path.exists(os.path.join(piece_dir, piece)): | |||||
| missing_pieces.append(piece) | |||||
| if missing_pieces: | |||||
| window = Toplevel() | |||||
| window.title("PyQuizzer") | |||||
| window.protocol("WM_DELETE_WINDOW", root.destroy) | |||||
| status = StringVar() | |||||
| status.set("I'm missing {0} music ".format(len(missing_pieces)) + | |||||
| "pieces;\nwould you like me to download them for you now?") | |||||
| head_label = Label(window, text="Download Music Pieces", font=Font( | |||||
| family="Verdana", size=10, weight="bold")) | |||||
| head_label.grid(row=0, column=0, columnspan=2) | |||||
| status_label = Label(window, textvar=status, justify=LEFT, padx=15, | |||||
| pady=10) | |||||
| status_label.grid(row=1, column=0, columnspan=2) | |||||
| quit_button = Button(window, text="Quit", command=lambda: exit()) | |||||
| quit_button.grid(row=2, column=0) | |||||
| dl_button = Button(window, text="Download", | |||||
| command=lambda: do_pieces_download(missing_pieces, status, | |||||
| dl_button, status_label, window)) | |||||
| dl_button.grid(row=2, column=1) | |||||
| window.mainloop() | |||||
| else: | |||||
| global download_complete | |||||
| download_complete = True | |||||
| def do_pieces_download(pieces, status, dl_button, status_label, window): | |||||
| global download_complete | |||||
| dl_button.configure(state=DISABLED) | |||||
| counter = 1 | |||||
| for piece in pieces: | |||||
| url = config.get("general", "base_url") + config.get(piece, "url") | |||||
| name = "{0} of {1}: {2}".format(counter, len(pieces), | |||||
| config.get(piece, "title")) | |||||
| urlretrieve(url, os.path.join(piece_dir, piece), | |||||
| lambda x, y, z: progress(x, y, z, name, status, status_label)) | |||||
| counter += 1 | |||||
| window.quit() | |||||
| window.withdraw() | |||||
| download_complete = True | |||||
| def progress(count, block_size, total_size, name, status, label): | |||||
| percent = int(count * block_size * 100 / total_size) | |||||
| status.set("Downloading pieces...\n" + name + ": %2d%%" % percent) | |||||
| label.update_idletasks() | |||||
| def run(): | |||||
| root = Tk() | |||||
| root.withdraw() | |||||
| load_config() | |||||
| get_missing_pieces(root) | |||||
| while not download_complete: | |||||
| time.sleep(0.5) | |||||
| window = Toplevel() | |||||
| window.title("MusicQuizzer") | |||||
| answer_sheet = AnswerSheet(window) | |||||
| answer_sheet.canvas.create_window(0, 0, anchor=NW, | |||||
| window=answer_sheet.frame) | |||||
| answer_sheet.frame.update_idletasks() | |||||
| answer_sheet.canvas.config(scrollregion=answer_sheet.canvas.bbox("all")) | |||||
| window.protocol("WM_DELETE_WINDOW", root.destroy) # make the 'x' in the | |||||
| # corner quit the entire program, not just this window | |||||
| window.mainloop() | |||||
| if __name__ == "__main__": | |||||
| run() | |||||