#! /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.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) if not os.path.exists(piece_dir): os.mkdir(piece_dir) 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()