Skip to content

Instantly share code, notes, and snippets.

@lifning
Created November 23, 2011 19:35
Show Gist options
  • Save lifning/1389662 to your computer and use it in GitHub Desktop.
Save lifning/1389662 to your computer and use it in GitHub Desktop.
Quick and dirty libsnes-based emulator using pygame.
#!/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