| @@ -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() | |||