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