Lyntin sound plugin with PyMedia

Guido Gloor <[email protected]> Mon, 09 Aug 2004 00:39:47 +0200
Newsgroups gmane.comp.games.mud.client.lyntin
Message-ID <[email protected]>
Hi all
======

First off, I have to apologize for posting this to two newsgroups. But 
the problem, as I see it, is in the way those two things work together 
(or don't). I guess the problem is more Lyntin's than PyMedia's though, 
and it may be a stupid error on my part as well (actually, I think it 
is), I just started learning Python.

                                 ***

I really like Lyntin [1], it's my MUD client of choice for quite some 
time now. A friend of mine created a plugin, I extended it (see [2]), 
and this made me think I could create a plugin of my own.

I thought adding sound support to Lyntin would be a good idea. Something
to learn Python with as well, since I don't know that language yet. So
my question is also not primarily a Lyntin or a PyMedia question, the 
problem may just as well lie in my limited Python knowledge.

I looked if I'd find something for playing back sound in the Python API,
didn't find anything, but then saw the PyMedia framework [3] and guessed
that would work.

                                 ***

The attached file is said sound plugin for Lyntin, with textual 
triggers. The triggering works (Lyntin side: I made it a mud_filter_hook 
[4]), the whole thing is multithreaded, inter-thread communication works 
as well.

The sound playback code works in the interactive Python window. I've 
copied it from the PyMedia documenation at [5].

                                 ***

The problem stems from this code segment:

	        f = open(self.file, 'rb')
		f.seek(0)
	        s = ' '
		while len(s) > 0:
			# exported.write_message(len(s))
	                s = f.read(10000)
	                snd.play(s)

Somehow the thread is cut off as soon as ... I don't know what happens. 
The effect of it is, that the sound is not played until its end. The 
lower the bit count for the buffer, the less of the sound is played, 
down to plain nothing.

I tried the same code in the interactive window. Some notes:
   * you'll have to adapt the path to the sound file
   * the number 10000 is plain arbitrary, changing it has no effect
     (apart from more or less loops)
   * the 8 is actually the constant pymedia.AFMT_U8, but it is easier for
     the plugin configuration if I write it out

		import pymedia.audio.sound as pymedia
		snd = pymedia.Output(22050, 1, 8)
		f = open('C:\Python23\Scripts\test.wav', 'rb')
		f.seek(0)
		s = ' '
		while len(s) > 0:
			len(s)
			s = f.read(10000)
			snd.play(s)

                                 ***

The following line in the lyntinrc loads the plugin correctly and
initializes it with all needed parameters:

#soundset test test.wav 22050 1 8

Afterwards, if there's 'test' somewhere in the MUD's or telnet's or
whatever output, the sound will play. Well, it won't play completely, 
with a small buffer size of 10000 it will probably not play at all.

                                 ***

Anybody has any idea what I'm doing wrong?

Cheers,
Guido

                                 ***

[1] http://lyntin.sourceforge.net/
[2] http://www.haslo.ch/permalink_300~en
[3] http://pymedia.sourceforge.net/
[4] http://lyntin.sourceforge.net/4.0/tutorials/tutorial2.php and
     http://lyntin.sourceforge.net/phpwiki/index.php?Hooks
[5] http://pymedia.sourceforge.net/docs/pymedia.audio.sound.html
test.wav (audio/wav, 16.8 KB) - not displayed
soundtriggers.py (text/plain, 2.8 KB)
#
# Copyright (c) 2004 by Guido Gloor
# Published under the GNU General Public License
# by [email protected]
#
# Loosely based on the dosomething plugin by
# Stefan Aeschbacher
#
__author__ = "Guido Gloor"
__version__ = "alpha"
__date__ = "August 9, 2004"

from lyntin import exported
from lyntin.modules import modutils
import string, os, threading
import pymedia.audio.sound as pymedia

# The dictionary of the commands that are added to lyntin
commands_dict = {}

# all the sounds
sounds  = {}

def sound_set(session, args, input):
	"""
	Add a sound with trigger and parameters to the list
	(NOTE: if you want spaces in the trigger escape them with \\)
	"""
	global sounds

	trigger  = args['trigger']
	file     = args['file']
	bitrate  = args['bitrate']
	channels = args['channels']
	bitdepth = args['bitdepth']

	if sounds.has_key(trigger):
		exported.write_message('duplicate trigger ' + trigger)
		return
	
	sound = []
	sound.append(file)
	sound.append(bitrate)
	sound.append(channels)
	sound.append(bitdepth)
	sound.append(False)
	sounds[trigger] = sound
	exported.write_message('OK, sound trigger for ' + trigger + ' added with file ' + file)
	
def handle_mudtext(args):
	"""
	This function parses all the text from the MUD and checks if a sound should be
	played back
	"""
	line = args['data']
	# check if the line triggers something
	for trigger in sounds:
		if line.find(trigger) != -1:
			# yes, it did trigger. check the block
			sound = sounds[trigger]
			if not sound[4]:
				# block the sound (inter-thread communication)
				sound[4] = True
				SoundPlayer(sound[0], int(sound[1]), int(sound[2]), sound[3], trigger).start()
	# the line remains unchanged
	return line

class SoundPlayer(threading.Thread):
	"""
	A thread for the sound playback
	"""
        def __init__(self, file, bitrate, channels, bitdepth, trigger):
		threading.Thread.__init__(self)
		# get and save parameters
		self.file     = file
		self.bitrate  = bitrate
		self.channels = channels
		self.bitdepth = bitdepth
		self.trigger  = trigger
	def run(self):
		# init PyMedia and play back the sound
	        snd = pymedia.Output(self.bitrate, self.channels, self.bitdepth)
	        f = open(self.file, 'rb')
		f.seek(0)
	        s = ' '
		while len(s) > 0:
			exported.write_message(len(s))
	                s = f.read(10000)
	                snd.play(s)
		f.close()
		sound = sounds[self.trigger]
		sound[4] = False
	
commands_dict['soundset'] = (sound_set, 'trigger file= bitrate= channels= bitdepth=')

def load():
	""" Initializes the module by binding all the commands."""
	modutils.load_commands(commands_dict)
	exported.hook_register("mud_filter_hook", handle_mudtext)

def unload():
	""" Unbinds the commands (for when we reimport the module)."""
	modutils.unload_commands(commands_dict)
	exported.hook_unregister("mud_filter_hook", handle_mudtext)