Browse Source

Merge branch 'develop'

tags/v0.1
Ben Kurtovic 12 years ago
parent
commit
0fabe78ef3
6 changed files with 724 additions and 0 deletions
  1. +2
    -0
      .gitignore
  2. +19
    -0
      LICENSE
  3. +86
    -0
      README.md
  4. +205
    -0
      config.cfg
  5. +2
    -0
      mac_osx.sh
  6. +410
    -0
      musicquizzer.py

+ 2
- 0
.gitignore View File

@@ -0,0 +1,2 @@
# For the love of God, don't track the actual music piece files themselves.
pieces/

+ 19
- 0
LICENSE View File

@@ -0,0 +1,19 @@
Copyright (c) 2011 by Ben Kurtovic <ben.kurtovic@verizon.net>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

+ 86
- 0
README.md View File

@@ -0,0 +1,86 @@
__MusicQuizzer__ (_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.

# Installation

## Mac OS X

Get [MacPorts](http://www.macports.org/install.php), if you don't have it.

From Terminal (`/Applications/Utiliies/Terminal`), do:

sudo port install python26
sudo port install py26-game

Next,
[download MusicQuizzer](https://github.com/earwig/music-quizzer/tarball/v0.1)
and uncompress it. Move the folder wherever you want (keep its contents
intact!) and double-click on `mac_osx.sh` to use the quizzer.

## Windows

MusicQuizzer is written in Python, a language that does not come with Windows
by default. Download the latest version of Python 2.7.x
[here](http://python.org/download/) (_not_ Python 3). Use the default settings
during installation.

Next, download and install pygame from
[here](http://pygame.org/ftp/pygame-1.9.2a0.win32-py2.7.msi).

Finally,
[download MusicQuizzer](https://github.com/earwig/music-quizzer/zipball/v0.1)
and extract it wherever you want. To use, simply double-click on the
`musicquizzer` file inside (do not move or delete any of the other files).

## Linux (with apt-get)

You should be on at least Python 2.7 (check with `python --version`), assuming
you keep your operating system up-to-date. Install the latest versions of
pygame and tk with:

sudo apt-get install python-pygame python-tk

Then,
[download MusicQuizzer](https://github.com/earwig/music-quizzer/tarball/v0.1)
and execute the program with `python musicquizzer.py` from your terminal.

# Usage

The first time you start the program, it will download all of the 25 necessary
(default) music pieces to the `pieces` folder. This is a ~70 MB download.

MusicQuizzer will present you with an answer sheet, containing four or five
multiple choice questions per piece (which are, of course, randomized every
time you begin a new quiz). Press `Start Quiz` to begin listening to the
excerpts. Each one is played for 30 seconds. You are then given five seconds of
rest, followed by the next piece. After all excerpts have been played, you
_cannot_ re-listen to them. Press `Submit Answers` to "hand in" your quiz and
view the results.

# Modifying

The music pieces are located in `pieces/`, in `.mp3` format. The file
`config.cfg` contains the information for each excerpt, like so:
[10.mp3]
title: Der Erlkönig
composer: Franz Schubert
era: Romantic
genre: Lied
form: Through-composed
url: http://stuy.enschool.org/music/10_The_Erlking_Erlkonig.mp3

...and so-on. The section's header is the name of the file in `pieces/` (or
whatever directory you have chosen), and the fields hold the information that
MusicQuizzer will use to generate questions. The exception is the `url` field,
which is the _direct_ URL that MusicQuizzer will use to download the piece if
it does not have a file with that name.

Feel free to rename any of the pieces, delete them, add totally new ones, or
change their information. This program is designed to be customizable.

In the config file, you can also change the length of time each excerpt is
played for, the time between each excerpt, and other things. If an attribute is
not defined for a certain piece, the quizzer will not ask the question in that
excerpt, but the question will remain for other pieces.

+ 205
- 0
config.cfg View File

@@ -0,0 +1,205 @@
; This file contains some configuration variables for MusicQuizzer that you can
; change if desired. Comments begin with a ; character. This file must be
; included with MusicQuizzer in order for it to work correctly.

[general]
; Length of time each excerpt is played for. Longer ones will be cut short, and
; shorter ones will be played until their end:
excerpt_length: 30

; Length of the silent break in between pieces:
break_length: 5
; Number of answer choices per question:
answers: 6

; Directory where pieces are stored, relative to the script or absolute:
piece_dir: pieces/

; Website to get music pieces from:
base_url: http://stuy.enschool.org

; Data for each piece follows. Section name is the (full, with extension)
; filename of the piece in pieces/.

[01.mp3]
title: Toccata and Fugue in D minor
composer: Johann Sebastian Bach
era: Baroque
genre: Fugue
url: /music/1_Toccata_Fugue_In_D_Minor.mp3

[02.mp3]
title: Brandenburg Concerto No. 2
composer: Johann Sebastian Bach
era: Baroque
genre: Concerto grosso
url: /music/2_Brandenburg_Concerto_2.mp3

[03.mp3]
title: Hornpipe from Water Music Suite
composer: George Frideric Handel
era: Baroque
genre: Suite
url: /music/3_Hornpipe_From_Water_Music_Suite.mp3

[04.mp3]
title: Spring from The Four Seasons
composer: Antonio Vivaldi
era: Baroque
genre: Concerto
url: /music/4_Spring_From_Four_Seasons_Movement_1.mp3

[05.mp3]
title: Trumpet Concerto in E-flat major
composer: Joseph Haydn
era: Classical
genre: Concerto
url: /music/5_Trumpet_Concerto_In_E_Flat_Major_Movement_3.mp3

[06.mp3]
title: Piano Concerto No. 20
composer: Wolfgang Amadeus Mozart
era: Classical
genre: Concerto
url: /music/6_Piano_Concerto_20_In_D_Minor_Movement_1.mp3

[07.mp3]
title: Symphony No. 40
composer: Wolfgang Amadeus Mozart
era: Classical
genre: Symphony
url: /music/7_Symphony_40_In_G_Minor_Movement_1.mp3

[08.mp3]
title: Symphony No. 5
composer: Ludwig van Beethoven
era: Transitional
genre: Symphony
url: /music/8_Symphony_5_In_C_Minor_Fate_Movement_1.mp3

[09.mp3]
title: Symphony No. 9
composer: Ludwig van Beethoven
era: Transitional
genre: Choral symphony
url: /music/9_Symphony_9_In_D_Minor_Choral_With_Verses_From_Ode_To_Joy_Movement_4.mp3

[10.mp3]
title: Der Erlkönig
composer: Franz Schubert
era: Romantic
genre: Lied
form: Through-composed
url: /music/10_The_Erlking_Erlkonig.mp3

[11.mp3]
title: Symphony No. 8: Unfinished
composer: Franz Schubert
era: Romantic
genre: Symphony
form: Sonata-allegro
url: /music/11_Symphony_8_Unfinished.mp3

[12.mp3]
title: Symphony No. 3
composer: Johannes Brahms
era: Romantic
genre: Symphony
form: Ternary
url: /music/12_Symphony_3_Movement_1.mp3

[13.mp3]
title: Violin Concerto in E minor
composer: Felix Mendelssohn
era: Romantic
genre: Concerto
form: Sonata-allegro
url: /music/13_Violin_Concerto_Movement_1.mp3

[14.mp3]
title: Symphonie Fantastique
composer: Hector Berlioz
era: Romantic
genre: Program symphony
url: /music/14_Symphonie_Fantastique_Movement_4.mp3

[15.mp3]
title: The Moldau
composer: Bedrich Smetana
era: Romantic
genre: Symphonic poem
form: Episodic
url: /music/15_The_Moldau.mp3

[16.mp3]
title: 1812 Overture
composer: Piotr Tchaikovsky
era: Romantic
genre: Concert overture
form: Sonata-allegro
url: /music/16_Overture_1812.mp3

[17.mp3]
title: Revolutionary Etude
composer: Frederic Chopin
era: Romantic
genre: Etude
url: /music/17_Revolutionary_Etude.mp3

[18.mp3]
title: Dies Irae from Requiem
composer: Giuseppe Verdi
era: Romantic
genre: Requiem
url: /music/18_Requiem_Dies_Irae.mp3

[19.mp3]
title: Ride of the Valkyries
composer: Richard Wagner
era: Romantic
genre: Opera
url: /music/19_Ride_Of_The_Valkyries.mp3

[20.mp3]
title: Vissi d'arte from Tosca
composer: Giaccomo Puccini
era: Romantic
genre: Opera
url: /music/20_Vissi_Darte_From_Tosca.mp3

[21.mp3]
title: Prelude to the Afternoon of a Faun
composer: Claude Debussy
era: Impressionism
genre: Symphonic poem
url: /music/21_Prelude_To_The_Afternoon_Of_A_Faun.mp3

[22.mp3]
title: Rite of Spring
composer: Igor Stravinsky
era: 20th Century
genre: Ballet
url: /music/22_Rite_Of_Spring.mp3

[23.mp3]
title: Hoedown from Rodeo
composer: Aaron Copland
era: 20th Century
genre: Ballet
form: Ternary
url: /music/23_Hoedown_From_Rodeo.mp3

[24.mp3]
title: Adagio for Strings
composer: Samuel Barber
era: 20th Century
form: Through-composed
url: /music/24_Adagio_For_Strings.mp3

[25.mp3]
title: Short Ride in a Fast Machine
composer: John Adams
era: 20th Century
genre: Minimalism
url: /music/25_Short_Ride_In_A_Fast_Machine.mp3

+ 2
- 0
mac_osx.sh View File

@@ -0,0 +1,2 @@
#! /bin/bash
python2.6 ./musicquizzer.py

+ 410
- 0
musicquizzer.py View File

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

Loading…
Cancel
Save