commit:60b81d82978fcc83379e373d89c824987fb18760
author:Chip Black
committer:Chip Black
date:Sun Jun 8 04:16:20 2008 -0500
parents:
Initial commit from version 1e-99
diff --git a/COPYING b/COPYING
line changes: +347/-0
index 0000000..2b940a4
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,347 @@
+		    GNU GENERAL PUBLIC LICENSE
+		       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+     59 Temple Place, Suite 330, Boston, MA 02111-1307, USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+The Free Software Foundation has exempted Bash from the requirement of
+Paragraph 2c of the General Public License.  This is to say, there is
+no requirement for Bash to print a notice when it is started
+interactively in the usual way.  We made this exception because users
+and standards expect shells not to print such messages.  This
+exception applies to any program that serves as a shell and that is
+based primarily on Bash as opposed to other GNU software.
+
+			    Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+		    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+			    NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+		     END OF TERMS AND CONDITIONS
+
+	Appendix: How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) 19yy  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) 19yy name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.

diff --git a/README.txt b/README.txt
line changes: +73/-0
index 0000000..492a2a9
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,73 @@
+beatscape - the red-headed stepchild of all vertical column rhythm games
+
+beatscape is a simulator for vertical column rhythm games. Right now, it
+simulates 7-key beatmania IIDX (and, by induction, 5-key beatmania), but
+plans to support the whole line from Pop'n Music to Keyboardmania. It
+does *not* simulate any arrow games (DDR, ITG, Pump it Up, etc). If you
+want that, go use StepMania. Right now it reads BMS/BME (pretty well)
+and StepMania SM (poorly).
+
+beatscape requires:
+	pygame of a recent vintage (I'm using 1.7.1)
+	Python 2.4
+	a little bit of a clue
+
+USAGE
+
+Add your songs to the 'songs' folder. There is no restriction on the
+placement of files, other than that all files referenced by a keyfile
+(WAV,BMP,etc) must be in the same folder as that keyfile. I like to
+arrange my keyfiles logically by source, but you can do whatever you
+want.
+
+beatscape does not automatically index your music files (yet), so you
+will have to do this manually each time you add or remove songs by
+running 'mksongcache.py'.
+
+beatscape accepts a command line argument specifying a keyfile to play.
+This is handy if you're testing a keyfile and don't want to navigate the
+menus.
+
+FILES
+
+config.py
+	This lists configuration variables for various aspects of the
+	game, such as timings and button assignments. Timings are set to
+	what I think is a reasonable approximation of the console
+	versions of IIDX, and button assignments should work with your
+	standard IIDX controller hooked up via a PSX -> USB adapter.
+	Other controllers and adapters will likely require tweaking.
+
+There is not (as yet) any per-user configuration or way to install
+beatscape system-wide. Feel free to contribute. :)
+
+BUGS
+
+Oh, god, too many to mention, but these are extremely noticable:
+* bpm-change problems on some songs (notably both era mixes)
+* sometimes a song will just decide not to work after you've selected
+  it. If you select it again, it will probably work.
+* A lot of undocumented features of the BMS/BME format are still
+  unhandled, and will likely cause errors or crashes. If anyone can
+  point me to a definitive BMS/BME reference, I'd be much obliged!
+* Sometimes samples will just fail to play. I'm not yet sure if this is
+  a fault of my engine or of pygame.
+* There's no way to adjust any game options, especially speed (without
+  hacking the source).
+* It's visually abominable.
+
+LICENSE
+
+beatscape is released under the GPLv2. Details can be found in the COPYING file.
+
+OTHER
+
+beatscape might run on windows. I've only tested this a little bit, and it does
+run, but the sound is usually lagged and choppy.
+
+CONTACT
+
+I can be contacted at bytex64@bytex64.net if you have any bug reports,
+questions, fixes, or other contributions.
+
+Thanks, and have fun!

diff --git a/accuracygraph.py b/accuracygraph.py
line changes: +36/-0
index 0000000..0de687e
--- /dev/null
+++ b/accuracygraph.py
@@ -0,0 +1,36 @@
+import pygame
+from bmevent import *
+from keyhandler import *
+
+class AccuracyGraph:
+	linecolor = (255,255,0)
+	tickcolor = (200,200,200)
+	accpoints = [-90,-60,-30,-15,0,15,30,60,90]
+	
+	def __init__(self,rect):
+		self.screen = pygame.display.get_surface()
+		self.disprect = rect
+		self.datapoints = [0]
+		self.ratio = rect.height / 100.0
+		self.tickpoints = map(lambda y: (y * self.ratio) + (rect.height/2), self.accpoints)
+		self.n_hits = 0
+		self.total = 0
+
+	def addpoint(self,point):
+		self.datapoints[0] = point
+		self.total += point
+		self.n_hits += 1
+		return self.total / float(self.n_hits)
+
+	def draw(self):
+		self.screen.fill((0,0,0), self.disprect)
+		for t in self.tickpoints:
+			y = self.disprect.top + t
+			pygame.draw.line(self.screen, self.tickcolor,
+				(self.disprect.left, y), (self.disprect.right-1, y))
+		if abs(self.datapoints[0]) > 100:
+			return
+		y = self.disprect.height / 2 + self.disprect.top + (self.datapoints[0] * self.ratio)
+		pygame.draw.line(self.screen, self.linecolor,
+			(self.disprect.left, y), (self.disprect.right, y),2)
+

diff --git a/beatscape.py b/beatscape.py
line changes: +67/-0
index 0000000..c2f84e0
--- /dev/null
+++ b/beatscape.py
@@ -0,0 +1,67 @@
+#!/usr/bin/python
+# beatscape is the mother of all musical button games.
+
+import sys
+import pygame
+from keygraph import *
+from keyfile import *
+from bmevent import *
+from chanman import *
+from formats import *
+from keyhandler import *
+import game
+
+try:
+	import config
+except ImportError:
+	print "Could not parse config.py. Maybe you forgot a semicolon? :-P"
+	exit(1)
+
+song = ''
+
+if len(sys.argv) > 1:
+	song = sys.argv[1]
+
+try:
+	import psyco
+	print "w00t! going psyco!"
+	psyco.full()
+except ImportError:
+	pass
+
+pygame.mixer.pre_init(44100, -16, True, 1)
+pygame.init()
+opts = pygame.FULLSCREEN | pygame.DOUBLEBUF | pygame.HWSURFACE
+pygame.display.set_mode((640,480), opts)
+screen = pygame.display.get_surface()
+
+for n in range(0,pygame.joystick.get_count()):
+	pygame.joystick.Joystick(n).init();
+
+if song:
+	# For now, assume IIDX. We'll make this smarter later.
+	game.playgame(gametypes['IIDX'],song)
+	exit(0)
+
+from mainmenu import *
+from fileselect import *
+
+l = ['mainmenu']
+
+while 1:
+	obj = None
+
+	if l[0] == 'mainmenu':
+		obj = MainMenu()
+	elif l[0] == 'fileselect':
+		obj = FileSelect()
+	elif l[0] == 'play':
+		obj = game.Game(l[1])
+	elif l[0] == 'quit':
+		sys.exit(0)
+
+	l = obj.run()
+	# Flush events before switching to the next mode
+	pygame.event.clear()
+
+pygame.quit()

diff --git a/bmevent.py b/bmevent.py
line changes: +36/-0
index 0000000..b6cc994
--- /dev/null
+++ b/bmevent.py
@@ -0,0 +1,36 @@
+BME_BGM = 0x01
+BME_TEMPO = 0x02
+BME_BGA = 0x04
+BME_CHPOOR = 0x08
+BME_NOTE1 = 0x10
+BME_NOTE2 = 0x20
+BME_HIT = 0x40
+BME_STOP = 0x80
+BME_LONGMEASURE = 0x100
+
+class BMEvent:
+	def __init__(self,beat,type,key=None,dataref=None):
+		self.beat = beat
+		self.type = type
+		self.key = key
+		self.dataref = dataref
+
+	def __str__(self):
+		s = "BMEvent:\n"
+		s += "   beat=" + str(self.beat) + "\n"
+		t = { BME_BGM:		"BGM",
+		      BME_TEMPO:	"TEMPO",
+		      BME_BGA:		"BGA",
+		      BME_CHPOOR:	"CHANGE POOR",
+		      BME_NOTE1:	"NOTE P1",
+		      BME_NOTE2:	"NOTE P2"
+		    }[self.type]
+		s += "   type=" + t + "\n"
+		if self.key:
+			s += "   key=" + str(self.key) + "\n"
+		if self.dataref:
+			try:
+				s += "   dataref=" + hex(self.dataref) + "\n"
+			except TypeError:
+				s += "   dataref=" + str(self.dataref) + "\n"
+		return s

diff --git a/chanman.py b/chanman.py
line changes: +17/-0
index 0000000..1323eac
--- /dev/null
+++ b/chanman.py
@@ -0,0 +1,17 @@
+from bmevent import *
+import pygame
+
+class ChannelManager:
+	keyfile = None
+	chanmap = None
+
+	def __init__(self,keyfile):
+		self.keyfile = keyfile
+		self.chanmap = [pygame.mixer.Channel(n) for n in range(0,self.keyfile.numkeys)]
+		pygame.mixer.set_num_channels(64)
+
+	def play(self,bme):
+		if bme.type & (BME_NOTE1 | BME_NOTE2 | BME_BGM | BME_HIT):
+			if bme.dataref > 0 and bme.dataref in self.keyfile.wavs:
+				self.keyfile.wavs[bme.dataref].play()
+

diff --git a/config.py b/config.py
line changes: +36/-0
index 0000000..23f6dd4
--- /dev/null
+++ b/config.py
@@ -0,0 +1,36 @@
+# Configuration. Edit this file by hand, but beware, it is a python file
+# and the game won't start if it does not parse correctly.
+
+# These are modules necessary for certain constants used here
+from pygame.locals import *
+from constants import *
+
+# Main config, for global options
+librarypath = "/media/audio/BMS/library"
+keytimeout = 250	# milliseconds
+
+# Game config, for various game types
+
+bm = {
+	'format': 'IIDXFormat',
+	'keymap':
+		{K_SPACE:0, K_z:1, K_s:2, K_x:3, K_d:4, K_c:5, K_f:6, K_v:7},
+	'jsbuttonmap':
+		{ 3: 1, 6: 2, 2: 3, 7: 4, 1: 5, 4: 6 },
+	'jsaxismap':
+		{ (0,-1.0): 7, (0,0.0): 7, (1,-1.0): 0, (1,0.0): 0, (1,1.0): 0 },
+	'timings':
+		#[(15,JUDGE_FGREAT), (35,JUDGE_GREAT), (50,JUDGE_GOOD), (80,JUDGE_BAD), (100,JUDGE_POOR)]
+		[(20,JUDGE_FGREAT), (42,JUDGE_GREAT), (70,JUDGE_GOOD), (250,JUDGE_BAD), (1000,JUDGE_POOR)]
+}
+
+popn = {
+	'format': 'PopnFormat',
+}
+
+# game type mapping
+gametypes = {
+	'beatmania': bm,	# For now
+	'IIDX': bm,
+	'Popn': popn
+}

diff --git a/constants.py b/constants.py
line changes: +8/-0
index 0000000..8d84fef
--- /dev/null
+++ b/constants.py
@@ -0,0 +1,8 @@
+# Constants used everywhere
+
+JUDGE_FGREAT = 1
+JUDGE_GREAT = 2
+JUDGE_GOOD = 3
+JUDGE_BAD = 4
+JUDGE_POOR = 5
+JUDGE_NA = 6

diff --git a/event.py b/event.py
line changes: +49/-0
index 0000000..09b233c
--- /dev/null
+++ b/event.py
@@ -0,0 +1,49 @@
+import pygame
+from pygame.locals import *
+import config
+
+LEFT = 0
+RIGHT = 1
+UP = 2
+DOWN = 3
+OK = 4
+CANCEL = 5
+OPTION = 6
+
+keymap = {K_UP: UP, K_DOWN: DOWN, K_LEFT: LEFT, K_RIGHT: RIGHT, K_RETURN: OK, K_ESCAPE: CANCEL}
+jsamap = {(0,-1.0): OK, (1,-1.0): DOWN, (1,1.0): UP }
+jsbmap = {3: OK, 6: CANCEL, 2: OK, 7: CANCEL, 1: OK, 4: CANCEL, 10: OPTION, 9: OK}
+
+def parseevent(e):
+	try: 
+		if e.type == KEYDOWN:
+			return keymap[e.key]
+		elif e.type == JOYBUTTONDOWN:
+			return jsbmap[e.button]
+		elif e.type == JOYAXISMOTION:
+			if e.value != 0.0:
+				return jsamap[(e.axis,e.value)]
+		else:
+			return None
+	except KeyError:
+		return None
+
+last_action = None
+last_action_time = 0
+
+def getaction():
+	global last_action, last_action_time
+
+	e = pygame.event.poll()
+	if e.type != NOEVENT:
+		last_action = parseevent(e)
+		if last_action:
+			last_action_time = pygame.time.get_ticks()
+		return last_action
+	else:
+		if pygame.time.get_ticks() - last_action_time > config.keytimeout:
+			last_action_time = pygame.time.get_ticks()
+			# Only repeat for directionals
+			if last_action in (LEFT,RIGHT,UP,DOWN):
+				return last_action
+	return None

diff --git a/fileselect.py b/fileselect.py
line changes: +296/-0
index 0000000..a21997a
--- /dev/null
+++ b/fileselect.py
@@ -0,0 +1,296 @@
+import pygame
+import cPickle
+import os, os.path, re, math
+
+from keyfile import *
+from fx import *
+import event
+
+fileroot = "songs"
+songcache = 'songcache.pickle'
+possave = 0
+
+def mksongcache():
+	songs = FileNode()
+	songs.crawl(fileroot)
+	f = open(songcache,'w')
+	cPickle.dump(songs,f)
+	f.close()
+	return songs
+
+filetile = pygame.image.load("gfx/filetile.png")
+
+class SelectWidget(FX):
+	color = (240,240,255)
+	basecolor = (200,0,0)
+	fadetime = 700
+	size = (400,30)
+
+	OFF = 0
+	ON = 1
+	FADING = 2
+
+	def __init__(self,text,location=(0,0)):
+		FX.__init__(self)
+		f = pygame.font.Font('font/Nano.ttf',16)
+		self.text = f.render(text,True,self.color)
+		self.surface = pygame.Surface(self.size).convert_alpha()
+		self.surface.fill( (0,0,0,0) )
+		self.surface.blit(filetile,(0,0))
+		self.surface.blit(self.text, (9,7))
+		self.pointlist = [(10,0),(self.size[0]-2,0),(self.size[0]-2,self.size[1]-2),(0,self.size[1]-2),(0,10)]
+		self.state = self.OFF
+		self.location = location
+
+	def off(self):
+		if self.state == self.ON:
+			self.state = self.FADING
+			self.fade_t = pygame.time.get_ticks()
+
+	def on(self):
+		self.state = self.ON
+
+	def draw(self,t):
+		self.screen.blit(self.surface,self.location)
+		color = self.basecolor
+		if self.state == self.ON:
+			color = self.color
+		if self.state == self.FADING:
+			dt = pygame.time.get_ticks() - self.fade_t
+			if dt < self.fadetime:
+				alpha = math.exp(-0.003*dt)
+				color = [(1.0-alpha) * self.basecolor[n] + alpha * self.color[n] for n in range(0,3)]
+			else:
+				self.state = self.OFF
+		pointlist = [(x[0] + self.location[0],x[1] + self.location[1]) for x in self.pointlist]
+		pygame.draw.lines(self.screen,color,True,pointlist,2)
+
+# This would look a lot less ugly if only there were something like
+# perl's cmp and <=> operators.
+# FUCK YOU PYTHON WHY DOES THIS HAVE TO BE SO COMPLICATED!?!?!?!
+def WTFsort(a,b):
+	if a.type == a.DIRECTORY and b.type == b.DIRECTORY:
+		if a.name > b.name:
+			return 1
+		elif a.name < b.name:
+			return -1
+		else:
+			return 0
+	elif a.type == a.DIRECTORY and b.type == b.FILE:
+		return 1
+	elif a.type == a.FILE and b.type == b.DIRECTORY:
+		return -1
+	else:
+		if a.info['player'] > b.info['player']:
+			return 1
+		elif a.info['player'] < b.info['player']:
+			return -1
+		else:
+			if not 'playlevel' in a.info:
+				return -1
+			if not 'playlevel' in b.info:
+				return 1
+			if a.info['playlevel'] > b.info['playlevel']:
+				return 1
+			elif a.info['playlevel'] < b.info['playlevel']:
+				return -1
+			else:
+				if not 'total' in a.info:
+					return -1
+				if not 'total' in b.info:
+					return 1
+				if a.info['total'] > b.info['total']:
+					return 1
+				elif a.info['total'] < b.info['total']:
+					return -1
+				else:
+					return 0
+
+filematch = re.compile('\.(bms|bme|sm)$',re.I)
+
+class FileNode:
+	DIRECTORY = 0
+	FILE = 1
+
+	def __init__(self):
+		self.children = []
+		self.open = False
+		self.info = None
+		self.widget = None
+
+	def crawl(self,path):
+		print "Crawling " + path
+		self.path = path
+		if os.path.isdir(path):
+			self.type = self.DIRECTORY
+			self.name = os.path.basename(path)
+			files = filter(lambda x: os.path.isdir(os.path.join(path,x)) or filematch.search(x), os.listdir(path))
+			for f in files:
+				c = FileNode()
+				if c.crawl(os.path.join(path,f)):
+					self.children.append(c)
+			self.children.sort(WTFsort)
+		elif os.path.isfile(path):
+			self.type = self.FILE
+			self.info = kf_info(path)
+			self.name = self.info['title']
+		else:
+			print path + " isn't a file or a directory. Ignoring."
+			return False
+		return True
+
+	def update(self):
+		for c in self.children:
+			if not os.path.exists(c.path):
+				self.children.remove(c)
+			else:
+				c.update()
+
+	def add(self,child):
+		self.children.append(child)
+
+	def tolist(self,depth=0):
+		self.depth = depth
+		l = [self]
+		if self.open:
+			for c in self.children:
+				r = c.tolist(depth+1)
+				for i in r:
+					l.append(i)
+		return l
+
+	def getwidget(self):
+		if not self.widget:
+			self.widget = SelectWidget(self.name)
+		return self.widget
+
+	def nukewidgets(self):
+		self.widget = None
+		for c in self.children:
+			c.nukewidgets()
+
+class FileSelect:
+
+	def __init__(self):
+		self.screen = pygame.display.get_surface()
+		try:
+			f = open(songcache,'r')
+			self.songs = cPickle.load(f)
+			f.close()
+		except IOError:
+			self.songs = mksongcache()
+		except cPickle.UnpicklingError:
+			self.songs = mksongcache()
+
+		self.period = 428
+		self.songlist = self.songs.tolist()
+		try:
+			self.position = self.songlist.index(self.songs.current)
+		except AttributeError:
+			self.position = 0
+		except ValueError:
+			self.position = 0
+
+		self.fslabel = Text('file select','font/Nano.ttf',30,(330,30),(255,0,0))
+		self.fadebottom = pygame.Surface((640,200)).convert_alpha()
+		for n in range(0,200):
+			alpha = int((1.0 - math.exp(-n/30.0)) * 255.0)
+			color = (0,0,0,alpha)
+			pygame.draw.line(self.fadebottom,color,(0,n),(640,n))
+		self.labels = {}
+		self.olabels = {}
+		self.olabels['title'] = Text('title','font/Nano.ttf',12, (20,320),(255,130,130))
+		self.labels['title'] = Text('','font/Nano.ttf',18, (30,330),(230,230,255))
+		self.olabels['artist'] = Text('artist','font/Nano.ttf',12, (20,360),(255,130,130))
+		self.labels['artist'] = Text('','font/Nano.ttf',16, (30,370),(230,230,255))
+		self.olabels['genre'] = Text('genre','font/Nano.ttf',12, (20,390),(255,130,130))
+		self.labels['genre'] = Text('','font/Nano.ttf',16, (30,400),(230,230,255))
+		self.olabels['playlevel'] = Text('difficulty','font/Nano.ttf',12, (20,420),(255,130,130))
+		self.labels['playlevel'] = Text('','font/Nano.ttf',16, (30,430),(230,230,255))
+		self.olabels['bpm'] = Text('BPM','font/Nano.ttf',12, (20,450),(255,130,130))
+		self.labels['bpm'] = Text('','font/Nano.ttf',16, (30,460),(230,230,255))
+		self.calcsongwidgets()
+
+	def draw(self,t):
+		self.screen.fill( (30,0,0) )
+		l = len(self.songlist)
+		for nu in range(self.position-4,self.position+8):
+			n = nu % l
+			self.songlist[n].getwidget().draw(t)
+
+		self.screen.blit(self.fadebottom,(0,280))
+		for k in self.olabels.keys():
+			self.olabels[k].draw(t)
+		for k in self.labels.keys():
+			self.labels[k].draw(t)
+		self.fslabel.draw(t)
+
+	def calcsongwidgets(self):
+		l = len(self.songlist)
+		for nu in range(self.position-4,self.position+8):
+			n = nu % l
+			w = self.songlist[n].getwidget()
+			w.location = (10 + self.songlist[n].depth * 20, 150 - (self.position - nu)*30)
+			if n == self.position:
+				w.on()
+				if self.songlist[n].type == FileNode.DIRECTORY:
+					for tag in ('artist','genre','playlevel','bpm'):
+						self.labels[tag].settext('')
+					self.labels['title'].settext(self.songlist[n].name)
+				else:
+					for tag in ('title','artist','genre','bpm'):
+						self.labels[tag].settext(self.songlist[n].info[tag])
+					try:
+						playlevel = int(self.songlist[n].info['playlevel'])
+						self.labels['playlevel'].settext(("*" * playlevel) + "(" + str(playlevel) + ")")
+					except ValueError:
+						self.labels['playlevel'].settext(str(self.songlist[n].info['playlevel']))
+			else:
+				w.off()
+
+	def run(self):
+		global possave
+		decided = 0
+		pygame.mixer.music.load('snd/0x01.ogg')
+		pygame.mixer.music.set_volume(0.75)
+		pygame.mixer.music.play(-1)
+		stab = pygame.mixer.Sound('snd/stab.ogg')
+		stab.set_volume(0.25)
+		start_t = pygame.time.get_ticks()
+
+		while decided == 0:
+			t = pygame.time.get_ticks() - start_t
+			act = event.getaction()
+			while act:
+				if act == event.CANCEL:
+					return ['mainmenu']
+				elif act == event.DOWN:
+					self.position += 1
+					self.calcsongwidgets()
+				elif act == event.UP:
+					self.position -= 1
+					self.calcsongwidgets()
+				elif act == event.OK:
+					if self.songlist[self.position].type == FileNode.DIRECTORY:
+						self.songlist[self.position].open = not self.songlist[self.position].open
+						self.songlist = self.songs.tolist()
+						self.calcsongwidgets()
+					else:
+						decided = self.position
+				act = event.getaction()
+
+			self.position = self.position % len(self.songlist)
+			self.draw(t)
+			pygame.display.flip()
+
+		pygame.mixer.music.fadeout(1500)
+		stab.play()
+
+		self.songs.nukewidgets()
+		self.songs.current = self.songlist[self.position]
+		f = open(songcache,'w')
+		cPickle.dump(self.songs,f)
+		f.close()
+
+		#possave = self.position
+		return ['play',self.songlist[decided].path]

diff --git a/font/Nano.ttf b/font/Nano.ttf
line changes: +0/-0
index 0000000..0c52860
--- /dev/null
+++ b/font/Nano.ttf

diff --git a/formats.py b/formats.py
line changes: +114/-0
index 0000000..7b2d221
--- /dev/null
+++ b/formats.py
@@ -0,0 +1,114 @@
+import pygame
+from keygraph import *
+from keyhandler import *
+from rotowidget import *
+from fx import *
+from constants import *
+
+red = (255,30,30)
+offwhite = (245,245,245)
+blue = (30,30,255)
+green = (30,255,30)
+black = (0,0,0)
+darkgray = (20,20,20)
+ggray = (200,255,200)
+
+class IIDXFormat:
+	keyfile = None
+	keystyle = [(1.75,red,black,ggray), (1,offwhite,darkgray,blue),
+		    (0.8,blue,black,green), (1,offwhite,darkgray,blue),
+		    (0.8,blue,black,green), (1,offwhite,darkgray,blue),
+		    (0.8,blue,black,green), (1,offwhite,darkgray,blue)]
+
+	def __init__(self,keyfile):
+		self.keyfile = keyfile
+		self.kg = (KeyGraph(keyfile,self.keystyle,1,pygame.Rect(0,0,192,400)),
+			   KeyGraph(keyfile,self.keystyle,2,pygame.Rect(448,0,192,400)) )
+		self.animpic = None
+		self.screen = pygame.display.get_surface()
+		self.roto = RotoWidget((30,430),25,1.0)
+		self.songlength = keyfile.length()
+		self.groove = 20.0
+		self.groovetext = GlyphText((60,415))
+		self.accuracy = 0
+		self.nacc = 0
+
+	def setproperties(self,properties):
+		if properties.has_key('judgement'):
+			j = properties['judgement']
+			if j == JUDGE_FGREAT:
+				self.groove += 1.0
+			elif j == JUDGE_GREAT:
+				self.groove += 0.7
+			elif j == JUDGE_GOOD:
+				self.groove += 0.2
+			elif j == JUDGE_BAD:
+				self.groove -= 2.0
+			elif j == JUDGE_POOR:
+				self.groove -= 6.0
+			if self.groove > 100.0:
+				self.groove = 100.0
+			elif self.groove < 0.0:
+				self.groove = 0.0
+		if 'accuracy' in properties:
+			self.accuracy += properties['accuracy']
+			self.nacc += 1
+			print "accuracy:", self.accuracy / float(self.nacc)
+		self.kg[0].setproperties(properties)
+
+	def draw(self, t, bmelist):
+		b = self.keyfile.eval_beatfunc(t)
+		for bme in bmelist:
+			if bme.type == BME_BGA and bme.dataref in self.keyfile.bmps:
+				self.animpic = self.keyfile.bmps[bme.dataref]
+		self.screen.fill( (20,20,20) )
+		for k in self.kg:
+			k.t = t
+			k.draw()
+		if self.animpic:
+	                self.screen.fill((0,0,0),(192,112,256,256))
+			self.screen.blit(self.animpic,(192,112))
+		self.roto.pulsev = self.groove
+		self.roto.arcv = (float(t) / self.songlength) * 100
+		self.roto.draw(b)
+		self.groovetext.settext("%02d" % self.groove)
+		self.groovetext.draw(t)
+		pygame.display.update()
+
+lightgray = (230,230,230)
+graybg = (30,30,30)
+yellow = (230,230,0)
+yellowbg = (30,30,0)
+green = (30,255,30)
+greenbg = (0,30,0)
+blue = (0,0,230)
+bluebg = (0,0,30)
+redbg = (30,0,0)
+
+class PopnFormat:
+	keyfile = None
+	keystyle = [(1,lightgray,graybg), (0.7,yellow,yellowbg),
+		    (1,green,greenbg), (0.7,blue,bluebg), (1,red,redbg),
+		    (0.7,blue,bluebg), (1,green,greenbg), (0.7,yellow,yellowbg),
+		    (1,lightgray,graybg)]
+
+	def __init__(self,keyfile):
+		self.keyfile = keyfile
+		self.kg = KeyGraph(keyfile,self.keystyle,1,pygame.Rect(256,0,384,400))
+		self.kg.bordercolor = black
+			  
+		self.animpic = None
+		self.screen = pygame.display.get_surface()
+
+	def draw(self, t, bmelist):
+		for bme in bmelist:
+			if bme.type == BME_BGA:
+				self.animpic = self.keyfile.bmps[bme.dataref]
+		self.screen.fill( (20,20,20) )
+		self.kg.t = t
+		self.kg.draw()
+		if self.animpic:
+	                self.screen.fill((0,0,0),(0,112,256,256))
+			self.screen.blit(self.animpic,(0,112))
+		pygame.display.update()
+

diff --git a/fx.py b/fx.py
line changes: +174/-0
index 0000000..4b608d6
--- /dev/null
+++ b/fx.py
@@ -0,0 +1,174 @@
+import pygame
+import math
+import os, os.path
+
+ALIGN_LEFT = 0
+ALIGN_RIGHT = 1
+
+# FX is the abstract base class for all "effects" objects. They have two
+# defining features:
+#
+# 1) The FX base class sets up self.screen so that you can draw to the
+#    screen. It is then important to call FX.__init__(self) in your
+#    __init__ routine.
+# 2) Each FX object has a draw(self,t) method that draws itself to the
+#    screen (possibly with appearance based on the current time).
+
+class FX:
+	def __init__(self):
+		self.screen = pygame.display.get_surface()
+
+	def draw(self,t):
+		pass
+
+class Blank(FX):
+	def draw(self,t):
+		self.screen.fill((0,0,0))
+
+class Image(FX):
+	align = ALIGN_LEFT
+
+	def __init__(self,file,location=(0,0)):
+		FX.__init__(self)
+		self.surface = pygame.image.load(file).convert_alpha()
+		self.location = location
+
+	def draw(self,t):
+		if self.align == ALIGN_RIGHT:
+			rl = (self.location[0] - self.surface.get_width(),self.location[1])
+			self.screen.blit(self.surface,rl)
+		else:
+			self.screen.blit(self.surface,self.location)
+
+class Text(Image):
+	def __init__(self,text,font,size,location=(0,0),color=(255,255,255)):
+		FX.__init__(self)
+		self.font = pygame.font.Font(font,size)
+		self.location = location
+		self.color = color
+		self.settext(text)
+
+	def settext(self,text):
+		f = self.font.render(text,True,(0,0,0))
+		self.surface = pygame.surface.Surface((f.get_width()+2,f.get_height()+2)).convert_alpha()
+		self.surface.fill((0,0,0,0))
+		self.surface.blit(f,(2,2))
+		f = self.font.render(text,True,self.color)
+		self.surface.blit(f,(0,0))
+
+textglyphs = {}
+
+class GlyphText(FX):
+	def __init__(self,location=(0,0),glyphdir='gfx/glyph'):
+		FX.__init__(self)
+		if not textglyphs:
+			l = os.listdir(glyphdir)
+			for g in l:
+				c = g.split('.')[0]
+				textglyphs[c] = pygame.image.load(os.path.join(glyphdir,g)).convert_alpha()
+		self.location = location
+		self.text = ''
+
+	def settext(self,str):
+		self.text = str
+
+	def draw(self,t):
+		(x,y) = self.location
+		for c in self.text:
+			if textglyphs.has_key(c):
+				self.screen.blit(textglyphs[c],(x,y))
+				x += textglyphs[c].get_width()
+
+class PulseLine(FX):
+	r = -0.02
+
+	def __init__(self, pointlist, color, period, maxwidth = 8, minwidth = 1):
+		FX.__init__(self)
+		self.period = period
+		self.color = color
+		if maxwidth:
+			self.maxwidth = maxwidth
+		if minwidth:
+			self.minwidth = minwidth
+		self.pointlist = pointlist
+		self.location = (0,0)
+
+	def draw(self,t):
+		w = (self.maxwidth - 1) * math.exp(self.r * (t % self.period)) + self.minwidth
+		pl = map(lambda x: (x[0] + self.location[0],x[1] + self.location[1]),self.pointlist)
+		pygame.draw.lines(self.screen,self.color, 0, pl, int(w))
+
+class ColumnPulse(FX):
+	r = -0.03
+
+	def __init__(self,rect,color):
+		FX.__init__(self)
+		self.rect = rect
+		self.color = color
+		self.st = 0
+		self.surf = []
+		t = 0
+		a = 1.0
+		while a > 0.01:
+			s = pygame.Surface(self.rect.size).convert_alpha()
+			s.lock()
+			s.fill( (0,0,0,0) )
+			for n in range(0,self.rect.height):
+				c = (self.color[0],self.color[1],self.color[2],float(n) / self.rect.height * a * 255.0)
+				w = (self.rect.width / 2) * (1.0 - a)
+				pygame.draw.line(s,c,(w,n),(self.rect.width-w,n))
+				#s.fill(c,(0,n,self.rect.width,1))
+			s.unlock()
+			self.surf.append(s)
+			t += 16
+			a = math.exp(self.r * t)
+
+		self.hit = 0
+
+	def down(self):
+		self.hit = 1
+
+	def up(self):
+		self.st = pygame.time.get_ticks()
+		self.hit = 0
+
+	def draw(self):
+		dt = pygame.time.get_ticks() - self.st
+		if self.hit:
+			self.screen.blit(self.surf[0],self.rect)
+		else:
+			n = dt / 16
+			if n > len(self.surf) - 1:
+				return
+			self.screen.blit(self.surf[n],self.rect)
+
+class PlaneScroll(FX):
+	surface = None
+
+	def __init__(self,file,direction,zoom=1.0,alpha=1.0):
+		FX.__init__(self)
+		ts = pygame.image.load(file)
+		if zoom != 1.0:
+			ts = pygame.transform.rotozoom(ts, 0.0, zoom)
+		self.surface = ts.convert()
+		del ts
+		if alpha < 1.0 and alpha >= 0.0:
+			self.surface.set_alpha(int(alpha * 255))
+		self.direction = direction
+
+	def draw(self,t):
+		w = self.surface.get_width()
+		h = self.surface.get_height()
+		location = self.surface.get_rect()
+		location.size = (640,480)
+		location.move_ip((t * self.direction[0]) % w, (t * self.direction[1]) % h)
+		self.screen.blit(self.surface,(0,0),location)
+		if location.right > w:
+			x = 640 - (location.right - w)
+			self.screen.blit(self.surface,(x,0),(0,location.top,640,480))
+		if location.bottom > h:
+			y = 480 - (location.bottom - h)
+			self.screen.blit(self.surface,(0,y),(location.left,0,640,480))
+		if location.bottom > h and location.right > w:
+			# x and y must be calculated from above two tests
+			self.screen.blit(self.surface,(x,y),(0,0,640,480))

diff --git a/game.py b/game.py
line changes: +132/-0
index 0000000..9ee7961
--- /dev/null
+++ b/game.py
@@ -0,0 +1,132 @@
+import pygame
+import thread
+
+from keygraph import *
+from keyfile import *
+from bmevent import *
+from formats import *
+from chanman import *
+from keyhandler import *
+import config
+
+events = []
+evlock = thread.allocate_lock()
+evrun = 1
+t_orig = 0
+
+def event_thread():
+	global events,evlock,t_orig
+	while evrun:
+		e = pygame.event.wait()
+		t = pygame.time.get_ticks() - t_orig
+		evlock.acquire()
+		events.append((t,e))
+		evlock.release()
+
+def event_thread_start():
+	global evrun
+	evrun = 1
+	thread.start_new_thread(event_thread,())
+
+def event_thread_stop():
+	global evrun
+	evrun = 0
+	pygame.event.post(pygame.event.Event(pygame.NOEVENT,{}))
+
+def get_events():
+	global events,evlock
+	evlock.acquire()
+	eold = events
+	events = []
+	evlock.release()
+	return eold
+
+def playgame(gameconfig,song):
+	global t_orig
+	k = kf_load(song)
+
+	dm = eval(gameconfig['format'] + '(k)');
+	cm = ChannelManager(k);
+	kh = KeyHandler(gameconfig,k);
+
+	# No background anims? No problem!
+	if not k.bmps:
+		print "IT'S PEANUT BUTTER JELLY TIME!"
+		for x in range(0,8):
+			k.bmps[x] = pygame.transform.scale(pygame.image.load("gfx/banana%d.png" % x).convert(), (256,256))
+		lastbeat = k.bmelist[-1].beat
+		i = 0.0
+		j = 0
+		while i < lastbeat:
+			k.add(BMEvent(i,BME_BGA,None,j))
+			j = (j+1) % 8
+			i += 0.25
+		k.sort()
+	
+	li = BMEListIter(k.bmelist)
+	reaper_li = BMEListIter(k.bmelist)
+	t_finish = k.length() + 5000
+
+	t_orig = pygame.time.get_ticks()
+	try:
+		if k.offset < 0:
+			t_orig -= int(k.offset * 1000)
+	except NameError:
+		pass
+	b = b_l = k.eval_beatfunc(t_orig)
+
+	event_thread_start()
+
+	animpic = None
+	while 1:
+		t = pygame.time.get_ticks() - t_orig
+		b = k.eval_beatfunc(t)
+		print t,b,b_l
+		db = b - b_l
+		b_l = b
+		li.goto(b)
+		bmelist = li.window(db)
+		dm.draw(t + 45, bmelist)
+		for bme in bmelist:
+			if bme.type == BME_BGM:
+				cm.play(bme);
+			if bme.type == BME_TEMPO:
+				bpm = bme.dataref
+				kh.setproperties({'bpm': bme.dataref})
+
+		for (t,e) in get_events():
+			r = kh.handle(e,t)
+			if r:
+				(hitbme, properties) = r
+				if hitbme:
+					cm.play(hitbme)
+				dm.setproperties(properties)
+
+			if e.type == pygame.KEYDOWN:
+				if e.key == pygame.K_ESCAPE:
+					event_thread_stop()
+					pygame.mixer.stop()
+					return False
+				elif e.key == pygame.K_PRINT:
+					pygame.image.save(screen,"screenshot.bmp")
+
+		reaper_li.goto(b - 2.0)
+		bmelist = reaper_li.window(1.0, BME_NOTE1 | BME_NOTE2)
+		for bme in bmelist:
+			dm.setproperties({'judgement': JUDGE_POOR})
+			bme.type = BME_HIT
+
+		if t > t_finish:
+			event_thread_stop()
+			return True
+
+# Hmm. This is sort of braindead, isn't it?
+class Game:
+	def __init__(self,song):
+		self.song = song
+
+	def run(self):
+		print "Playing",self.song
+		# IIDX is hardcoded for now
+		playgame(config.gametypes['IIDX'], self.song)
+		return ['fileselect']

diff --git a/gfx/bad.png b/gfx/bad.png
line changes: +0/-0
index 0000000..9d852f6
--- /dev/null
+++ b/gfx/bad.png

diff --git a/gfx/banana0.png b/gfx/banana0.png
line changes: +0/-0
index 0000000..1fc979a
--- /dev/null
+++ b/gfx/banana0.png

diff --git a/gfx/banana1.png b/gfx/banana1.png
line changes: +0/-0
index 0000000..fef450e
--- /dev/null
+++ b/gfx/banana1.png

diff --git a/gfx/banana2.png b/gfx/banana2.png
line changes: +0/-0
index 0000000..00ec334
--- /dev/null
+++ b/gfx/banana2.png

diff --git a/gfx/banana3.png b/gfx/banana3.png
line changes: +0/-0
index 0000000..59636d6
--- /dev/null
+++ b/gfx/banana3.png

diff --git a/gfx/banana4.png b/gfx/banana4.png
line changes: +0/-0
index 0000000..94c407c
--- /dev/null
+++ b/gfx/banana4.png

diff --git a/gfx/banana5.png b/gfx/banana5.png
line changes: +0/-0
index 0000000..7ca5579
--- /dev/null
+++ b/gfx/banana5.png

diff --git a/gfx/banana6.png b/gfx/banana6.png
line changes: +0/-0
index 0000000..de4c2dc
--- /dev/null
+++ b/gfx/banana6.png

diff --git a/gfx/banana7.png b/gfx/banana7.png
line changes: +0/-0
index 0000000..da45870
--- /dev/null
+++ b/gfx/banana7.png

diff --git a/gfx/chips_layer1.jpg b/gfx/chips_layer1.jpg
line changes: +0/-0
index 0000000..198d5ef
--- /dev/null
+++ b/gfx/chips_layer1.jpg

diff --git a/gfx/chips_layer2.jpg b/gfx/chips_layer2.jpg
line changes: +0/-0
index 0000000..abcd3dd
--- /dev/null
+++ b/gfx/chips_layer2.jpg

diff --git a/gfx/circle.png b/gfx/circle.png
line changes: +0/-0
index 0000000..28b0d0a
--- /dev/null
+++ b/gfx/circle.png

diff --git a/gfx/cross.png b/gfx/cross.png
line changes: +0/-0
index 0000000..5c73448
--- /dev/null
+++ b/gfx/cross.png

diff --git a/gfx/dcircle.png b/gfx/dcircle.png
line changes: +0/-0
index 0000000..70921b6
--- /dev/null
+++ b/gfx/dcircle.png

diff --git a/gfx/fgreat.png b/gfx/fgreat.png
line changes: +0/-0
index 0000000..ca8e3a3
--- /dev/null
+++ b/gfx/fgreat.png

diff --git a/gfx/filetile.png b/gfx/filetile.png
line changes: +0/-0
index 0000000..77e140c
--- /dev/null
+++ b/gfx/filetile.png

diff --git a/gfx/glyph/0.png b/gfx/glyph/0.png
line changes: +0/-0
index 0000000..993fe0d
--- /dev/null
+++ b/gfx/glyph/0.png

diff --git a/gfx/glyph/1.png b/gfx/glyph/1.png
line changes: +0/-0
index 0000000..40fb9da
--- /dev/null
+++ b/gfx/glyph/1.png

diff --git a/gfx/glyph/2.png b/gfx/glyph/2.png
line changes: +0/-0
index 0000000..e5be3d2
--- /dev/null
+++ b/gfx/glyph/2.png

diff --git a/gfx/glyph/3.png b/gfx/glyph/3.png
line changes: +0/-0
index 0000000..6b32519
--- /dev/null
+++ b/gfx/glyph/3.png

diff --git a/gfx/glyph/4.png b/gfx/glyph/4.png
line changes: +0/-0
index 0000000..e8fe6a4
--- /dev/null
+++ b/gfx/glyph/4.png

diff --git a/gfx/glyph/5.png b/gfx/glyph/5.png
line changes: +0/-0
index 0000000..f3948b4
--- /dev/null
+++ b/gfx/glyph/5.png

diff --git a/gfx/glyph/6.png b/gfx/glyph/6.png
line changes: +0/-0
index 0000000..fa5e1a1
--- /dev/null
+++ b/gfx/glyph/6.png

diff --git a/gfx/glyph/7.png b/gfx/glyph/7.png
line changes: +0/-0
index 0000000..f515801
--- /dev/null
+++ b/gfx/glyph/7.png

diff --git a/gfx/glyph/8.png b/gfx/glyph/8.png
line changes: +0/-0
index 0000000..56e877a
--- /dev/null
+++ b/gfx/glyph/8.png

diff --git a/gfx/glyph/9.png b/gfx/glyph/9.png
line changes: +0/-0
index 0000000..507f024
--- /dev/null
+++ b/gfx/glyph/9.png

diff --git a/gfx/good.png b/gfx/good.png
line changes: +0/-0
index 0000000..f48ad0c
--- /dev/null
+++ b/gfx/good.png

diff --git a/gfx/great.png b/gfx/great.png
line changes: +0/-0
index 0000000..455899f
--- /dev/null
+++ b/gfx/great.png

diff --git a/gfx/poor.png b/gfx/poor.png
line changes: +0/-0
index 0000000..d453669
--- /dev/null
+++ b/gfx/poor.png

diff --git a/gfx/title.png b/gfx/title.png
line changes: +0/-0
index 0000000..222d3cb
--- /dev/null
+++ b/gfx/title.png

diff --git a/gfx/triangle.png b/gfx/triangle.png
line changes: +0/-0
index 0000000..0df4d05
--- /dev/null
+++ b/gfx/triangle.png

diff --git a/keyfile.py b/keyfile.py
line changes: +214/-0
index 0000000..3375e65
--- /dev/null
+++ b/keyfile.py
@@ -0,0 +1,214 @@
+import pygame
+import imp
+import os.path
+from bmevent import *
+
+class KeyFile:
+	bmelist = None
+	numkeys = 0
+
+	# Loaded from a file
+	player = None
+	genre = None
+	title = None
+	artist = None
+	stagefile = None
+	playlevel = -1
+	rank = 3
+	stagefile = None
+	volwav = 1.0
+	wavs = None
+	bmps = None
+	offset = 0
+
+	# Allows us to continuously map time to the current beat
+	beatfunc = None
+
+	# Keymapping is a sequence of keycodes, mapping the index to the
+	# key
+	def __init__(self):
+		self.bmelist = []
+		self.wavs = {}
+		self.bmps = {}
+
+	def add(self,bme):
+		self.bmelist.append(bme)
+
+	def remove(self,bme):
+		return self.bmelist.remove(bme)
+
+	def sort(self):
+		def sortfun(a,b):
+			if a.beat == b.beat:
+				return 0
+			elif a.beat > b.beat:
+				return 1
+			elif a.beat < b.beat:
+				return -1
+		self.bmelist.sort(sortfun)
+
+	def dump(self):
+		for b in self.bmelist:
+			print str(b)
+
+	# AWFUL DIRTY NO GOOD HACK
+	# (but does find the end of the song within 10ms)
+	def length(self):
+		if len(self.beatfunc) >= 2:
+			t = self.beatfunc[-2][0]
+		else:
+			t = 0
+		last_beat = self.bmelist[-1].beat
+		b = self.eval_beatfunc(t)
+		while b < last_beat:
+			t+= 10
+			b = self.eval_beatfunc(t)
+		return t
+
+	# It's time to get func-y
+	def generate_beatfunc(self):
+		self.beatfunc = []
+		bpms = filter(lambda x: x.type & (BME_TEMPO | BME_LONGMEASURE | BME_STOP), self.bmelist)
+		self.beatfunc = self.generate_beatfunc_r(bpms)
+
+	def generate_beatfunc_r(self, bpms, ct=0):
+		if len(bpms) == 0:
+			return [(3600000.0,0)]
+		beat = bpms[0].beat
+		type = bpms[0].type
+
+		if type == BME_TEMPO:
+			ms_per_beat = 60000.0 / bpms[0].dataref
+			func = lambda t: (t - ct) / ms_per_beat + beat
+			if len(bpms) == 1:
+				next = 4000
+			else:
+				next = bpms[1].beat
+			duration = (next - beat) * ms_per_beat
+			self.lastbpm = bpms[0].dataref
+		elif type == BME_LONGMEASURE:
+			# Long measures only last one measure, so we do
+			# the slow measure just like a tempo change,
+			# then add a tempo change back at the end.
+			ms_per_beat = (60000.0 / self.lastbpm) * bpms[0].dataref
+			func = lambda t: (t - ct) / ms_per_beat + beat
+
+			print "Last BPM:",self.lastbpm
+			duration = 4 * ms_per_beat
+			bpms.insert(1,BMEvent(beat+4,BME_TEMPO,0,self.lastbpm))
+		elif type == BME_STOP:
+			func = lambda t: beat
+			duration = bpms[0].dataref
+		else:
+			raise Exception("WTF? Invalid type in generate_beatfunc_r")
+		l = self.generate_beatfunc_r(bpms[1:], ct + duration)
+		l.insert(0,(ct,func))
+		return l
+
+	def eval_beatfunc(self,t):
+		for n in range(0,len(self.beatfunc)-1):
+			if self.beatfunc[n][0] <= t and self.beatfunc[n+1][0] > t:
+				return self.beatfunc[n][1](t)
+
+	def show_beatfunc(self,surface):
+		end = self.length()
+		xscale = end / surface.get_width()
+		yscale = self.bmelist[-1].beat / surface.get_height()
+
+		for t in range(0,end,100):
+			beat = self.eval_beatfunc(t)
+			if beat:
+				pygame.draw.circle(surface,(255,255,255),(t/xscale,480 - beat / yscale),1)
+			else:
+				pygame.draw.line(surface,(255,0,0), (t/140.625,0), (t/140.625,480))
+
+
+class BMEListIter:
+	bmelist = None
+
+	def __init__(self,bmelist):
+		self.bmelist = bmelist 
+		self.b = 0.0
+		self.i = 0
+
+	def goto(self,b):
+		l = len(self.bmelist) - 1
+		self.b = b
+
+		while self.i > 0 and b < self.bmelist[self.i].beat:
+			self.i -= 1
+		while self.i < l and b > self.bmelist[self.i].beat:
+			self.i += 1
+
+	def window(self,db,type=None):
+		l = len(self.bmelist)
+		eb = self.b + db
+		ei = self.i
+
+		while ei < l and eb >= self.bmelist[ei].beat:
+			ei += 1
+
+		if type:
+			return filter(lambda x: x.type & type, self.bmelist[self.i:ei])
+		else:
+			return self.bmelist[self.i:ei]
+
+
+screen = None
+font = None
+
+loaders = []
+for x in ["BMloader","SMloader"]:
+	f = open("loaders/" + x + ".py")
+	loaders.append(imp.load_module(x,f,x + ".py",(".py",'r',imp.PY_SOURCE)))
+	#f.close()
+
+def vmessage(message):
+	fs = font.render(message, 0, (255,255,255),(0,0,0))
+	screen.fill((0,0,0),(0,450,640,30))
+	screen.blit(fs,(0,450))
+
+def vstatus(type,arg):
+	if type == "STAGEFILE":
+		screen.blit(arg,(0,0))
+	elif type == "WAV":
+		vmessage("Loaded WAV " + arg)
+	elif type == "BMP":
+		vmessage("Loaded BMP " + arg)
+	elif type == "TRACK":
+		vmessage("Parsing track " + str(arg))
+	elif type == "ERROR":
+		vmessage("ERROR: " + arg)
+	pygame.display.flip()
+
+def likelihood(file):
+	likelihoods = map(lambda l: l.detect(file), loaders)
+	gl = 0.0
+	gn = None
+	for n in range(0,len(likelihoods)):
+		if likelihoods[n] > gl:
+			gl = likelihoods[n]
+			gn = n
+	return gn
+
+def kf_load(file):
+	global screen,font
+	screen = pygame.display.get_surface()
+	font = pygame.font.SysFont("Helvetica Normal",30)
+	gn = likelihood(file)
+	if gn != None:
+		print "Load..."
+		kf = loaders[gn].load(file,vstatus)
+		kf.generate_beatfunc()
+		return kf
+	else:
+		return None
+	font = None
+
+def kf_info(file):
+	gn = likelihood(file)
+	d = loaders[gn].info(file)
+	d['loader.name'] = loaders[gn].name
+	d['loader.version'] = loaders[gn].version
+
+	return d

diff --git a/keygraph.py b/keygraph.py
line changes: +189/-0
index 0000000..0468075
--- /dev/null
+++ b/keygraph.py
@@ -0,0 +1,189 @@
+import pygame
+from bmevent import *
+from keyhandler import *
+from keyfile import BMEListIter
+from fx import ColumnPulse
+from constants import *
+
+red = (255,30,30)
+offwhite = (245,245,245)
+blue = (30,30,255)
+black = (0,0,0)
+darkgray = (20,20,20)
+
+class KeyGraph:
+	keyfile = None
+	t = 0
+	b = 0
+	velocity = 150.0
+	li = None
+	bordercolor = (255,255,255)
+	judgemapping = {JUDGE_FGREAT: ["gfx/great.png","gfx/fgreat.png"],
+			JUDGE_GREAT: ["gfx/great.png"],
+			JUDGE_GOOD: ["gfx/good.png"],
+			JUDGE_BAD: ["gfx/bad.png"],
+			JUDGE_POOR: ["gfx/poor.png"],
+			JUDGE_NA: []}
+	colmarkmap = {JUDGE_FGREAT: "gfx/dcircle.png",
+		      JUDGE_GREAT: "gfx/circle.png",
+		      JUDGE_GOOD: "gfx/triangle.png",
+		      JUDGE_BAD: "gfx/cross.png",
+		      JUDGE_POOR: "gfx/cross.png",
+		      JUDGE_NA: None}
+	numberfiles = ["gfx/glyph/0.png","gfx/glyph/1.png","gfx/glyph/2.png","gfx/glyph/3.png","gfx/glyph/4.png","gfx/glyph/5.png","gfx/glyph/6.png","gfx/glyph/7.png","gfx/glyph/8.png","gfx/glyph/9.png"]
+	
+	def __init__(self, kf, keystyle, player, disprect):
+		self.numkeys = len(keystyle)
+		self.keyfile = kf
+		self.screen = pygame.display.get_surface()
+		self.disprect = disprect
+		self.vheight = disprect.height - 5;
+		self.barrect = pygame.Rect(disprect.left,
+					   disprect.bottom - 4,
+					   disprect.width,
+					   5)
+		if player == 1:
+			self.notefilter = BME_NOTE1
+		elif player == 2:
+			self.notefilter = BME_NOTE2
+		else:
+			print "Invalid player given to keygraph: " + kf
+			exit(1)
+		self.t = 0
+		self.b = 0
+		self.li = BMEListIter(self.keyfile.bmelist)
+		self.keystyle = []
+
+		w = 0
+		tw = sum(map(lambda x: x[0],keystyle))
+		for x in keystyle:
+			self.keystyle.append((int(w),x[1],x[2]))
+			w += (x[0] / tw) * (disprect.width - 1)
+		self.keystyle.append((int(w),None,None))
+
+		# Load gfx
+		self.judgeimg = {}
+		for k in self.judgemapping.keys():
+			self.judgeimg[k] = map(lambda y: pygame.image.load(y).convert_alpha(),self.judgemapping[k])
+		self.colmarkimg = {}
+		for k in self.colmarkmap.keys():
+			if self.colmarkmap[k]:
+				self.colmarkimg[k] = pygame.image.load(self.colmarkmap[k]).convert_alpha()
+			else:
+				self.colmarkimg[k] = None
+		self.colmarkimg[0] = None
+		self.numberimg = map(lambda y: pygame.image.load(y).convert_alpha(),self.numberfiles)
+		self.judgement = 0
+		self.kjudgement = [0 for x in range(0,self.numkeys)]
+		self.judgetime = 0
+		self.kjudgetime = [0 for x in range(0,self.numkeys)]
+		self.combo = 0
+
+		# Initialize keypulses (they are neat)
+		self.pulse = []
+		for n in range(0,len(keystyle)):
+			x = self.keystyle[n][0] + 1
+			y = self.disprect.top + 0.3 * self.disprect.height
+			w = self.keystyle[n+1][0] - self.keystyle[n][0] - 1
+			h = 0.7 * self.disprect.height - 4
+			self.pulse.append(ColumnPulse(pygame.Rect(x,y,w,h),keystyle[n][3]))
+		self.accuracy = 0
+		self.key = 0
+
+	def setproperties(self,properties):
+		try:
+			ud = properties['ud']
+			k = properties['key']
+			if ud == 1:
+				self.pulse[k].down()
+			else:
+				self.pulse[k].up()
+			self.key = k
+		except KeyError:
+			pass
+
+		try:
+			self.judgement = properties['judgement']
+			self.kjudgement[self.key] = properties['judgement']
+			self.judgetime = self.t
+			self.kjudgetime[self.key] = self.t
+			if self.judgement <= JUDGE_GOOD:
+				self.combo += 1
+			else:
+				self.combo = 0
+		except KeyError:
+			pass
+
+		try:
+			self.accuracy = properties['accuracy']
+		except KeyError:
+			pass
+
+	def numimgs(self,n):
+		imgs = []
+		while n:
+			imgs.append(self.numberimg[n % 10])
+			n = n / 10
+		imgs.reverse()
+		return imgs
+
+	def draw(self):
+		self.b = self.keyfile.eval_beatfunc(self.t)
+		for n in range(0,self.numkeys):
+			x = self.disprect.left + self.keystyle[n][0]
+			w = self.keystyle[n+1][0] - self.keystyle[n][0]
+			self.screen.fill(self.keystyle[n][2],
+				(x,self.disprect.top,w,self.disprect.height))
+		for n in range(0,self.numkeys+1):
+			pygame.draw.line(self.screen, self.bordercolor,
+				(self.disprect.left + self.keystyle[n][0], self.disprect.top),
+				(self.disprect.left + self.keystyle[n][0], self.disprect.bottom - 1) )
+
+		self.li.goto(self.b)
+		keylist = self.li.window(self.vheight / self.velocity, self.notefilter)
+		pygame.draw.rect(self.screen, (255,0,0), self.barrect)
+		distortion = 1.0
+		for k in keylist:
+			y = self.disprect.top + self.vheight - int((k.beat - self.b) * (self.velocity / distortion))
+			x = self.disprect.left + self.keystyle[k.key][0] + 1
+			w = self.keystyle[k.key+1][0] - self.keystyle[k.key][0] - 1
+			pygame.draw.rect(self.screen, self.keystyle[k.key][1], (x,y,w,5) )
+		for n in self.pulse:
+			n.draw()
+
+		if self.judgement:
+			if self.t - self.judgetime > 500:
+				self.judgement = 0
+			elif len(self.judgeimg[self.judgement]) == 0:
+				pass
+			else:
+				z = (self.t / 60) % len(self.judgeimg[self.judgement])
+				w = self.judgeimg[self.judgement][z].get_width()
+				x = self.disprect.left + (self.disprect.width / 2) - (w / 2)
+				y = self.disprect.top + self.disprect.height * 0.7
+				if self.combo >= 2:
+					imgs = self.numimgs(self.combo)
+					x -= 2
+					w += 4
+					for i in imgs:
+						x -= i.get_width() / 2
+					for i in imgs:
+						self.screen.blit(i,(x + w,y))
+						w += i.get_width()
+				self.screen.blit(self.judgeimg[self.judgement][z],(x,y))
+				# Draw accuracy bar
+				x = self.disprect.left + self.disprect.width / 2
+				y += 50
+				dx = self.accuracy
+				if dx < 0:
+					x += dx
+					dx = -dx
+				self.screen.fill((255,0,0), (x,y,dx,3))
+		for i in range(0,self.numkeys):
+			if self.t - self.kjudgetime[i] > 300:
+				self.kjudgement[i] = 0
+			else:
+				x = self.disprect.left + self.keystyle[i][0] + (self.keystyle[i+1][0] - self.keystyle[i][0] - 24) / 2
+				y = self.disprect.bottom - 30
+				if self.colmarkimg[self.kjudgement[i]]:
+					self.screen.blit(self.colmarkimg[self.kjudgement[i]],(x,y))

diff --git a/keyhandler.py b/keyhandler.py
line changes: +100/-0
index 0000000..f06e344
--- /dev/null
+++ b/keyhandler.py
@@ -0,0 +1,100 @@
+from bmevent import *
+from keyfile import *
+from pygame.locals import *
+from constants import *
+
+class KeyHandler:
+	keyfile = None
+	li = None
+
+	keymap = None
+	jsbuttonmap = None
+	jsaxismap = None
+	timings = None
+
+	def __init__(self,gameconfig,keyfile):
+		self.keyfile = keyfile
+		self.li = BMEListIter(keyfile.bmelist)
+		self.lastdataref = [0,0,0,0,0,0,0,0]
+		self.li.goto(0)
+		for b in self.li.window(1.0):
+			if b.type == BME_TEMPO:
+				self.bpm = b.dataref
+				self.adjust_window()
+				break
+		self.keymap = gameconfig['keymap']
+		self.jsbuttonmap = gameconfig['jsbuttonmap']
+		self.jsaxismap = gameconfig['jsaxismap']
+		self.timings = gameconfig['timings']
+
+	def decodekey(self,event):
+		try: 
+			if event.type == KEYDOWN:
+				return (self.keymap[event.key],1)
+			elif event.type == KEYUP:
+				return (self.keymap[event.key],0)
+			elif event.type == JOYBUTTONDOWN:
+				return (self.jsbuttonmap[event.button],1)
+			elif event.type == JOYBUTTONUP:
+				return (self.jsbuttonmap[event.button],0)
+			elif event.type == JOYAXISMOTION:
+				if event.value == 0.0:
+					return (self.jsaxismap[(event.axis,event.value)],0)
+				else:
+					return (self.jsaxismap[(event.axis,event.value)],1)
+			else:
+				return (None,-1)
+		except KeyError:
+			return (None,-1)
+
+	def adjust_window(self):
+		self.ms_per_beat = 60000.0 / self.bpm
+		self.tstart = 200.0 / self.ms_per_beat
+		self.tlength = 2000.0 / self.ms_per_beat
+
+	def setproperties(self,properties):
+		try:
+			self.bpm = properties['bpm']
+			self.adjust_window()
+		except KeyError:
+			pass
+
+	def handle(self,event,t):
+		b = self.keyfile.eval_beatfunc(t)
+		properties = {}
+		(k,ud) = self.decodekey(event)
+		if k == None:
+			return
+		properties['key'] = k
+		properties['ud'] = ud
+		if ud == 0:
+			return (None, properties)
+		self.li.goto(b - self.tstart)
+		bmelist = filter(lambda x: x.key == k, self.li.window(self.tlength,BME_NOTE1))
+		if not bmelist:
+			properties['judgement'] = JUDGE_NA
+			return (BMEvent(b,BME_HIT,k,self.lastdataref[k]), properties)
+
+		# Find the closest bme to our event
+		sd = self.tstart
+		cbme = None
+		for bme in bmelist:
+			d = abs(b - bme.beat)
+			if d < sd:
+				sd = d
+				cbme = bme
+		# Convert back to ms
+		sd = sd * self.ms_per_beat
+
+		if cbme:
+			for bt in self.timings:
+				if sd < bt[0]:
+					properties['judgement'] = bt[1]
+					cbme.type = BME_HIT
+					break
+			self.lastdataref[k] = cbme.dataref
+		else:
+			properties['judgement'] = JUDGE_NA
+			return (BMEvent(b,BME_HIT,k,self.lastdataref[k]),properties)
+		properties['accuracy'] = (b - cbme.beat) * self.ms_per_beat
+		return (cbme, properties)

diff --git a/loaders/BMloader.py b/loaders/BMloader.py
line changes: +175/-0
index 0000000..591bd01
--- /dev/null
+++ b/loaders/BMloader.py
@@ -0,0 +1,175 @@
+import pygame
+import os.path
+import re
+from bmevent import *
+from keyfile import *
+from util import *
+
+# Required information for a loader module. If this isn't here, the game 
+# will crash and burn and it will be ALL YOUR FAULT.
+name = "BeMusic BMS/BME"
+version = 0.0
+
+def detect(file):
+	res = [re.compile("^#PLAYLEVEL"), re.compile("^#WAV"), re.compile("^#BMP")]
+	match = [0,0,0]
+
+	f = open(file)
+	for line in f:
+		for n in range(0,len(res)):
+			if res[n].match(line):
+				match[n] = 1
+	return sum(match) / float(len(match))
+
+def info(file):
+	d = {}
+	f = open(file,'r')
+	r = re.compile('(WAV|BMP|\d{5}:)')
+	for line in f:
+		if len(line) == 0 or line[0] != "#":
+			continue
+		line = line[1:]
+		l = line.split(' ')
+		arg = ''
+		cmd = l[0]
+		if len(l) >= 2:
+			arg = ' '.join(l[1:])
+		if not r.match(cmd):
+			d[cmd.lower()] = arg.strip()
+	return d
+
+def load(file,status=lambda x,y:None):
+	keymapping = {0:1, 1:2, 2:3, 3:4, 4:5, 5:0, 7:6, 8:7}
+	kf = KeyFile()
+	kf.numkeys = len(keymapping)
+	lastbpm = 0
+	dir = os.path.dirname(file)
+	f = open(file,'r')
+	kf.track = -1
+	for line in f:
+		line = line.strip()
+		if len(line) == 0 or line[0] != "#":
+			continue
+		line = line[1:]
+		l = line.split(' ')
+		arg = ''
+		cmd = l[0]
+		if len(l) >= 2:
+			arg = ' '.join(l[1:])
+		if cmd == "PLAYER":
+			kf.player = arg
+		elif cmd == "GENRE":
+			kf.genre = arg
+		elif cmd == "TITLE":
+			kf.title = arg
+		elif cmd == "ARTIST":
+			kf.artist = arg
+		elif cmd == "BPM":
+			try:
+				lastbpm = float(arg)
+				kf.add(BMEvent(0, BME_TEMPO, None, lastbpm))
+			except ValueError:
+				print "Invalid value for BPM"
+			#kf.ms_per_measure = 240000.0 / kf.bpm
+		elif cmd == "PLAYLEVEL":
+			try:
+				kf.playlevel = int(arg)
+			except ValueError:
+				print "Invalid value for PLAYLEVEL"
+		elif cmd == "RANK":
+			try:
+				kf.rank = int(arg)
+			except ValueError:
+				print "Invalid value for RANK"
+		elif cmd == "STAGEFILE":
+			# This will stay here for now. Eventually, this
+			# will go into the "status" routine or somewhere
+			# further up.
+			if arg:
+				kf.stagefile = loadBMP(os.path.join(dir,arg))
+				status("STAGEFILE",kf.stagefile)
+		elif cmd == "VOLWAV":
+			kf.volwav = float(arg) / 100.0
+		elif cmd[0:3] == "WAV":
+			slot = int(cmd[3:5],36)
+			if arg[-3:].lower() == 'mp3':
+				wav = loadMP3(os.path.join(dir,arg))
+			else:
+				wav = loadWAV(os.path.join(dir,arg))
+				if wav:
+					wav.set_volume(kf.volwav)
+			if wav:
+				kf.wavs[slot] = wav
+				status("WAV",arg)
+			else:
+				status("ERROR","Could not load " + arg)
+				pygame.time.wait(1500)
+		elif cmd[0:3] == "BMP":
+			slot = int(cmd[3:5],36)
+			bmp = loadBMP(os.path.join(dir,arg))
+			if bmp:
+				kf.bmps[slot] = bmp
+				status("BMP",arg)
+			else:
+				status("ERROR","Could not load " + arg)
+		elif len(cmd) > 5 and cmd[5] == ":":
+			# Hmm. Should "track" really be "measure"?
+			track = int(cmd[0:3])
+			channel = int(cmd[3:5])
+			message = cmd[6:]
+
+			if channel == 2:
+				# Channel 2 is a floating-point
+				# multiplier that changes the length of
+				# the measure.
+				kf.add(BMEvent(track*4, BME_LONGMEASURE, None, float(message)))
+				continue
+			if track != kf.track:
+				status("TRACK",track)
+			kf.track = track
+
+			v = [int(message[n*2:n*2+2],36) for n in range(0,len(message)/2)]
+			l = float(len(v)) / 4.0
+
+			bme = None
+			if channel == 1:
+				for n in range(0,len(v)):
+					if v[n] == 0:
+						continue
+					bme = BMEvent(track*4 + n / l, BME_BGM, None, v[n])
+					kf.add(bme)
+			if channel == 3:
+				v = [int(message[n*2:n*2+2],16) for n in range(0,len(message)/2)]
+				for n in range(0,len(v)):
+					# WTF is up with the low tempos?
+					if v[n] == 0 or v[n] < 30:
+						continue
+					print "new BPM: " + str(v[n])
+					bme = BMEvent(track*4 + n / l, BME_TEMPO, None, v[n])
+					kf.add(bme)
+					lastbpm = v[n]
+			if channel == 4:
+				for n in range(0,len(v)):
+					if v[n] == 0:
+						continue
+					bme = BMEvent(track*4 + n / l, BME_BGA, None, v[n])
+					kf.add(bme)
+			elif (channel >= 11 and channel <= 19) or (channel >= 21 and channel <= 29):
+				if channel >= 21 and channel <= 29:
+					type = BME_NOTE2
+					b = 21
+				else:
+					type = BME_NOTE1
+					b = 11
+				for n in range(0,len(v)):
+					if v[n] == 0 or not (channel - b) in keymapping:
+						continue
+					bme = BMEvent(track*4 + n / l, type, keymapping[channel - b], v[n])
+					kf.add(bme)
+		else:
+			print "Unknown command:",cmd
+	kf.sort()
+	f.close()
+
+	return kf
+

diff --git a/loaders/SMloader.py b/loaders/SMloader.py
line changes: +114/-0
index 0000000..1918aa6
--- /dev/null
+++ b/loaders/SMloader.py
@@ -0,0 +1,114 @@
+import pygame
+import os.path
+import re
+from bmevent import *
+from keyfile import *
+
+# Required information for a loader module. If this isn't here, the game
+# will crash and burn and it will be ALL YOUR FAULT.
+name = "StepMania"
+version = 0.0
+
+def detect(file):
+	res = [re.compile("^#SUBTITLE"), re.compile("^#BANNER"),
+	       re.compile("^#BACKGROUND"),re.compile("^#NOTES")]
+	match = [0,0,0,0]
+
+	f = open(file)
+	for line in f:
+		for n in range(0,len(res)):
+			if res[n].match(line):
+				match[n] = 1
+	return sum(match) / float(len(match))
+
+def info(file):
+	return {}
+
+def load(file,status=lambda x,y:None):
+	keymapping = {0:1, 1:2, 2:3, 3:4, 4:5, 5:0, 7:6, 8:7}
+	kf = KeyFile()
+	kf.numkeys = len(keymapping)
+	dir = os.path.dirname(file)
+	f = open(file,'r')
+	kf.track = -1
+	matcher = re.compile("#([A-Z]+):(.*);",re.DOTALL)
+	kf.stoplist = []
+	buf = ''
+	for line in f:
+		line = line.split('//')[0];
+		line = line.strip()
+		if len(line) == 0:
+			continue
+		buf += line
+		m = matcher.match(buf)
+		if m:
+			buf = '';
+			cmd = m.group(1)
+			arg = m.group(2).split(':')
+		else:
+			continue
+
+		if cmd == "TITLE":
+			kf.title = arg[0]
+		elif cmd == "SUBTITLE":
+			kf.subtitle = arg[0]
+		elif cmd == "ARTIST":
+			kf.artist = arg[0]
+		elif cmd == "BPMS":
+			bpmlist = map(lambda x: x.split('='),arg[0].split(','))
+			kf.bpm = bpmlist[0][1]
+			for b in bpmlist:
+				kf.add(BMEvent(float(b[0]), BME_TEMPO, None, float(b[1])))
+		elif cmd == "STOPS":
+			if arg[0]:
+				stoplist = map(lambda x: x.split('='),arg[0].split(','))
+				for s in stoplist:
+					print s[0]
+					kf.add(BMEvent(float(s[0]), BME_STOP, None, int(float(s[1])*1000)))
+		elif cmd == "BACKGROUND":
+			if arg[0]:
+				try:
+					kf.stagefile = pygame.image.load(os.path.join(dir,arg[0]))
+					kf.stagefile = kf.stagefile.convert()
+					status("STAGEFILE",kf.stagefile)
+				except pygame.error:
+					pass
+		elif cmd == "OFFSET":
+			kf.offset = float(arg[0])
+			kf.add(BMEvent(int(kf.offset * 1000), BME_BGM, None, 255))
+		elif cmd == "MUSIC":
+			print os.path.join(dir,arg[0])
+			try:
+				pygame.mixer.music.load(os.path.join(dir,arg[0]))
+                        except pygame.error:
+				pygame.mixer.music.load(os.path.join(dir,arg[0].lower()))
+			kf.wavs[255] = pygame.mixer.music
+		elif cmd == "NOTES":
+			if arg[0] == 'dance-single' and arg[2].lower() == 'challenge':
+				parse_notes(arg[5],kf)
+		else:
+			print "Unknown command:",cmd
+	kf.sort()
+	f.close()
+
+	return kf
+
+# BUGS! This assumes a four-column chart.
+def parse_notes(str,kf):
+	tracks = str.split(',')
+	track = 0
+
+	for t in tracks:
+		l = len(t) / 4
+		for n in range(0,l):
+			for m in range(0,4):
+				k = t[n*4+m]
+				if k == 'M':	# Freaking mines...
+					continue
+				k = int(k)
+				if k > 0:
+					beat = 4 * (track + (float(n)/l))
+					# Add note filter here?
+					kf.add(BMEvent(beat, BME_NOTE1, m*2+1, 1))
+		track += 1
+

diff --git a/mainmenu.py b/mainmenu.py
line changes: +130/-0
index 0000000..5fce8d2
--- /dev/null
+++ b/mainmenu.py
@@ -0,0 +1,130 @@
+import pygame
+import thread
+from fx import *
+import event
+
+class MainMenu:
+	fx = None
+	fxlock = None
+	period = 428
+	circuitlist = [[(590,0), (550,40), (200,40), (170,70), (170,310)],
+		       [(570,0), (540,30), (190,30), (160,60), (20,60), (20,220), (50,250), (50,420), (90,460), (610,460)],
+		       [(0,220), (40,260), (40,480)],
+		       [(180,0), (180,20), (150,50), (20,50), (20,0)]]
+	# (label, message)
+	menuitems = [("play","fileselect"), ("quit","quit")]
+	subtitle = "holy shit parallax!!!11!1"
+	version = 1e-99
+
+	def __init__(self):
+		self.fxlock = thread.allocate_lock()
+		self.fx = []
+		self.menufx = []
+		self.position = 0
+		self.bar = PulseLine([(0,0),(130,0)],(200,200,255),self.period,5)
+		thread.start_new_thread(self.fxloader,())
+
+	def fxloader(self):
+		self.fxlock.acquire()
+		self.fx.append(Blank())
+		for l in self.circuitlist:
+			o = PulseLine(l,(255,30,30),self.period)
+			self.fx.append(o)
+		self.fxlock.release()
+
+		o = Image('gfx/title.png',(200,40))
+		self.fxlock.acquire()
+		self.fx.append(o)
+		self.fxlock.release()
+
+		n = 0
+		for l in self.menuitems:
+			o = Text(l[0],'font/Nano.ttf',18,(30,70 + 20*n), (255,30,30))
+			self.fxlock.acquire()
+			self.fx.append(o)
+			self.menufx.append(o)
+			self.fxlock.release()
+			n += 1
+
+		o = Text(self.subtitle,'font/Nano.ttf',15,(600,100),(255,30,30))
+		o.align = ALIGN_RIGHT
+		self.fxlock.acquire()
+		self.fx.append(o)
+		self.fxlock.release()
+
+		o = Text("version " + str(self.version),'font/Nano.ttf',14,(610,440),(255,30,30))
+		o.align = ALIGN_RIGHT
+		self.fxlock.acquire()
+		self.fx.append(o)
+		self.fxlock.release()
+
+		o = PlaneScroll('gfx/chips_layer1.jpg',(-0.12,0.06),2.0,0.75)
+		self.fxlock.acquire()
+		self.fx.insert(1,o)
+		self.fxlock.release()
+
+		o = PlaneScroll('gfx/chips_layer2.jpg',(-0.06,0.03))
+		self.fxlock.acquire()
+		self.fx.remove(self.fx[0])
+		self.fx.insert(0,o)
+		self.fxlock.release()
+			
+	def draw(self,t):
+		self.fxlock.acquire()
+		for f in self.fx:
+			f.draw(t)
+		self.fxlock.release()
+		self.bar.location = (30, 68 + 20*self.position)
+		self.bar.draw(t)
+		self.bar.location = (30, 68 + 20*self.position + 20)
+		self.bar.draw(t)
+
+	def run(self):
+		screen = pygame.display.get_surface()
+		decided = 0
+		pygame.mixer.music.load('snd/0x00.ogg')
+		pygame.mixer.music.set_volume(0.75)
+		pygame.mixer.music.play(-1)
+		stab = pygame.mixer.Sound('snd/stab.ogg')
+		stab.set_volume(0.25)
+		start_t = pygame.time.get_ticks()
+
+		while decided == 0:
+			t = pygame.time.get_ticks() - start_t
+			for e in pygame.event.get():
+				act = event.parseevent(e)
+				if act == event.CANCEL:
+					decided = 'quit'
+				elif act == event.DOWN:
+					self.position += 1
+				elif act == event.UP:
+					self.position -= 1
+				elif act == event.OK:
+					decided = self.menuitems[self.position][1]
+
+			self.position = self.position % len(self.menuitems)
+			self.draw(t)
+			pygame.display.flip()
+
+		pygame.mixer.music.fadeout(1500)
+		stab.play()
+
+		start_t2 = pygame.time.get_ticks()
+		dt = 0
+		blanksurf = pygame.surface.Surface((640,480)).convert()
+		blanksurf.fill( (0,0,0) )
+		while dt < 1500:
+			dt = pygame.time.get_ticks() - start_t2
+			if dt < 1000:
+				self.draw(pygame.time.get_ticks() - start_t)
+				blanksurf.set_alpha(int(dt / 1000.0 * 255.0))
+				screen.blit(blanksurf,(0,0))
+			else:
+				screen.fill( (0,0,0) )
+			self.menufx[self.position].draw(0)
+			if dt >= 1000:
+				blanksurf.set_alpha(int((dt-1000) / 500.0 * 255.0))
+				screen.blit(blanksurf,(0,0))
+			pygame.display.flip()
+
+		return [decided]

diff --git a/mksongcache.py b/mksongcache.py
line changes: +6/-0
index 0000000..03549c3
--- /dev/null
+++ b/mksongcache.py
@@ -0,0 +1,6 @@
+#!/usr/bin/python
+
+import fileselect
+
+fileselect.mksongcache()
+

diff --git a/rotowidget.py b/rotowidget.py
line changes: +44/-0
index 0000000..6b9fef3
--- /dev/null
+++ b/rotowidget.py
@@ -0,0 +1,44 @@
+import pygame
+from bmevent import *
+from keyhandler import *
+from math import exp,pi,sqrt
+
+# A rotowidget displays two axes of information in a rotary fashion.  A
+# central "pulser" is designed to show the score, and an arc around the
+# edge shows the progress through the song.
+class RotoWidget:
+	# Color format: (level, color)
+	# If the first item's level is greater than zero, the first
+	# color is black
+	pulsecolors = [(0,(180,0,0)), (80,(50,255,0))]
+	arccolors = [(0,(200,0,200))]
+	r = -0.2
+	
+	def __init__(self,center,radius,period):
+		self.screen = pygame.display.get_surface()
+		self.center = center
+		self.radius = radius
+		self.period = period
+		self.pulsev = 0
+		self.arcv = 0
+		self.t = 0
+
+	def draw(self,t):
+		rad = self.radius * (self.pulsev / 100.0) + (0.1 * self.radius) * exp(self.r * (t % self.period))
+		if rad > self.radius:
+			rad = self.radius
+
+		pc = (0,0,0)
+		for n in range(len(self.pulsecolors)-1,-1,-1):
+			if self.pulsecolors[n][0] <= self.pulsev:
+				pc = self.pulsecolors[n][1]
+				break
+		pygame.draw.circle(self.screen,pc,self.center,rad,0)
+
+		ac = (0,0,0)
+		for n in range(len(self.arccolors)-1,-1,-1):
+			if self.arccolors[n][0] <= self.arcv:
+				ac = self.arccolors[n][1]
+				break
+		pygame.draw.arc(self.screen,ac,(self.center[0]-self.radius, self.center[1]-self.radius, self.radius*2, self.radius*2),0,2*pi*(self.arcv / 100.0),3)
+

diff --git a/snd/0x00.ogg b/snd/0x00.ogg
line changes: +0/-0
index 0000000..2b9ce02
--- /dev/null
+++ b/snd/0x00.ogg

diff --git a/snd/0x01.ogg b/snd/0x01.ogg
line changes: +0/-0
index 0000000..9f7c3b3
--- /dev/null
+++ b/snd/0x01.ogg

diff --git a/snd/stab.ogg b/snd/stab.ogg
line changes: +0/-0
index 0000000..3b814cc
--- /dev/null
+++ b/snd/stab.ogg

diff --git a/songs/README.txt b/songs/README.txt
line changes: +1/-0
index 0000000..d716355
--- /dev/null
+++ b/songs/README.txt
@@ -0,0 +1 @@
+Put your songs here.

diff --git a/util.py b/util.py
line changes: +33/-0
index 0000000..230a765
--- /dev/null
+++ b/util.py
@@ -0,0 +1,33 @@
+import pygame
+import os.path
+
+import config
+
+def loadWAV(path,library=False):
+	try:
+		return pygame.mixer.Sound(path)
+	except pygame.error:
+		(dir,file) = os.path.split(path)
+		try:
+			return pygame.mixer.Sound(os.path.join(dir,file.lower()))
+		except pygame.error:
+			if library:
+				return None
+			else:
+				# Ok, try the system sample library...
+				return loadWAV(os.path.join(config.librarypath,file),True)
+
+def loadMP3(path):
+	pygame.mixer.music.load(path)
+	return pygame.mixer.music
+
+def loadBMP(path):
+	try:
+		return pygame.image.load(path).convert()
+	except pygame.error:
+		(dir,file) = os.path.split(path)
+		try:
+			return pygame.image.load(os.path.join(dir,file.lower())).convert()
+		except pygame.error:
+			return None
+