From 7e95cbc0d86729b43899a0c798b9ca78bebb3d18 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Thu, 9 Jun 2011 18:58:22 -0400 Subject: [PATCH] initial commit of main script --- musicquizzer.py | 410 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 410 insertions(+) create mode 100644 musicquizzer.py diff --git a/musicquizzer.py b/musicquizzer.py new file mode 100644 index 0000000..bb42968 --- /dev/null +++ b/musicquizzer.py @@ -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()