SVN commit 1284540 by wrohdewald: use predefined voices from /usr/share/kd4/apps/kajongg/voices language preference is the same as for the kajongg application M +41 -11 doc/kajongg/index.docbook M +13 -7 kajongg/src/game.py M +4 -3 kajongg/src/message.py M +68 -21 kajongg/src/sound.py --- trunk/KDE/kdegames/doc/kajongg/index.docbook #1284539:1284540 @@ -628,31 +628,61 @@ Sound - It is possible to record sound files for your announcements which will be heard by the other players. There is some manual work needed: + Playing gives you a much nicer experience if you hear players claim tiles and announce discards. It also makes playing + easier because you can concentrate on your own tiles, you do not have to watch what others discard. + + &kajongg; comes with voices for different players. It automatically assigns voices to players. For every language there should be + four voices, preferably two male and two female. &kajongg; tries to use voices in the same language &kajongg; runs in. If it + finds no voices in that language, &kajongg; will fallback to other languages, using the same fallback languages you defined for + &kajongg; (see HelpSwitch Application Language). If no + voice files exist yet for your language, you are invited to record and contribute them to &kajongg;. + + + + This is how you can generate voices yourself. There is a separate directory for every voice. There are two groups of voices + with different places in the file system: - First you need to find out where to store your voice files. This folder normally is ~/.kde/share/apps/kajongg. So if your login name is joe, - this might be /home/joe/.kde/share/apps/kajongg. In there you will also find the database kajongg.db. + Predefined voices for every language. They live in a directory like /usr/share/kde4/apps/kajongg/voices/LANG where + LANG is the code for your language. The voice directories directly under .../kajongg/voices/ are the US-english + versions. Predefined voices are randomly assigned to players. Those voices have names like "male1 male2 female1 female2" but those names do not matter, the user will never see them. Also, it makes no sense to define more than four predefined voices per language. - In the folder you just located, generate a new subfolder with the name voices. + User voices can be defined for a specific user. + If &kajongg; makes use of such a voice, it can automatically be transferred to other players + over the internet. + Those voices live in a directory like ~/.kde/share/apps/kajongg/voices/. + So if your login name is joe, this might be /home/joe/.kde/share/apps/kajongg/voices/joe/. + The directory voices/ might not yet exist - if so, create it. + - In the folder voices generate another subfolder with your player name. This is the name you use when - connecting to a game server. You may also want to define voices for the players ROBOT 1, ROBOT 2, ROBOT 3. + Now that you know where to place your new voice, generate a new directory for it. In that subfolder, you can place the sound files. They have to be encoded with the Ogg Vorbis audio compressor, the file names need to have the extension .ogg. + A single file has a name like s3.ogg - this is Stone 3. You should define such files for all tiles and + for all claims and announcements. But if a sound file is missing, nothing bad happens - you just will not hear that sound. Fallback + to other voices or languages only works for entire voices, not for single sound files. - A single file has a name like s3.ogg - this is Stone 3. You should define such files for all tiles and - for all claims and announcements. + After having defined all sound files for a predefined voice, please make sure that the &kajongg; process has write permission in + the new voice directory. Start &kajongg; and test the voice. After everything is OK, you can revoke write permission (and you should + do so for predefined voices). &kajongg; will automatically generate a file named md5sum containing a checksum + over all sound files. Whenever you change, add or delete a sound file for a voice, &kajongg; will try to rewrite that file. - Please make the length of the sound short but speak clearly - this game can be played very fast when you get used to it. It is easier if you first write down a list with everything that should be spoken. Do not read it as a sequence of items, try to pronounce each item as if it were the only one and as you would when playing. Reading this needs a little practicing. - This is just one possibility to create the sound files: + + Please make the length of the sound short but speak clearly - this game can be played very fast when you get used to it. It is easier + if you first write down a list with everything that should be spoken. Do not read it as a sequence of items, try to pronounce each + item as if it were the only one and as you would when playing. Leave room between the items. Reading this needs a little practicing. + + This is just one possibility to create the sound files: Record a sound file with the program qarecord, save it in the WAV format. In this single sound file, speak all of the sounds. Edit that file with the sound editor audacity. In that editor, use the mouse to mark a range of the sound waves. - The editor will play that part. Repeat until you have the correct part selected. This may be easier if you ViewZoom In. Use the menu command FileExport Selection and save in the ogg format. Under Options, choose low quality - this is sufficient and even + The editor will play that part. Repeat until you have the correct part selected. + This may be easier if you ViewZoom In. + Use the menu command FileExport Selection and save in the ogg format. + Under Options, choose low quality - this is sufficient and even wanted because this reduces the time to transfer the sounds to the other players. The following are the names of the files that you should generate. Always append .ogg to them: --- trunk/KDE/kdegames/kajongg/src/game.py #1284539:1284540 @@ -29,7 +29,7 @@ from tile import Tile from meld import tileKey from scoringengine import HandContent -from sound import Voice, Sound +from sound import Voice from wall import Wall from move import Move from animation import Animated @@ -264,8 +264,6 @@ def assignVoices(self): """now we have all remote user voices""" assert self.belongsToHumanPlayer() - if not Sound.enabled: - return available = Voice.availableVoices()[:] # available is without transferred human voices for player in self.players: @@ -278,11 +276,19 @@ if player.voice: if Debug.sound: logDebug('%s has own local voice %s' % (player.name, player.voice)) - if player.voice in available: - available.remove(player.voice) + if player.voice: + for voice in Voice.availableVoices(): + if voice in available and voice.md5sum == player.voice.md5sum: + # if the local voice is also predefined, + # make sure we do not use both + available.remove(voice) + # for the other players use predefined voices in preferred language. Only if + # we do not have enough predefined voices, look again in locally defined voices + predefined = [x for x in available if x.language() != 'local'] + predefined.extend(available) for player in self.players: - if player.voice is None and available: - player.voice = available.pop() + if player.voice is None and predefined: + player.voice = predefined.pop(0) if Debug.sound: logDebug('%s gets one of the still available voices %s' % (player.name, player.voice)) --- trunk/KDE/kdegames/kajongg/src/message.py #1284539:1284540 @@ -400,14 +400,15 @@ """we got voice sounds from the server, assign them to the player voice""" def clientAction(self, dummyClient, move): """server sent us voice sounds about somebody else""" + move.player.voice = Voice(move.md5sum, move.source) if Debug.sound: - logDebug('%s gets voice data %s from server' % ( - move.player, move.player.voice)) - move.player.voice = Voice(move.md5sum, move.source) + logDebug('%s gets voice data %s from server, language=%s' % ( + move.player, move.player.voice, move.player.voice.language())) class MessageAssignVoices(ServerMessage): """The server tells us that we now got all voice data available""" def clientAction(self, client, move): + if Sound.enabled: client.game.assignVoices() class MessageClientWantsVoiceData(ClientMessage): --- trunk/KDE/kdegames/kajongg/src/sound.py #1284539:1284540 @@ -24,8 +24,15 @@ import winsound # pylint: disable=F0401 import common -from util import which, logWarning, m18n, appdataDir, cacheDir, logDebug, \ - removeIfExists +from util import which, logWarning, m18n, cacheDir, logDebug, \ + removeIfExists, logException + +try: + from kde import KGlobal, KConfigGroup + HAVE_KDE = True +except BaseException: + HAVE_KDE = False + from common import Debug from meld import Meld @@ -138,29 +145,59 @@ def __repr__(self): return "" % self + def language(self): + """the language code of this voice. Locally defined voices + have no language code and return 'local'. + remote voices received from other clients return 'remote', + they always get predecence.""" + if len(self.directory) == 32: + if os.path.split(self.directory)[1] == self.md5sum: + # TODO: test this. Needs separate computers for server and client. + return 'remote' + if self.directory.startswith(os.environ['HOME']): + # TODO: how is this on Windows? + return 'local' + result = os.path.split(self.directory)[0] + result = os.path.split(result)[1] + if result == 'voices': + result = 'en_US' + return result + + @staticmethod def availableVoices(): """a list of all voice directories""" - if not Voice.__availableVoices: - ownVoices = os.path.join(appdataDir(), 'voices') - if not os.path.exists(ownVoices): - # happens if we use an empty $HOME for testing - os.makedirs(ownVoices) - voices = [os.path.join(ownVoices, x) for x in sorted(os.listdir(ownVoices))] - voices = [x for x in voices if os.path.exists(os.path.join(x, 's1.ogg'))] - Voice.__availableVoices = list(Voice(x) for x in voices) + if not Voice.__availableVoices and HAVE_KDE: + result = [] + for parentDirectory in KGlobal.dirs().findDirs("appdata", "voices"): + parentDirectory = unicode(parentDirectory) + for (dirpath, _, _) in os.walk(parentDirectory, followlinks=True): + if os.path.exists(os.path.join(dirpath, 's1.ogg')): + result.append(Voice(dirpath)) + config = KGlobal.config() + group = KConfigGroup(config, 'Locale') + prefLanguages = str(group.readEntry('Language')).split(':') + prefLanguages.insert(0, 'local') + if 'en_US' not in prefLanguages: + prefLanguages.append('en_US') + prefLanguages = dict((x[1], x[0]) for x in enumerate(prefLanguages)) + result = sorted(result, key=lambda x: prefLanguages.get(x.language(), 9999)) + if Debug.sound: + logDebug('found voices:%s' % [str(x) for x in result]) + Voice.__availableVoices = result return Voice.__availableVoices @staticmethod def locate(name): - """returns Voice or None if no voice matches""" + """returns Voice or None if no foreign or local voice matches. + In other words never return a predefined voice""" for voice in Voice.availableVoices(): dirname = os.path.split(voice.directory)[-1] if name == voice.md5sum: if Debug.sound: logDebug('locate found %s by md5sum in %s' % (name, voice.directory)) return voice - elif name == dirname: + elif name == dirname and voice.language() == 'local': if Debug.sound: logDebug('locate found %s by name in %s' % (name, voice.directory)) return voice @@ -205,19 +242,20 @@ md5sum.update(open(os.path.join(self.directory, oggFile)).read()) # the md5 stamp goes into the old archive directory 'username' self.__md5sum = md5sum.hexdigest() - if os.path.exists(md5FileName): - existingMd5sum = open(md5FileName, 'r').readlines()[0].strip() - else: - existingMd5sum = None + existingMd5sum = self.savedmd5Sum() + md5Name = self.md5FileName() if self.__md5sum != existingMd5sum: if Debug.sound: - if not os.path.exists(md5FileName): - logDebug('creating new %s' % md5FileName) + if not os.path.exists(md5Name): + logDebug('creating new %s' % md5Name) else: - logDebug('md5sum %s changed, rewriting %s with %s' % (existingMd5sum, md5FileName, self.__md5sum)) - open(md5FileName, 'w').write('%s\n' % self.__md5sum) + logDebug('md5sum %s changed, rewriting %s with %s' % (existingMd5sum, md5Name, self.__md5sum)) + try: + open(md5Name, 'w').write('%s\n' % self.__md5sum) + except BaseException, exception: + logException(m18n('cannot write %1: %2', md5Name, str(exception))) if archiveExists: - archiveIsOlder = os.path.getmtime(md5FileName) > os.path.getmtime(self.archiveName()) + archiveIsOlder = os.path.getmtime(md5Name) > os.path.getmtime(self.archiveName()) if self.__md5sum != existingMd5sum or archiveIsOlder: os.remove(self.archiveName()) @@ -234,6 +272,15 @@ """ the full path of the archive file""" return os.path.join(self.directory, 'content.tbz') + def md5FileName(self): + """the name of the md5sum file""" + return os.path.join(self.directory, 'md5sum') + + def savedmd5Sum(self): + """returns the current value of the md5sum file""" + if os.path.exists(self.md5FileName()): + return open(self.md5FileName(), 'r').readlines()[0].strip() + @apply def md5sum(): """the current checksum over all ogg files"""