Skip to content

Commit

Permalink
Merge pull request #32 from mikolysz/AVSpeechSynthesizer
Browse files Browse the repository at this point in the history
Rewrite the Mac speech server to use AVSpeechSynthesizer
  • Loading branch information
tspivey authored Nov 15, 2023
2 parents 1c1c6b6 + 2d4f0c2 commit e1d574f
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 80 deletions.
104 changes: 26 additions & 78 deletions mac
Original file line number Diff line number Diff line change
Expand Up @@ -5,63 +5,16 @@ from Foundation import (
NSObject, NSFileHandle, NSNotificationCenter,
NSFileHandleReadCompletionNotification, NSFileHandleNotificationDataItem,
)
from AppKit import NSSpeechSynthesizer, NSSpeechRecentSyncProperty
from AVFoundation import AVSpeechSynthesizer, AVSpeechUtterance, AVSpeechBoundaryImmediate, AVSpeechSynthesisVoice
from PyObjCTools import AppHelper
from objc import python_method
import threading

rate = None
volume = None

class QueuedSynth(NSObject):
def init_(self, handler):
self = super(QueuedSynth, self).init()
self.synth = NSSpeechSynthesizer.alloc().init()
self.speaking = False
self.lock = threading.Lock()
self.synth.setDelegate_(self)
self.queue = []
self.last_index = None
self.handle_index = handler
return self

@python_method
def speak(self, text, index=None):
with self.lock:
self.queue.append((text, index))
if self.speaking:
return
self.speak_more()

def speechSynthesizer_didFinishSpeaking_(self, synth, success):
with self.lock:
if not self.speaking:
return
if self.last_index:
self.handle_index(self.last_index)
if len(self.queue) == 0:
self.speaking = False
return
self.speak_more()

def speak_more(self):
text, index = self.queue.pop(0)
res = self.synth.startSpeakingString_(text)
if res == False:
self.speaking = False
return
self.speaking = True
self.last_index = index

def stop(self):
with self.lock:
if not self.speaking: return
self.synth.stopSpeaking()
self.queue = []

def speechSynthesizer_didEncounterSyncMessage_(self, synth, message):
n = synth.objectForProperty_error_(NSSpeechRecentSyncProperty, None)[0]
self.handle_index(n)
voice_idx = None
voices = AVSpeechSynthesisVoice.speechVoices()
avsynth = AVSpeechSynthesizer.new()

class FileObserver(NSObject):
def initWithFileDescriptor_readCallback_errorCallback_(self,
Expand Down Expand Up @@ -121,48 +74,43 @@ def gotLine(observer, line):
for l in line.split(b'\n'):
handle_line(l)



def handle_line(line):
global rate, volume
global rate, volume, voice_idx
line = line.decode('utf-8', 'replace')
if line[0] == u"s":
l = ""
if rate:
l += u"[[rate %s]]" % rate
if volume:
l += u"[[volm %s]]" % volume
l += line[1:].replace('[[', ' ')
if line[0] == u"s" or line[0] == "l":
l = line[1:].replace('[[', ' ')
l = l.replace(u'\u23ce', ' ')
synth.speak(l)
u = AVSpeechUtterance.alloc().initWithString_(l)
u.setPrefersAssistiveTechnologySettings_(True)
if rate is not None:
u.setRate_(rate)
if volume is not None:
u.setVolume_(volume)
if voice_idx is not None:
u.setVoice_(voices[voice_idx])
avsynth.speakUtterance_(u)
elif line[0] == u"x":
synth.stop()
elif line[0] == u"l":
prefix = u"[[char ltrl]]"
if rate:
prefix += u"[[rate %s]]" % rate
if volume:
prefix += u"[[volm %s]]" % volume
suffix = "[[char norm]]"
if len(line) == 2 and line[1].isupper():
prefix += u"[[pbas +10]]"
suffix += u"[[pbas -10]]"
synth.speak(prefix+line[1:]+suffix)
avsynth.stopSpeakingAtBoundary_(AVSpeechBoundaryImmediate)
elif line[0] == u"r":
rate = line[1:]
rate = float(line[1:])/100
elif line[0] == u"v":
volume = str(int(line[1:]) / 100.0)
volume = int(line[1:]) / 100.0
elif line[0] == "V":
voice_idx = int(line[1:])
if voice_idx >= len(voices):
voice_idx = None

def gotError(observer, err):
print("error:", err)
AppHelper.stopEventLoop()

synth = None

def main():
global synth
import sys
observer = FileObserver.alloc().initWithFileDescriptor_readCallback_errorCallback_(
sys.stdin.fileno(), gotLine, gotError)
synth = QueuedSynth.alloc().init_(None)

AppHelper.runConsoleEventLoop(installInterrupt=True)

if __name__ == '__main__':
Expand Down
30 changes: 28 additions & 2 deletions tdsr
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ class ConfigHandler(KeyHandler):
self.keymap = {
b'r': self.set_rate,
b'v': self.set_volume,
b'V': self.set_voice_idx,
b'p': self.set_process_symbols,
b'd': self.set_delay,
b'e': self.set_echo,
Expand Down Expand Up @@ -147,6 +148,22 @@ class ConfigHandler(KeyHandler):
state.save_config()
say("Confirmed")

def set_voice_idx(self):
say("voice index")
state.key_handlers.append(BufferHandler(on_accept=self.set_voice_idx2))

def set_voice_idx2(self, val):
try:
val = int(val)
except ValueError:
say("Invalid value")
return
synth.set_voice_idx(val)
state.config['speech']['voice_idx'] = str(val)
state.save_config()
say("Confirmed")


def set_process_symbols(self):
current = state.config.getboolean('speech', 'process_symbols', fallback=False)
current = not current
Expand Down Expand Up @@ -246,13 +263,16 @@ class Synth:
self.speech_server = speech_server
self.rate = None
self.volume = None
self.voice_idx = None

def start(self):
self.pipe = subprocess.Popen(self.speech_server, stdin=subprocess.PIPE)
if self.rate:
if self.rate is not None:
self.set_rate(self.rate)
if self.volume:
if self.volume is not None:
self.set_volume(self.volume)
if self.voice_idx is not None:
self.set_voice_idx(self.voice_idx)

def send(self, text):
text = text.encode('utf-8')
Expand All @@ -275,6 +295,10 @@ class Synth:
self.volume = volume
self.send('v%d\n' % volume)

def set_voice_idx(self, voice_idx):
self.voice_idx = voice_idx
self.send('V%d\n' % voice_idx)

def close(self):
self.pipe.stdin.close()
self.pipe.wait()
Expand Down Expand Up @@ -317,6 +341,8 @@ def main():
synth.set_rate(int(state.config['speech']['rate']))
if 'volume' in state.config['speech']:
synth.set_volume(int(state.config['speech']['volume']))
if 'voice_idx' in state.config['speech']:
synth.set_voice_idx(int(state.config['speech']['voice_idx']))
if 'cursor_delay' in state.config['speech']:
CURSOR_TIMEOUT = float(state.config['speech']['cursor_delay'])
pid, fd = os.forkpty()
Expand Down

0 comments on commit e1d574f

Please sign in to comment.