Created
November 23, 2011 19:35
-
-
Save lifning/1389662 to your computer and use it in GitHub Desktop.
Quick and dirty libsnes-based emulator using pygame.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python2 | |
import sys, getopt, ctypes, struct | |
import pygame, numpy | |
from snes import core as snes_core | |
# libsnes library to use by default. | |
libsnes = '/usr/lib/libsnes-compatibility.so' | |
if sys.platform == 'win32': libsnes = 'snes.dll' | |
# frameskip | |
screen = None | |
convsurf = None | |
video_frameskip = 0 | |
video_frameskip_idx = 0 | |
# sound buffer initialization and details. | |
soundbuf_size = 512 | |
soundbuf_raw = '' | |
soundbuf_buf = None | |
soundbuf_playing = False | |
# joypad: xinput (x360) layout by default. | |
# BYet^v<>AXLR | |
joymap_arg = '0267----1345' | |
def usage(): | |
global libsnes, video_frameskip, soundbuf_size, joymap_arg | |
return """ | |
Usage: | |
python {} [options] rom.sfc [save.srm] | |
-h, --help | |
Display this help message. | |
-l, --libsnes | |
Specify the dynamically linked LibSNES library to use. | |
If unspecified, {} is used by default. | |
-f, --frameskip | |
Specify a number of video frames to skip rendering (integer) | |
Default value is {}. | |
-s, --soundbuf | |
Specify a size (in samples) of the sound buffer to use. | |
Default value is {}. | |
-j, --joymap | |
Specify a mapping of SNES inputs to PC joypad buttons. | |
This must be a string of 12 characters, specifying which | |
PC joypad button to check for each SNES button, in the order: | |
B, Y, Select, Start, Up, Down, Left, Right, A, X, L, R. | |
Non-numerals are ignored. Only buttons 0-9 are supported. | |
If buttons are mapped to the D-pad, they will be used, but | |
the first POV hat on the joypad is also mapped to the D-pad. | |
The default string is suitable for Xbox controllers: {} | |
rom.sfc | |
The ROM file to load. Must be specified after all options. | |
save.srm | |
The SRAM to load (optional). Must be specified after the ROM. | |
Warning: Won't be updated or overwritten during or after emulation. | |
""".format(sys.argv[0], libsnes, video_frameskip, soundbuf_size, joymap_arg) | |
# parse arguments | |
try: | |
opts, args = getopt.getopt(sys.argv[1:], "hl:f:s:j:", ["help", "libsnes=", "frameskip=", "soundbuf=", "joymap="]) | |
if len(args) < 1: | |
raise getopt.GetoptError('Must specify one ROM argument.') | |
for o,a in opts: | |
if o in ('-h', '--help'): | |
usage() | |
exit(0) | |
elif o in ('-l', '--libsnes'): | |
libsnes = a | |
elif o in ('-f', '--frameskip'): | |
video_frameskip = int(a) | |
elif o in ('-s', '--soundbuf'): | |
soundbuf_size = int(a) | |
elif o in ('-j', '--joymap'): | |
if len(a) != 12: | |
raise getopt.GetoptError('--joymap must specify a string of length 12.') | |
joymap_arg = a | |
except Exception, e: | |
print str(e), usage() | |
sys.exit(1) | |
# callback functions... | |
def video_refresh(data, width, height, hires, interlace, overscan, pitch): | |
global video_frameskip, video_frameskip_idx, convsurf, screen | |
video_frameskip_idx += 1 | |
# init pygame display here, once we know the width and height. | |
if screen is None: | |
if hires: width /= 2 | |
screen = pygame.display.set_mode((width,height)) | |
if video_frameskip_idx > video_frameskip: | |
video_frameskip_idx = 0 | |
# make a surface with the SNES's pixel format, so pygame automatically converts | |
if convsurf is None: | |
convsurf = pygame.Surface( | |
(pitch, height), depth=15, masks=(0x7c00, 0x03e0, 0x001f, 0) | |
) | |
convsurf.get_buffer().write(ctypes.string_at(data,pitch*height*2), 0) | |
if hires: | |
screen.blit(pygame.transform.scale(convsurf, (pitch/2,height)), (0,0)) | |
else: | |
screen.blit(convsurf, (0,0)) | |
pygame.display.flip() | |
def audio_sample(left, right): | |
global soundbuf, soundbuf_buf, soundbuf_raw, soundbuf_playing | |
soundbuf_raw += struct.pack('<HH', left, right) | |
if not soundbuf_playing: | |
soundbuf_playing = True | |
soundbuf.play(loops=-1) | |
if len(soundbuf_raw) >= soundbuf_buf.length: | |
soundbuf_buf.write(soundbuf_raw, 0) | |
soundbuf_raw = '' | |
def input_state(port, device, index, id): | |
global joymap | |
ret = False | |
# we're only interested in player 1 | |
if not port and 0 <= id < 12: | |
# pov hat as fallback for d-pad (up, down, left, right) | |
if id == 4: ret |= joypad.get_hat(0)[1] == 1 | |
elif id == 5: ret |= joypad.get_hat(0)[1] == -1 | |
elif id == 6: ret |= joypad.get_hat(0)[0] == -1 | |
elif id == 7: ret |= joypad.get_hat(0)[0] == 1 | |
tmp = joymap[id] | |
if tmp >= 0: | |
ret |= joypad.get_button(tmp) | |
return ret | |
# map snes buttons to joybuttons | |
# we want a mapping of "joymap[x] = y", where: | |
# x = the snes button id | |
# y = the joypad button corresponding to x | |
joymap = [-1] * 12 | |
for i in xrange(12): | |
button = joymap_arg[i] | |
if button in '0123456789': | |
joymap[i] = int(button) | |
# init pygame sound. snes freq is 32000, 16bit unsigned stereo. | |
pygame.mixer.init(frequency=32000, size=16, channels=2, buffer=soundbuf_size) | |
soundbuf = pygame.sndarray.make_sound(numpy.zeros( (soundbuf_size,2), dtype='uint16', order='C' )) | |
soundbuf_buf = soundbuf.get_buffer() | |
# init pygame joypad input | |
pygame.joystick.init() | |
joypad = pygame.joystick.Joystick(0) | |
joypad.init() | |
# pygame 'clock' used to limit to 60fps on fast computers | |
clock = pygame.time.Clock() | |
# load game and init emulator | |
rom = open(args[0], 'rb').read() | |
sram = None | |
if len(args) > 1: | |
sram = open(args[1], 'rb').read() | |
emu = snes_core.EmulatedSNES(libsnes) | |
emu.load_cartridge_normal(rom, sram) | |
# register callbacks | |
emu.set_video_refresh_cb(video_refresh) | |
emu.set_audio_sample_cb(audio_sample) | |
emu.set_input_state_cb(input_state) | |
# unplug player 2 controller so we don't get twice as many input state callbacks | |
emu.set_controller_port_device(snes_core.PORT_2, snes_core.DEVICE_NONE) | |
# run each frame until closed. | |
running = True | |
state = '' | |
while running: | |
emu.run() | |
clock.tick(60) | |
for event in pygame.event.get(): | |
if event.type == pygame.QUIT: | |
running = False | |
elif event.type == pygame.KEYDOWN: | |
if event.key == pygame.K_F2: | |
state = emu.serialize() | |
elif event.key == pygame.K_F4 and len(state): | |
emu.unserialize(state) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment