diff --git a/source/synthDriverHandler.py b/source/synthDriverHandler.py
index 46b0b5d..6fbd361 100644
--- a/source/synthDriverHandler.py
+++ b/source/synthDriverHandler.py
@@ -156,6 +156,7 @@ class SynthDriver(baseObject.AutoPropertyObject):
 	
 	At a minimum, synth drivers must set L{name} and L{description} and override the L{check} method.
 	The methods L{speak}, L{cancel} and L{pause} should be overridden as appropriate.
+	L{busy} attribute is needed to support virtualSynthDriver.
 	L{supportedSettings} should be set as appropriate for the settings supported by the synthesiser.
 	There are factory functions to create L{SynthSetting} instances for common settings; e.g. L{VoiceSetting} and L{RateSetting}.
 	Each setting is retrieved and set using attributes named after the setting;
@@ -317,6 +318,9 @@ class SynthDriver(baseObject.AutoPropertyObject):
 		"""Silence speech immediately.
 		"""
 
+	def _get_busy(self):
+		return False
+
 	def _get_language(self):
 		return self.availableVoices[self.voice].language
 
diff --git a/source/synthDrivers/audiologic.py b/source/synthDrivers/audiologic.py
index fa17964..bc51ed5 100755
--- a/source/synthDrivers/audiologic.py
+++ b/source/synthDrivers/audiologic.py
@@ -82,3 +82,7 @@ class SynthDriver(SynthDriver):
 			_audiologic.TtsPause()
 		else:
 			_audiologic.TtsRestart()
+
+	def _get_busy(self):
+		_audiologic.TtsGetStatus()
+		return _audiologic.Tts3Status().SynthStatus != _audiologic.SYNTH_IDLE
diff --git a/source/synthDrivers/espeak.py b/source/synthDrivers/espeak.py
index d7ebac5..0ae90ee 100644
--- a/source/synthDrivers/espeak.py
+++ b/source/synthDrivers/espeak.py
@@ -176,3 +176,6 @@ class SynthDriver(SynthDriver):
 
 	def _getAvailableVariants(self):
 		return OrderedDict((ID,VoiceInfo(ID, name)) for ID, name in self._variantDict.iteritems())
+
+	def _get_busy(self):
+		return _espeak.isSpeaking
diff --git a/source/synthDrivers/multilang-cjk.py b/source/synthDrivers/multilang-cjk.py
new file mode 100644
index 0000000..25daa53
--- /dev/null
+++ b/source/synthDrivers/multilang-cjk.py
@@ -0,0 +1,252 @@
+﻿#synthDrivers/multilang-cjk.py
+#A part of NonVisual Desktop Access (NVDA)
+#This file is covered by the GNU General Public License.
+#See the file COPYING for more details.
+#Copyright 2011 World Light Information Limited and Hong Kong Blind Union.
+
+from collections import OrderedDict
+import globalVars
+import speech
+from logHandler import log
+from characterProcessing import processSpeechSymbol
+from languageHandler import getLanguage
+from synthDriverHandler import SynthDriver,SynthSetting,NumericSynthSetting,VoiceInfo,getSynthList
+from virtualSynthDriver import VirtualSynthDriver
+
+_defaultCjkSynth="espeak"
+_defaultWesternSynth="sapi5"
+
+class SynthDriver(VirtualSynthDriver):
+	name="multilang-cjk"
+	description=_("CJK dual-voice virtual synthesizer")
+
+	supportedSettings=(
+		SynthSetting("synth1",_("Primar&y synthesizer")),
+		NumericSynthSetting("rate1",_("Primary &rate")),
+		NumericSynthSetting("pitch1",_("Primary &pitch")),
+		NumericSynthSetting("inflection1",_("Primary &inflection")),
+		NumericSynthSetting("volume1",_("Primary v&olume")),
+		SynthSetting("synth2",_("&English synthesizer")),
+		NumericSynthSetting("rate2",_("English r&ate")),
+		NumericSynthSetting("pitch2",_("English pi&tch")),
+		NumericSynthSetting("inflection2",_("English in&flection")),
+		NumericSynthSetting("volume2",_("English vol&ume")),
+		SynthSetting("cjk_number",_("Read nu&mbers using")),
+		SynthSetting("spell_digit",_("Speak individual &digit")),
+	)
+
+	cjkSynth=_defaultCjkSynth
+	westernSynth=_defaultWesternSynth
+	cjkNumber=True
+	spellDigit=True
+
+	@classmethod
+	def check(cls):
+		return True
+
+	def __init__(self):
+		# Setup default parameters
+		self.synth1=_defaultCjkSynth
+		self.rate1=50
+		self.pitch1=50
+		self.inflection1=50
+		self.volume1=100
+		self.synth2=_defaultWesternSynth
+		self.rate2=50
+		self.pitch2=50
+		self.inflection2=50
+		self.volume2=100
+
+		# Build voice dictionary from available synth drivers
+		self.synthDict=OrderedDict()
+		for x in getSynthList():
+			if x[0]==self.name: continue
+			self.synthDict[x[0]]=VoiceInfo(unicode(x[0],"utf-8"),x[1])
+
+	def initSynths(self):
+		self.unloadAllSynthsExcept((self.cjkSynth,self.westernSynth))
+		if not self.loadSynth(self.cjkSynth):
+			# Fall back to espeak upon failure
+			self.cjkSynth="espeak"
+		if not self.loadSynth(self.westernSynth):
+			# Fall back to espeak upon failure
+			self.westernSynth="espeak"
+		self.setMasterSynth(self.cjkSynth)
+
+	def _get_availableSynth1s(self):
+		# CJK synthesizer
+		return self.synthDict
+
+	def _get_synth1(self):
+		return self.cjkSynth
+
+	def _set_synth1(self,synth1):
+		self.cjkSynth=synth1
+		self.initSynths()
+
+	def _get_rate1(self):
+		return self.getSynthSetting(self.cjkSynth, "rate")
+
+	def _set_rate1(self,rate):
+		return self.setSynthSetting(self.cjkSynth, "rate", rate)
+
+	def _get_pitch1(self):
+		return self.getSynthSetting(self.cjkSynth, "pitch")
+
+	def _set_pitch1(self,pitch):
+		return self.setSynthSetting(self.cjkSynth, "pitch", pitch)
+
+	def _get_inflection1(self):
+		return self.getSynthSetting(self.cjkSynth, "inflection")
+
+	def _set_inflection1(self,inflection):
+		return self.setSynthSetting(self.cjkSynth, "inflection", inflection)
+
+	def _get_volume1(self):
+		return self.getSynthSetting(self.cjkSynth, "volume")
+
+	def _set_volume1(self,volume):
+		return self.setSynthSetting(self.cjkSynth, "volume", volume)
+
+	def _get_availableSynth2s(self):
+		# Western synthesizer
+		return self.synthDict
+
+	def _get_synth2(self):
+		return self.westernSynth
+
+	def _set_synth2(self,synth2):
+		self.westernSynth=synth2
+		self.initSynths()
+
+	def _get_synth2(self):
+		return self.westernSynth
+
+	def _get_rate2(self):
+		return self.getSynthSetting(self.westernSynth, "rate")
+
+	def _set_rate2(self,rate):
+		return self.setSynthSetting(self.westernSynth, "rate", rate)
+
+	def _get_pitch2(self):
+		return self.getSynthSetting(self.westernSynth, "pitch")
+
+	def _set_pitch2(self,pitch):
+		return self.setSynthSetting(self.westernSynth, "pitch", pitch)
+
+	def _get_inflection2(self):
+		return self.getSynthSetting(self.westernSynth, "inflection")
+
+	def _set_inflection2(self,inflection):
+		return self.setSynthSetting(self.westernSynth, "inflection", inflection)
+
+	def _get_volume2(self):
+		return self.getSynthSetting(self.westernSynth, "volume")
+
+	def _set_volume2(self,volume):
+		return self.setSynthSetting(self.westernSynth, "volume", volume)
+
+	def _get_availableSpell_digits(self):
+		dict=OrderedDict()
+		dict[u"yes"]=VoiceInfo(u"yes",_("Yes"))
+		dict[u"no"]=VoiceInfo(u"no",_("No"))
+		return dict
+
+	def _get_spell_digit(self):
+		return "yes" if self.spellDigit else "no"
+
+	def _set_spell_digit(self,spell_digit):
+		self.spellDigit=True if spell_digit=="yes" else False
+
+	def _get_availableCjk_numbers(self):
+		dict=OrderedDict()
+		dict[u"primary"]=VoiceInfo(u"primary",_("Primary synthesizer"))
+		dict[u"english"]=VoiceInfo(u"english",_("English synthesizer"))
+		return dict
+
+	def _get_cjk_number(self):
+		return "primary" if self.cjkNumber else "english"
+
+	def _set_cjk_number(self,cjk_number):
+		self.cjkNumber=True if cjk_number=="primary" else False
+
+	@classmethod
+	def isWestern(cls,c):
+		if (c>='A' and c<='Z') or (c>='a' and c<='z'):
+			return True
+		return False
+
+	def speakNow(self,text,index):
+		locale=getLanguage()
+		prevSynth=self.cjkSynth
+		portion=u""
+		maxLen=len(text)
+		for i in range(maxLen+1):
+			if i==maxLen:
+				# Always speak last portion at end of text
+				c=u""
+				synth=None
+			else:
+				c=text[i]
+				if c<=' ':
+					# Space and control codes do not alter language
+					synth=prevSynth
+				elif c>='0' and c<='9':
+					# CJK synth read digits
+					if self.spellDigit: c+=' '
+					synth=self.cjkSynth if self.cjkNumber else self.westernSynth
+				elif c=='.':
+					# Check if full-stop is between digits
+					if i==0 or i>=(maxLen-1):
+						synth=self.westernSynth
+					elif text[i-1]<'0' or text[i-1]>'9':
+						synth=self.westernSynth
+					elif text[i+1]<'0' or text[i+1]>'9':
+						synth=self.westernSynth
+					elif not self.cjkNumber:
+						synth=self.westernSynth
+					else:
+						# CJK synth read decimal point
+						c=processSpeechSymbol(locale,u'decimal point')
+						if len(c)==0:
+							c=processSpeechSymbol(locale,u'.')
+						synth=self.cjkSynth
+				elif self.isWestern(c):
+					# Western synth read western characters
+					synth=self.westernSynth
+				elif ord(c)<=255 and i>0 and self.isWestern(text[i-1]):
+					# Western synth read symbol after western text
+					synth=self.westernSynth
+				elif ord(c)<=255 and i<(maxLen-1) and self.isWestern(text[i+1]):
+					# Western synth read symbol before western text
+					synth=self.westernSynth
+				else:
+					# CJK synth read everything else
+					synth=self.cjkSynth
+			if synth==prevSynth:
+				portion+=c
+			else:
+				portion=portion.strip()
+				if len(portion)>0:
+					self.speakPortion(prevSynth,portion,index)
+				prevSynth=synth
+				portion=c
+
+	def speak(self,speechSequence):
+		lastIndex=None
+		charMode=False
+		for item in speechSequence:
+			if isinstance(item,basestring):
+				if charMode:
+					text=' '.join([x for x in unicode(item)])
+				else:
+					text=unicode(item)
+				self.speakNow(text,lastIndex)
+			elif isinstance(item,speech.IndexCommand):
+				lastIndex=item.index
+			elif isinstance(item,speech.CharacterModeCommand):
+				charMode=item.state
+			elif isinstance(item,speech.SpeechCommand):
+				log.debugWarning("Unsupported speech command: %s"%item)
+			else:
+				log.error("Unknown speech: %s"%item)
diff --git a/source/synthDrivers/sapi4.py b/source/synthDrivers/sapi4.py
index 9017d2c..74fad66 100755
--- a/source/synthDrivers/sapi4.py
+++ b/source/synthDrivers/sapi4.py
@@ -24,6 +24,9 @@ class SynthDriverBufSink(COMObject):
 		self._allowDelete = True
 		super(SynthDriverBufSink,self).__init__()
 
+	def ITTSBufNotifySink_TextDataDone(self, this, qTimeStamp, dwFlags):
+		self._synthDriver._busy=False
+
 	def ITTSBufNotifySink_BookMark(self, this, qTimeStamp, dwMarkNum):
 		self._synthDriver.lastIndex=dwMarkNum
 
@@ -75,6 +78,7 @@ class SynthDriver(SynthDriver):
 		if len(self._enginesList)==0:
 			raise RuntimeError("No Sapi4 engines available")
 		self.voice=str(self._enginesList[0].gModeID)
+		self._busy=False
 
 	def terminate(self):
 		self._bufSink._allowDelete = True
@@ -99,11 +103,13 @@ class SynthDriver(SynthDriver):
 			textList.append("\\RmS=0\\")
 		text="".join(textList)
 		flags=TTSDATAFLAG_TAGGED
+		self._busy=True
 		self._ttsCentral.TextData(VOICECHARSET.CHARSET_TEXT, flags,TextSDATA(text),self._bufSinkPtr,ITTSBufNotifySink._iid_)
 
 	def cancel(self):
 		self._ttsCentral.AudioReset()
 		self.lastIndex=None
+		self._busy=False
 
 	def pause(self,switch):
 		if switch:
@@ -251,3 +257,6 @@ class SynthDriver(SynthDriver):
 		val=self._percentToParam(val,self._minVolume&0xffff,self._maxVolume&0xffff)
 		val+=val<<16
 		self._ttsAttrs.VolumeSet(val)
+
+	def _get_busy(self):
+		return self._busy
diff --git a/source/synthDrivers/sapi5.py b/source/synthDrivers/sapi5.py
index 1c866de..d4dedf8 100644
--- a/source/synthDrivers/sapi5.py
+++ b/source/synthDrivers/sapi5.py
@@ -154,3 +154,6 @@ class SynthDriver(SynthDriver):
 	def pause(self,switch):
 		if switch:
 			self.cancel()
+
+	def _get_busy(self):
+		return self.tts.Status.RunningState != 1
diff --git a/source/virtualSynthDriver.py b/source/virtualSynthDriver.py
new file mode 100644
index 0000000..2599401
--- /dev/null
+++ b/source/virtualSynthDriver.py
@@ -0,0 +1,313 @@
+﻿#virtualSynthDriver.py
+#A part of NonVisual Desktop Access (NVDA)
+#This file is covered by the GNU General Public License.
+#See the file COPYING for more details.
+#Copyright 2011-2013 World Light Information Limited and Hong Kong Blind Union.
+
+from collections import OrderedDict
+import config
+import globalVars
+import queueHandler
+from logHandler import log
+from synthSettingsRing import SynthSettingsRing
+from speechDictHandler import loadVoiceDict
+from speech import IndexCommand
+import synthDriverHandler
+
+class _SavedSettings(object):
+	"""Stores original settings of a physical synthesizer."""
+
+	def __init__(self,synth):
+		self.params={
+			'rate':synth.rate,
+			'pitch':synth.pitch,
+			'inflection':synth.inflection,
+			'volume':synth.volume
+		}
+
+	def adjustParams(self,synth,rate,pitch,inflection,volume):
+		"""Make relative changes to the parameters of a synthsizer instance.
+
+		All relative parameters are ranged between 0 and 100.  0 means half of
+		the original value;  50 means original value;  100 means twice the
+		original value.
+		@param synth: Synthesizer that requires parameter update.
+		@type synth: L{SynthDriver}
+		@param rate: Relative rate.
+		@type rate: int
+		@param pitch: Relative pitch.
+		@type pitch: int
+		@param inflection: Relative inflection.
+		@type inflection: int
+		@param volume: Relative volume.
+		@type volume: int
+		"""
+		synth.rate=self._relativeToActual(rate,self.params['rate'])
+		synth.pitch=self._relativeToActual(pitch,self.params['pitch'])
+		synth.inflection=self._relativeToActual(inflection,self.params['inflection'])
+		synth.volume=self._relativeToActual(volume,self.params['volume'])
+
+	@classmethod
+	def _relativeToActual(cls,relative,oldValue):
+		"""Converts a percentage relatively.
+		@param relative: The relative adjustment.  Ranges between 0 and 100.
+		@type relative: int
+		@param oldValue: The original percentage value.
+		@type oldValue: int
+		"""
+		if relative<0:
+			return oldValue
+		elif relative<50:
+			min=oldValue/2
+			max=oldValue
+			percent=relative*2
+			return int(round(float(percent)/100*(max-min)+min))
+		elif relative==50:
+			return oldValue
+		elif relative<=100:
+			min=oldValue
+			max=oldValue*2
+			if max>100: max=100
+			percent=(relative-50)*2
+			return int(round(float(percent)/100*(max-min)+min))
+		else:
+			return oldValue
+
+class VirtualSynthDriver(synthDriverHandler.SynthDriver):
+	"""Abstract virtual synthesizer that manages multiple drivers.
+
+	Support for rate, pitch, inflection and volume is provided by this class.
+	At a minimum, multi-language synthesizer drivers inheriting from this class must override the L{speak} methods.
+	The L{speak} method should break the text into portions and feed each portion to a different synthesizer by calling L{speakPortion}.
+	"""
+	synths={}
+	settings={}
+	curSynth=None
+	genID=None
+	queue=[]
+	_lastIndex=None
+
+	# Default parameters
+	relativeRate=50
+	relativePitch=50
+	relativeInflection=50
+	relativeVolume=50
+
+	def isSupported(self,settingName):
+		if settingName in ("rate", "pitch", "inflection", "volume"):
+			return True
+		return super(VirtualSynthDriver,self).isSupported(settingName)
+
+	def saveSettings(self):
+		super(VirtualSynthDriver,self).saveSettings()
+		conf=config.conf["speech"][self.name]
+		conf["rate"]=self.relativeRate;
+		conf["pitch"]=self.relativePitch;
+		conf["inflection"]=self.relativeInflection;
+		conf["volume"]=self.relativeVolume;
+
+	def loadSettings(self):
+		conf=config.conf["speech"][self.name]
+		conf["rate"]=self.relativeRate;
+		conf["pitch"]=self.relativePitch;
+		conf["inflection"]=self.relativeInflection;
+		conf["volume"]=self.relativeVolume;
+		super(VirtualSynthDriver,self).loadSettings()
+
+	def pause(self,switch):
+		if self.curSynth in self.synths:
+			self.synths[self.curSynth].pause(switch)
+
+	def cancel(self):
+		del self.queue[:]
+		for name in self.synths.keys():
+			self.synths[name].cancel()
+		self.curSynth=None
+
+	def terminate(self):
+		"""Terminates this virtual synthesizer.
+		Subclasses should call the superclass method first.
+		"""
+		self.cancel()
+		if self.genID!=None:
+			queueHandler.cancelGeneratorObject(self.genID)
+		self.unloadAllSynthsExcept()
+
+	def _get_lastIndex(self):
+		return self._lastIndex
+
+	def _get_rate(self):
+		return self.relativeRate
+
+	def _set_rate(self,rate):
+		self.relativeRate=rate
+
+	def _get_pitch(self):
+		return self.relativePitch
+
+	def _set_pitch(self,pitch):
+		self.relativePitch=pitch
+
+	def _get_inflection(self):
+		return self.relativeInflection
+
+	def _set_inflection(self,inflection):
+		self.relativeInflection=inflection
+
+	def _get_volume(self):
+		return self.relativeVolume
+
+	def _set_volume(self,volume):
+		self.relativeVolume=volume
+
+	def _processPortion(self):
+		"""Speaks next text portion in queue with the requested synthesizer.
+		@return: C{True} if speech was started, C{False} if not.
+		@rtype: bool
+		"""
+		(name,portion,index,rate,pitch,inflection,volume)=self.queue.pop(0)
+		self._lastIndex=0 if index==None else index
+		if name in self.synths:
+			self.curSynth=name
+			synth=self.synths[name]
+			setting=self.settings[name]
+			setting.adjustParams(synth,rate,pitch,inflection,volume)
+			# Feed speech sequence to target synthesizer
+			speechSequence=[]
+			if index is not None:
+				speechSequence.append(IndexCommand(index))
+			speechSequence.append(portion)
+			synth.speak(speechSequence)
+			return True
+		else:
+			self.curSynth=None
+			return False
+
+	def _portionGenerator(self):
+		while True:
+			if self.curSynth!=None:
+				# Check whether current synthersizer has finished speaking
+				if not self.curSynth in self.synths:
+					self.curSynth=None
+				elif not self.synths[self.curSynth].busy:
+					self.curSynth=None
+			if self.curSynth==None and len(self.queue)>0:
+				# Process next portion when there is no active synth
+				self._processPortion()
+			yield
+
+	def loadSynth(self,name):
+		"""Adds a new synthesizer to the list of managed synthersizers.
+		@return: C{True} if the synthesizer is now managed, C{False} if not.
+		@rtype: bool
+		@param name: The name of the synthesizer that needs to be managed.
+		@type name: str
+		"""
+		if len(name)==0:
+			# Ignore blank names
+			return False
+		if name==self.name:
+			# Prevent recursive loading of synthesizer
+			return False
+		if name in self.synths:
+			# Open each synthesizer only once
+			return True
+		try:
+			newSynth=synthDriverHandler._getSynthDriver(name)()
+			if name in config.conf["speech"]:
+				newSynth.loadSettings()
+			else:
+				# Create the new section.
+				config.conf["speech"][name]={}
+				if newSynth.isSupported("voice"):
+					voice=newSynth.voice
+				else:
+					voice=None
+				synthDriverHandler.changeVoice(newSynth,voice)
+				newSynth.saveSettings()
+			# Start managing synthesizer now
+			self.synths[name]=newSynth
+			self.settings[name]=_SavedSettings(newSynth)
+			return True
+		except:
+			log.error("Error while importing SynthDriver %s"%name,exc_info=True)
+			return False
+
+	def unloadAllSynthsExcept(self,retained=()):
+		"""Unloads all synthesizers managed by this driver with exception.
+		@param retained: Names of synthesizer to retain under management.
+		@type retained: list or tuple of str
+		"""
+		self.cancel()
+		for name in self.synths.keys():
+			if name in retained: continue
+			self.synths[name].terminate()
+			del self.synths[name]
+			del self.settings[name]
+
+	def setMasterSynth(self,name):
+		"""Uses a synthesizer as the master and initializes NVDA accordingly.
+		@param name: The name of the master synthesizer.
+		@type name: str
+		"""
+		if name in self.synths:
+			synth=self.synths[name]
+			if synth.isSupported("voice"):
+				#We need to call changeVoice here so voice dictionaries can be managed
+				synthDriverHandler.changeVoice(synth,synth.voice)
+			else:
+				#start or update the synthSettingsRing (for those synths which do not support 'voice')
+				if globalVars.settingsRing:
+					globalVars.settingsRing.updateSupportedSettings(synth)
+				else:
+					globalVars.settingsRing=SynthSettingsRing(synth)
+				loadVoiceDict(synth)
+
+	def getSynthSetting(self,name,attr):
+		"""Obtains the setting of a managed synthesizer
+		@return: Value of the required setting.
+		@rtype: int
+		@param name: The name of the synthesizer.
+		@type name: str
+		@param attr: The name of the setting.
+		@type attr: str
+		"""
+		if name in self.synths:
+			setting=self.settings[name]
+			if attr in setting.params:  return setting.params[attr]
+		return 50
+
+	def setSynthSetting(self,name,attr,value):
+		"""Changes the setting of a managed synthesizer
+		@param name: The name of the synthesizer.
+		@type name: str
+		@param attr: The name of the setting.
+		@type attr: str
+		@param value: New value for the setting.
+		@type value: int
+		"""
+		if name in self.synths:
+			setting=self.settings[name]
+			if attr in setting.params:  setting.params[attr]=value
+
+	def speakPortion(self,name,portion,index):
+		"""Queues a text portion to be spoken by a particular synthesizer.
+		@param name: Name of the synthesizer that will speak the text portion.
+		@type name: str
+		@param portion: The text portion to be spoken.
+		@type portion: str
+		@param index: An index to associate with this text portion.
+		@type index: int
+		"""
+		if self.genID==None:
+			self.genID=queueHandler.registerGeneratorObject(self._portionGenerator())
+		if len(portion)>0:
+			self.queue.append([
+				name,
+				portion,
+				index,
+				self.relativeRate,
+				self.relativePitch,
+				self.relativeInflection,
+				self.relativeVolume
+			])
